diff --git a/.codeql-prebuild-cpp-Linux.sh b/.codeql-prebuild-cpp-Linux.sh new file mode 100644 index 00000000000..042ae12c828 --- /dev/null +++ b/.codeql-prebuild-cpp-Linux.sh @@ -0,0 +1,79 @@ +# install dependencies for C++ analysis +set -e + +CUDA_VERSION=11.8.0 +CUDA_BUILD=520.61.05 + +# install wget and cuda first +sudo apt-get update -y +sudo apt-get install -y \ + wget + +# Install CUDA +url_base="https://developer.download.nvidia.com/compute/cuda/${CUDA_VERSION}/local_installers" +url="${url_base}/cuda_${CUDA_VERSION}_${CUDA_BUILD}_linux.run" +sudo wget -q -O /root/cuda.run ${url} +sudo chmod a+x /root/cuda.run +sudo /root/cuda.run --silent --toolkit --toolkitpath=/usr/local/cuda --no-opengl-libs --no-man-page --no-drm +sudo rm /root/cuda.run + +# Install dependencies +sudo apt-get install -y \ + build-essential \ + gcc-10 \ + g++-10 \ + libayatana-appindicator3-dev \ + libavdevice-dev \ + libboost-filesystem-dev \ + libboost-locale-dev \ + libboost-log-dev \ + libboost-program-options-dev \ + libcap-dev \ + libcurl4-openssl-dev \ + libdrm-dev \ + libevdev-dev \ + libminiupnpc-dev \ + libmfx-dev \ + libnotify-dev \ + libnuma-dev \ + libopus-dev \ + libpulse-dev \ + libssl-dev \ + libva-dev \ + libvdpau-dev \ + libwayland-dev \ + libx11-dev \ + libxcb-shm0-dev \ + libxcb-xfixes0-dev \ + libxcb1-dev \ + libxfixes-dev \ + libxrandr-dev \ + libxtst-dev + +# clean apt cache +sudo apt-get clean +sudo rm -rf /var/lib/apt/lists/* + +# Update gcc alias +# https://stackoverflow.com/a/70653945/11214013 +sudo update-alternatives --install \ + /usr/bin/gcc gcc /usr/bin/gcc-10 100 \ + --slave /usr/bin/g++ g++ /usr/bin/g++-10 \ + --slave /usr/bin/gcov gcov /usr/bin/gcov-10 \ + --slave /usr/bin/gcc-ar gcc-ar /usr/bin/gcc-ar-10 \ + --slave /usr/bin/gcc-ranlib gcc-ranlib /usr/bin/gcc-ranlib-10 + +# build +mkdir -p build +cd build || exit 1 +cmake \ + -DCMAKE_CUDA_COMPILER=/usr/local/cuda/bin/nvcc \ + -G "Unix Makefiles" \ + .. +make -j"$(nproc)" + +# Delete CUDA +sudo rm -rf /usr/local/cuda + +# skip autobuild +echo "skip_autobuild=true" >> "$GITHUB_OUTPUT" diff --git a/.codeql-prebuild-cpp-Windows.sh b/.codeql-prebuild-cpp-Windows.sh new file mode 100644 index 00000000000..98b49cc8d6b --- /dev/null +++ b/.codeql-prebuild-cpp-Windows.sh @@ -0,0 +1,43 @@ +# install dependencies for C++ analysis +set -e + +# update pacman +pacman --noconfirm -Suy + +# install wget +pacman --noconfirm -S \ + wget + +# download working curl +wget https://repo.msys2.org/mingw/ucrt64/mingw-w64-ucrt-x86_64-curl-8.8.0-1-any.pkg.tar.zst + +# install dependencies +pacman -U --noconfirm mingw-w64-ucrt-x86_64-curl-8.8.0-1-any.pkg.tar.zst +pacman -Syu --noconfirm --ignore=mingw-w64-ucrt-x86_64-curl \ + base-devel \ + diffutils \ + gcc \ + git \ + make \ + mingw-w64-ucrt-x86_64-boost \ + mingw-w64-ucrt-x86_64-cmake \ + mingw-w64-ucrt-x86_64-cppwinrt \ + mingw-w64-ucrt-x86_64-graphviz \ + mingw-w64-ucrt-x86_64-miniupnpc \ + mingw-w64-ucrt-x86_64-nlohmann-json \ + mingw-w64-ucrt-x86_64-nodejs \ + mingw-w64-ucrt-x86_64-nsis \ + mingw-w64-ucrt-x86_64-onevpl \ + mingw-w64-ucrt-x86_64-openssl \ + mingw-w64-ucrt-x86_64-opus \ + mingw-w64-ucrt-x86_64-rust \ + mingw-w64-ucrt-x86_64-toolchain + +# build +mkdir -p build +cd build || exit 1 +cmake -G "MinGW Makefiles" .. +mingw32-make -j"$(nproc)" + +# skip autobuild +echo "skip_autobuild=true" >> "$GITHUB_OUTPUT" diff --git a/.codeql-prebuild-cpp-macOS.sh b/.codeql-prebuild-cpp-macOS.sh new file mode 100644 index 00000000000..4e74c8599e5 --- /dev/null +++ b/.codeql-prebuild-cpp-macOS.sh @@ -0,0 +1,20 @@ +# install dependencies for C++ analysis +set -e + +# install dependencies +brew install \ + boost \ + cmake \ + miniupnpc \ + node \ + opus \ + pkg-config + +# build +mkdir -p build +cd build || exit 1 +cmake -G "Unix Makefiles" .. +make -j"$(sysctl -n hw.logicalcpu)" + +# skip autobuild +echo "skip_autobuild=true" >> "$GITHUB_OUTPUT" diff --git a/.dockerignore b/.dockerignore index 6ada538cff0..a6ccb41e660 100644 --- a/.dockerignore +++ b/.dockerignore @@ -4,14 +4,19 @@ # do not ignore .git, needed for versioning !/.git +# do not ignore .rstcheck.cfg, needed to test building docs +!/.rstcheck.cfg + # ignore repo directories and files -docs/ +docker/ +gh-pages-template/ scripts/ tools/ crowdin.yml # ignore dev directories build/ +cmake-*/ venv/ # ignore artifacts diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000000..1ccee08a107 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +# ensure dockerfiles are checked out with LF line endings +Dockerfile text eol=lf +*.dockerfile text eol=lf diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index d701774ed37..64fa1b441aa 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -24,10 +24,10 @@ body: required: true - type: checkboxes attributes: - label: Is your issue present in the nightly release? - description: Please test the [nightly](https://github.com/LizardByte/Sunshine/releases/tag/nightly-dev) release + label: Is your issue present in the latest beta/pre-release? + description: Please test the latest [pre-release](https://github.com/LizardByte/Sunshine/releases) options: - - label: This issue is present in the nightly release + - label: This issue is present in the latest pre-release required: true - type: textarea id: description diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 88c8339c68f..6eb0cda2bd1 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -10,7 +10,6 @@ updates: schedule: interval: "daily" time: "08:00" - target-branch: "nightly" open-pull-requests-limit: 10 - package-ecosystem: "github-actions" @@ -18,7 +17,6 @@ updates: schedule: interval: "daily" time: "08:30" - target-branch: "nightly" open-pull-requests-limit: 10 - package-ecosystem: "npm" @@ -26,7 +24,6 @@ updates: schedule: interval: "daily" time: "09:00" - target-branch: "nightly" open-pull-requests-limit: 10 - package-ecosystem: "nuget" @@ -34,7 +31,6 @@ updates: schedule: interval: "daily" time: "09:30" - target-branch: "nightly" open-pull-requests-limit: 10 - package-ecosystem: "pip" @@ -42,7 +38,6 @@ updates: schedule: interval: "daily" time: "10:00" - target-branch: "nightly" open-pull-requests-limit: 10 - package-ecosystem: "gitsubmodule" @@ -50,5 +45,4 @@ updates: schedule: interval: "daily" time: "10:30" - target-branch: "nightly" open-pull-requests-limit: 10 diff --git a/.github/pr_release_template.md b/.github/pr_release_template.md deleted file mode 100644 index b6f6acf5847..00000000000 --- a/.github/pr_release_template.md +++ /dev/null @@ -1,28 +0,0 @@ -## Description - -This PR was created automatically. - - -### Screenshot - - - -### Issues Fixed or Closed - - - - - -## Type of Change -- [ ] Bug fix (non-breaking change which fixes an issue) -- [ ] New feature (non-breaking change which adds functionality) -- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) -- [ ] Dependency update (updates to dependencies) -- [ ] Documentation update (changes to documentation) -- [ ] Repository update (changes to repository files, e.g. `.github/...`) - -## Branch Updates -- [x] I want maintainers to keep my branch updated - -## Changelog Summary - diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index a2d7f99fb44..1d902563fce 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -3,14 +3,14 @@ name: CI on: pull_request: - branches: [master, nightly] + branches: [master] types: [opened, synchronize, reopened] push: - branches: [master, nightly] + branches: [master] workflow_dispatch: concurrency: - group: ${{ github.workflow }}-${{ github.ref }} + group: "${{ github.workflow }}-${{ github.ref }}" cancel-in-progress: true jobs: @@ -25,102 +25,23 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} - check_changelog: - name: Check Changelog - runs-on: ubuntu-latest - steps: - - name: Checkout - if: ${{ github.ref == 'refs/heads/master' || github.base_ref == 'master' }} - uses: actions/checkout@v3 - - - name: Verify Changelog - id: verify_changelog - if: ${{ github.ref == 'refs/heads/master' || github.base_ref == 'master' }} - # base_ref for pull request check, ref for push - uses: LizardByte/.github/actions/verify_changelog@master - with: - token: ${{ secrets.GITHUB_TOKEN }} - outputs: - next_version: ${{ steps.verify_changelog.outputs.changelog_parser_version }} - next_version_bare: ${{ steps.verify_changelog.outputs.changelog_parser_version_bare }} - last_version: ${{ steps.verify_changelog.outputs.latest_release_tag_name }} - release_body: ${{ steps.verify_changelog.outputs.changelog_parser_description }} - - # todo - remove this job once versioning is fully automated by cmake - check_versions: - name: Check Versions - runs-on: ubuntu-latest - needs: check_changelog - if: ${{ github.ref == 'refs/heads/master' || github.base_ref == 'master' }} - # base_ref for pull request check, ref for push - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Check CMakeLists.txt Version - run: | - version=$(grep -o -E '^project\(Sunshine VERSION [0-9]+\.[0-9]+\.[0-9]+' CMakeLists.txt | \ - grep -o -E '[0-9]+\.[0-9]+\.[0-9]+') - echo "cmakelists_version=${version}" >> $GITHUB_ENV - - - name: Compare CMakeList.txt Version - if: ${{ env.cmakelists_version != needs.check_changelog.outputs.next_version_bare }} - run: | - echo CMakeLists version: "$cmakelists_version" - echo Changelog version: "${{ needs.check_changelog.outputs.next_version_bare }}" - echo Within 'CMakeLists.txt' change "project(Sunshine [VERSION $cmakelists_version]" to \ - "project(Sunshine [VERSION ${{ needs.check_changelog.outputs.next_version_bare }}]" - exit 1 - setup_release: name: Setup Release - needs: check_changelog + outputs: + publish_release: ${{ steps.setup_release.outputs.publish_release }} + release_commit: ${{ steps.setup_release.outputs.release_commit }} + release_tag: ${{ steps.setup_release.outputs.release_tag }} + release_version: ${{ steps.setup_release.outputs.release_version }} runs-on: ubuntu-latest steps: - - name: Set release details - id: release_details - env: - RELEASE_BODY: ${{ needs.check_changelog.outputs.release_body }} - run: | - # determine to create a release or not - if [[ $GITHUB_EVENT_NAME == "push" ]]; then - RELEASE=true - else - RELEASE=false - fi - - # set the release tag - COMMIT=${{ github.sha }} - if [[ $GITHUB_REF == refs/heads/master ]]; then - TAG="${{ needs.check_changelog.outputs.next_version }}" - RELEASE_NAME="${{ needs.check_changelog.outputs.next_version }}" - RELEASE_BODY="$RELEASE_BODY" - PRE_RELEASE="false" - elif [[ $GITHUB_REF == refs/heads/nightly ]]; then - TAG="nightly-dev" - RELEASE_NAME="nightly" - RELEASE_BODY="automated nightly release - $(date -u +'%Y-%m-%dT%H:%M:%SZ') - ${COMMIT}" - PRE_RELEASE="true" - fi - - echo "create_release=${RELEASE}" >> $GITHUB_OUTPUT - echo "release_tag=${TAG}" >> $GITHUB_OUTPUT - echo "release_commit=${COMMIT}" >> $GITHUB_OUTPUT - echo "release_name=${RELEASE_NAME}" >> $GITHUB_OUTPUT - echo "pre_release=${PRE_RELEASE}" >> $GITHUB_OUTPUT - - # this is stupid but works for multiline strings - echo "RELEASE_BODY<> $GITHUB_ENV - echo "$RELEASE_BODY" >> $GITHUB_ENV - echo "EOF" >> $GITHUB_ENV + - name: Checkout + uses: actions/checkout@v4 - outputs: - create_release: ${{ steps.release_details.outputs.create_release }} - release_tag: ${{ steps.release_details.outputs.release_tag }} - release_commit: ${{ steps.release_details.outputs.release_commit }} - release_name: ${{ steps.release_details.outputs.release_name }} - release_body: ${{ env.RELEASE_BODY }} - pre_release: ${{ steps.release_details.outputs.pre_release }} + - name: Setup Release + id: setup_release + uses: LizardByte/setup-release-action@v2024.524.1411 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} setup_flatpak_matrix: name: Setup Flatpak Matrix @@ -131,7 +52,7 @@ jobs: # https://www.cynkra.com/blog/2020-12-23-dynamic-gha run: | # determine which architectures to build - if [[ $GITHUB_EVENT_NAME == "push" ]]; then + if [[ "${{ github.event_name }}" == "push" ]]; then matrix=$(( echo '{ "arch" : ["x86_64", "aarch64"] }' ) | jq -c .) @@ -157,8 +78,20 @@ jobs: matrix: ${{fromJson(needs.setup_flatpak_matrix.outputs.matrix)}} steps: + - name: Maximize build space + uses: easimon/maximize-build-space@v10 + with: + root-reserve-mb: 10240 + remove-dotnet: 'true' + remove-android: 'true' + remove-haskell: 'true' + remove-codeql: 'true' + remove-docker-images: 'true' + - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 + with: + submodules: recursive - name: Setup Dependencies Linux Flatpak run: | @@ -169,17 +102,20 @@ jobs: cmake \ flatpak \ qemu-user-static + sudo su $(whoami) -c "flatpak --user remote-add --if-not-exists flathub \ https://flathub.org/repo/flathub.flatpakrepo" + sudo su $(whoami) -c "flatpak --user install -y flathub \ org.flatpak.Builder \ org.freedesktop.Platform/${{ matrix.arch }}/${PLATFORM_VERSION} \ org.freedesktop.Sdk/${{ matrix.arch }}/${PLATFORM_VERSION} \ org.freedesktop.Sdk.Extension.node18/${{ matrix.arch }}/${PLATFORM_VERSION} \ + org.freedesktop.Sdk.Extension.vala/${{ matrix.arch }}/${PLATFORM_VERSION} \ " - name: Cache Flatpak build - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ./build/.flatpak-builder key: flatpak-${{ matrix.arch }}-${{ github.sha }} @@ -189,18 +125,18 @@ jobs: - name: Configure Flatpak Manifest run: | # variables for manifest - branch=${GITHUB_HEAD_REF} + branch="${{ github.head_ref }}" + commit=${{ needs.setup_release.outputs.release_commit }} # check the branch variable if [ -z "$branch" ] then echo "This is a PUSH event" branch=${{ github.ref_name }} - commit=${{ github.sha }} + build_version=${{ needs.setup_release.outputs.release_tag }} clone_url=${{ github.event.repository.clone_url }} else echo "This is a PR event" - commit=${{ github.event.pull_request.head.sha }} clone_url=${{ github.event.pull_request.head.repo.clone_url }} fi echo "Branch: ${branch}" @@ -212,6 +148,7 @@ jobs: cd build cmake -DGITHUB_CLONE_URL=${clone_url} \ + -DBUILD_VERSION=${build_version} \ -DGITHUB_BRANCH=${branch} \ -DGITHUB_COMMIT=${commit} \ -DSUNSHINE_CONFIGURE_FLATPAK_MAN=ON \ @@ -234,113 +171,115 @@ jobs: ../artifacts/sunshine_debug_${{ matrix.arch }}.flatpak dev.lizardbyte.sunshine.Debug' - name: Upload Artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: sunshine-linux-flatpak-${{ matrix.arch }} path: artifacts/ - name: Create/Update GitHub Release - if: ${{ needs.setup_release.outputs.create_release == 'true' }} - uses: ncipollo/release-action@v1 + if: ${{ needs.setup_release.outputs.publish_release == 'true' }} + uses: LizardByte/create-release-action@v2024.524.143912 with: - name: ${{ needs.setup_release.outputs.release_name }} - tag: ${{ needs.setup_release.outputs.release_tag }} - commit: ${{ needs.setup_release.outputs.release_commit }} - artifacts: "*artifacts/*" - token: ${{ secrets.GH_BOT_TOKEN }} allowUpdates: true - body: ${{ needs.setup_release.outputs.release_body }} discussionCategory: announcements - prerelease: ${{ needs.setup_release.outputs.pre_release }} + generateReleaseNotes: true + name: ${{ needs.setup_release.outputs.release_tag }} + prerelease: true + tag: ${{ needs.setup_release.outputs.release_tag }} + token: ${{ secrets.GH_BOT_TOKEN }} build_linux: - name: Linux + name: Linux ${{ matrix.type }} runs-on: ubuntu-${{ matrix.dist }} - needs: [check_changelog, setup_release] + needs: [setup_release] strategy: fail-fast: false # false to test all, true to fail entire job if any fail matrix: include: # package these differently - - type: appimage - EXTRA_ARGS: '-DSUNSHINE_CONFIGURE_APPIMAGE=ON' - dist: 20.04 + - type: AppImage + EXTRA_ARGS: '-DSUNSHINE_BUILD_APPIMAGE=ON' + dist: 22.04 steps: + - name: Maximize build space + uses: easimon/maximize-build-space@v10 + with: + root-reserve-mb: 30720 + remove-dotnet: 'true' + remove-android: 'true' + remove-haskell: 'true' + remove-codeql: 'true' + remove-docker-images: 'true' + - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: recursive + - name: Install wget + run: | + sudo apt-get update -y + sudo apt-get install -y \ + wget + + - name: Install CUDA + env: + CUDA_VERSION: 11.8.0 + CUDA_BUILD: 520.61.05 + timeout-minutes: 4 + run: | + url_base="https://developer.download.nvidia.com/compute/cuda/${CUDA_VERSION}/local_installers" + url="${url_base}/cuda_${CUDA_VERSION}_${CUDA_BUILD}_linux.run" + sudo wget -q -O /root/cuda.run ${url} + sudo chmod a+x /root/cuda.run + sudo /root/cuda.run --silent --toolkit --toolkitpath=/usr/local/cuda --no-opengl-libs --no-man-page --no-drm + sudo rm /root/cuda.run + - name: Setup Dependencies Linux + timeout-minutes: 5 run: | + # allow newer gcc sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y - if [[ ${{ matrix.dist }} == "18.04" ]]; then - # Ubuntu 18.04 packages - sudo add-apt-repository ppa:savoury1/boost-defaults-1.71 -y - - sudo apt-get update -y - sudo apt-get install -y \ - libboost-filesystem1.71-dev \ - libboost-locale1.71-dev \ - libboost-log1.71-dev \ - libboost-regex1.71-dev \ - libboost-thread1.71-dev \ - libboost-program-options1.71-dev - - # Install cmake - wget https://cmake.org/files/v3.22/cmake-3.22.2-linux-x86_64.sh - chmod +x cmake-3.22.2-linux-x86_64.sh - mkdir /opt/cmake - ./cmake-3.22.2-linux-x86_64.sh --prefix=/opt/cmake --skip-license - ln --force --symbolic /opt/cmake/bin/cmake /usr/local/bin/cmake - cmake --version - - # install newer tar from focal... appimagelint fails on 18.04 without this - echo "original tar version" - tar --version - wget -O tar.deb http://security.ubuntu.com/ubuntu/pool/main/t/tar/tar_1.30+dfsg-7ubuntu0.20.04.2_amd64.deb - sudo apt-get -y install -f ./tar.deb - echo "new tar version" - tar --version - else - # Ubuntu 20.04+ packages - sudo apt-get update -y - sudo apt-get install -y \ - cmake \ - libboost-filesystem-dev \ - libboost-locale-dev \ - libboost-log-dev \ - libboost-thread-dev \ - libboost-program-options-dev - fi + # allow libfuse2 for appimage on 22.04 + sudo add-apt-repository universe + # libx11-xcb-dev and libxcb-dri3-dev are required for building libva sudo apt-get install -y \ build-essential \ + cmake \ gcc-10 \ g++-10 \ - libappindicator3-dev \ + libayatana-appindicator3-dev \ libavdevice-dev \ + libboost-filesystem-dev \ + libboost-locale-dev \ + libboost-log-dev \ + libboost-program-options-dev \ libcap-dev \ libcurl4-openssl-dev \ libdrm-dev \ libevdev-dev \ + libfuse2 \ + libminiupnpc-dev \ libmfx-dev \ + libnotify-dev \ libnuma-dev \ libopus-dev \ libpulse-dev \ libssl-dev \ - libva-dev \ libvdpau-dev \ libwayland-dev \ libx11-dev \ + libx11-xcb-dev \ + libxcb-dri3-dev \ libxcb-shm0-dev \ libxcb-xfixes0-dev \ libxcb1-dev \ libxfixes-dev \ libxrandr-dev \ libxtst-dev \ - wget + python3 # clean apt cache sudo apt-get clean @@ -355,27 +294,47 @@ jobs: --slave /usr/bin/gcc-ar gcc-ar /usr/bin/gcc-ar-10 \ --slave /usr/bin/gcc-ranlib gcc-ranlib /usr/bin/gcc-ranlib-10 - # Install CUDA - sudo wget \ - https://developer.download.nvidia.com/compute/cuda/11.8.0/local_installers/cuda_11.8.0_520.61.05_linux.run \ - --progress=bar:force:noscroll -q --show-progress -O /root/cuda.run - sudo chmod a+x /root/cuda.run - sudo /root/cuda.run --silent --toolkit --toolkitpath=/usr --no-opengl-libs --no-man-page --no-drm - sudo rm /root/cuda.run + - name: Setup python + id: python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Build latest libva + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + timeout-minutes: 5 + run: | + gh release download --archive=tar.gz --repo=intel/libva + tar xzf libva-*.tar.gz && rm libva-*.tar.gz + cd libva-* + ./autogen.sh --prefix=/usr --libdir=/usr/lib/x86_64-linux-gnu \ + --enable-drm \ + --enable-x11 \ + --enable-glx \ + --enable-wayland \ + --without-legacy # emgd, nvctrl, fglrx + make -j $(nproc) + sudo make install + cd .. && rm -rf libva-* - name: Build Linux env: BRANCH: ${{ github.head_ref || github.ref_name }} - BUILD_VERSION: ${{ needs.check_changelog.outputs.next_version_bare }} - COMMIT: ${{ github.event.pull_request.head.sha || github.sha }} + BUILD_VERSION: ${{ needs.setup_release.outputs.release_tag }} + COMMIT: ${{ needs.setup_release.outputs.release_commit }} + timeout-minutes: 10 run: | + echo "nproc: $(nproc)" + mkdir -p build mkdir -p artifacts - npm install - cd build - cmake -DCMAKE_BUILD_TYPE=Release \ + cmake \ + -DBUILD_WERROR=ON \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_CUDA_COMPILER:PATH=/usr/local/cuda/bin/nvcc \ -DCMAKE_INSTALL_PREFIX=/usr \ -DSUNSHINE_ASSETS_DIR=share/sunshine \ -DSUNSHINE_EXECUTABLE_PATH=/usr/bin/sunshine \ @@ -385,28 +344,17 @@ jobs: -DSUNSHINE_ENABLE_CUDA=ON \ ${{ matrix.EXTRA_ARGS }} \ .. - make -j ${nproc} - - - name: Package Linux - CPACK - if: ${{ matrix.type == 'cpack' }} - working-directory: build - run: | - cpack -G DEB - mv ./cpack_artifacts/Sunshine.deb ../artifacts/sunshine-${{ matrix.dist }}.deb - - if [[ ${{ matrix.dist }} == "20.04" ]]; then - cpack -G RPM - mv ./cpack_artifacts/Sunshine.rpm ../artifacts/sunshine.rpm - fi + make -j $(expr $(nproc) - 1) # use all but one core - name: Set AppImage Version - if: ${{ matrix.type == 'appimage' && ( needs.check_changelog.outputs.next_version_bare != needs.check_changelog.outputs.last_version ) }} # yamllint disable-line rule:line-length + if: | + matrix.type == 'AppImage' run: | - version=${{ needs.check_changelog.outputs.next_version_bare }} + version=${{ needs.setup_release.outputs.release_tag }} echo "VERSION=${version}" >> $GITHUB_ENV - name: Package Linux - AppImage - if: ${{ matrix.type == 'appimage' }} + if: ${{ matrix.type == 'AppImage' }} working-directory: build run: | # install sunshine to the DESTDIR @@ -422,17 +370,21 @@ jobs: # AppImage # https://docs.appimage.org/packaging-guide/index.html - wget https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage + wget -q https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage chmod +x linuxdeploy-x86_64.AppImage + # https://github.com/linuxdeploy/linuxdeploy-plugin-gtk + sudo apt-get install libgtk-3-dev librsvg2-dev -y + wget -q https://raw.githubusercontent.com/linuxdeploy/linuxdeploy-plugin-gtk/master/linuxdeploy-plugin-gtk.sh + chmod +x linuxdeploy-plugin-gtk.sh + export DEPLOY_GTK_VERSION=3 + ./linuxdeploy-x86_64.AppImage \ --appdir ./AppDir \ + --plugin gtk \ --executable ./sunshine \ --icon-file "../$ICON_FILE" \ --desktop-file "./$DESKTOP_FILE" \ - --library /usr/lib/x86_64-linux-gnu/libpango-1.0.so.0 \ - --library /usr/lib/x86_64-linux-gnu/libpangocairo-1.0.so.0 \ - --library /usr/lib/x86_64-linux-gnu/libpangoft2-1.0.so.0 \ --output appimage # move @@ -441,122 +393,218 @@ jobs: # permissions chmod +x ../artifacts/sunshine.AppImage + - name: Delete CUDA + # free up space on the runner + run: | + sudo rm -rf /usr/local/cuda + - name: Verify AppImage - if: ${{ matrix.type == 'appimage' }} + if: ${{ matrix.type == 'AppImage' }} run: | wget https://github.com/TheAssassin/appimagelint/releases/download/continuous/appimagelint-x86_64.AppImage chmod +x appimagelint-x86_64.AppImage - # rm -rf ~/.cache/appimagelint/ - ./appimagelint-x86_64.AppImage ./artifacts/sunshine.AppImage - name: Upload Artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: sunshine-linux-${{ matrix.type }}-${{ matrix.dist }} path: artifacts/ + - name: Install test deps + run: | + sudo apt-get update -y + sudo apt-get install -y \ + doxygen \ + graphviz \ + python3-venv \ + x11-xserver-utils \ + xvfb + + # clean apt cache + sudo apt-get clean + sudo rm -rf /var/lib/apt/lists/* + + - name: Run tests + id: test + working-directory: build/tests + run: | + export DISPLAY=:1 + Xvfb ${DISPLAY} -screen 0 1024x768x24 & + + ./test_sunshine --gtest_color=yes + + - name: Generate gcov report + # any except canceled or skipped + if: always() && (steps.test.outcome == 'success' || steps.test.outcome == 'failure') + id: test_report + working-directory: build + run: | + ${{ steps.python.outputs.python-path }} -m pip install gcovr + ${{ steps.python.outputs.python-path }} -m gcovr -r .. \ + --exclude-noncode-lines \ + --exclude-throw-branches \ + --exclude-unreachable-branches \ + --exclude '.*tests/.*' \ + --exclude '.*third-party/.*' \ + --xml-pretty \ + -o coverage.xml + + - name: Upload coverage + # any except canceled or skipped + if: >- + always() && + (steps.test_report.outcome == 'success') && + startsWith(github.repository, 'LizardByte/') + uses: codecov/codecov-action@v4 + with: + disable_search: true + fail_ci_if_error: true + files: ./build/coverage.xml + flags: ${{ runner.os }} + token: ${{ secrets.CODECOV_TOKEN }} + verbose: true + - name: Create/Update GitHub Release - if: ${{ needs.setup_release.outputs.create_release == 'true' }} - uses: ncipollo/release-action@v1 + if: ${{ needs.setup_release.outputs.publish_release == 'true' }} + uses: LizardByte/create-release-action@v2024.524.143912 with: - name: ${{ needs.setup_release.outputs.release_name }} - tag: ${{ needs.setup_release.outputs.release_tag }} - commit: ${{ needs.setup_release.outputs.release_commit }} - artifacts: "*artifacts/*" - token: ${{ secrets.GH_BOT_TOKEN }} allowUpdates: true - body: ${{ needs.setup_release.outputs.release_body }} discussionCategory: announcements - prerelease: ${{ needs.setup_release.outputs.pre_release }} + generateReleaseNotes: true + name: ${{ needs.setup_release.outputs.release_tag }} + prerelease: true + tag: ${{ needs.setup_release.outputs.release_tag }} + token: ${{ secrets.GH_BOT_TOKEN }} - build_mac: - name: MacOS - runs-on: macos-11 - needs: [check_changelog, setup_release] + build_mac_brew: + needs: [setup_release] + strategy: + fail-fast: false # false to test all, true to fail entire job if any fail + matrix: + include: + # https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#standard-github-hosted-runners-for-public-repositories + # while GitHub has larger macOS runners, they are not available for our repos :( + - os_version: "12" + release: true + - os_version: "13" + - os_version: "14" + name: Homebrew (macOS-${{ matrix.os_version }}) + runs-on: macos-${{ matrix.os_version }} steps: - name: Checkout - uses: actions/checkout@v3 - with: - submodules: recursive + uses: actions/checkout@v4 - - name: Setup Dependencies MacOS + - name: Setup Dependencies Homebrew run: | # install dependencies using homebrew - brew install boost cmake curl node opus pkg-config - - # fix openssl header not found - ln -sf /usr/local/opt/openssl/include/openssl /usr/local/include/openssl + brew install cmake - - name: Build MacOS - env: - BRANCH: ${{ github.head_ref || github.ref_name }} - BUILD_VERSION: ${{ needs.check_changelog.outputs.next_version_bare }} - COMMIT: ${{ github.event.pull_request.head.sha || github.sha }} + - name: Configure formula run: | - npm install + # variables for formula + branch="${{ github.head_ref }}" + commit=${{ needs.setup_release.outputs.release_commit }} + + # check the branch variable + if [ -z "$branch" ] + then + echo "This is a PUSH event" + build_version=${{ needs.setup_release.outputs.release_tag }} + clone_url=${{ github.event.repository.clone_url }} + branch="${{ github.ref_name }}" + default_branch="${{ github.event.repository.default_branch }}" + else + echo "This is a PR event" + clone_url=${{ github.event.pull_request.head.repo.clone_url }} + branch="${{ github.event.pull_request.head.ref }}" + default_branch="${{ github.event.pull_request.head.repo.default_branch }}" + fi + echo "Branch: ${branch}" + echo "Clone URL: ${clone_url}" mkdir build cd build - cmake -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_INSTALL_PREFIX=/usr \ - -DSUNSHINE_ASSETS_DIR=local/sunshine/assets \ - -DSUNSHINE_EXECUTABLE_PATH=/usr/bin/sunshine \ + cmake \ + -DBUILD_VERSION="${build_version}" \ + -DGITHUB_BRANCH="${branch}" \ + -DGITHUB_COMMIT="${commit}" \ + -DGITHUB_CLONE_URL="${clone_url}" \ + -DGITHUB_DEFAULT_BRANCH="${default_branch}" \ + -DSUNSHINE_CONFIGURE_HOMEBREW=ON \ + -DSUNSHINE_CONFIGURE_ONLY=ON \ .. - make -j ${nproc} - - - name: Package MacOS - run: | - mkdir -p artifacts - cd build + cd .. - # package - cpack -G DragNDrop - mv ./cpack_artifacts/Sunshine.dmg ../artifacts/sunshine.dmg + # copy formula to artifacts + mkdir -p homebrew + cp -f ./build/sunshine.rb ./homebrew/sunshine.rb - # cpack -G Bundle - # mv ./cpack_artifacts/Sunshine.dmg ../artifacts/sunshine-bundle.dmg + # testing + cat ./homebrew/sunshine.rb - name: Upload Artifacts - uses: actions/upload-artifact@v3 + if: ${{ matrix.release }} + uses: actions/upload-artifact@v4 with: - name: sunshine-macos - path: artifacts/ + name: sunshine-homebrew + path: homebrew/ - - name: Create/Update GitHub Release - if: ${{ needs.setup_release.outputs.create_release == 'true' }} - uses: ncipollo/release-action@v1 + - name: Validate Homebrew Formula + uses: LizardByte/homebrew-release-action@v2024.522.222851 with: - name: ${{ needs.setup_release.outputs.release_name }} - tag: ${{ needs.setup_release.outputs.release_tag }} - commit: ${{ needs.setup_release.outputs.release_commit }} - artifacts: "*artifacts/*" + formula_file: ${{ github.workspace }}/homebrew/sunshine.rb + git_email: ${{ secrets.GH_BOT_EMAIL }} + git_username: ${{ secrets.GH_BOT_NAME }} + publish: false token: ${{ secrets.GH_BOT_TOKEN }} + validate: true + + - name: Create/Update GitHub Release + if: >- + matrix.release && + needs.setup_release.outputs.publish_release == 'true' + uses: LizardByte/create-release-action@v2024.524.143912 + with: allowUpdates: true - body: ${{ needs.setup_release.outputs.release_body }} + artifacts: '${{ github.workspace }}/homebrew/*' discussionCategory: announcements - prerelease: ${{ needs.setup_release.outputs.pre_release }} + generateReleaseNotes: true + name: ${{ needs.setup_release.outputs.release_tag }} + prerelease: true + tag: ${{ needs.setup_release.outputs.release_tag }} + token: ${{ secrets.GH_BOT_TOKEN }} build_mac_port: - name: Macports - needs: [check_changelog, setup_release] - runs-on: macos-11 + needs: [setup_release] + strategy: + fail-fast: false # false to test all, true to fail entire job if any fail + matrix: + include: + # https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#standard-github-hosted-runners-for-public-repositories + # while GitHub has larger macOS runners, they are not available for our repos :( + - os_version: "12" + release: true + - os_version: "13" + - os_version: "14" + name: Macports (macOS-${{ matrix.os_version }}) + runs-on: macos-${{ matrix.os_version }} steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Checkout ports - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: macports/macports-ports fetch-depth: 64 path: ports - name: Checkout mpbb - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: macports/mpbb path: mpbb @@ -566,20 +614,27 @@ jobs: # install dependencies using homebrew brew install cmake + - name: Setup python + id: python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Configure Portfile run: | # variables for Portfile - branch=${GITHUB_HEAD_REF} + branch="${{ github.head_ref }}" + commit=${{ needs.setup_release.outputs.release_commit }} # check the branch variable if [ -z "$branch" ] then echo "This is a PUSH event" - commit=${{ github.sha }} + branch="${{ github.ref_name }}" + build_version=${{ needs.setup_release.outputs.release_tag }} clone_url=${{ github.event.repository.clone_url }} else echo "This is a PR event" - commit=${{ github.event.pull_request.head.sha }} clone_url=${{ github.event.pull_request.head.repo.clone_url }} fi echo "Commit: ${commit}" @@ -588,6 +643,8 @@ jobs: mkdir build cd build cmake \ + -DBUILD_VERSION=${build_version} \ + -DGITHUB_BRANCH=${branch} \ -DGITHUB_COMMIT=${commit} \ -DGITHUB_CLONE_URL=${clone_url} \ -DSUNSHINE_CONFIGURE_PORTFILE=ON \ @@ -616,173 +673,330 @@ jobs: echo "/opt/local/bin" >> $GITHUB_PATH echo "/opt/local/sbin" >> $GITHUB_PATH - - name: Determine list of subports - id: subportlist - run: | - set -eu - port=Sunshine - subportlist="" - - echo "Listing subports for Sunshine" - new_subports=$(mpbb \ - --work-dir /tmp/mpbb \ - list-subports \ - --archive-site= \ - --archive-site-private= \ - --include-deps=no \ - "$port" \ - | tr '\n' ' ') - for subport in $new_subports; do - echo "$subport" - subportlist="$subportlist $subport" - done - echo "subportlist=${subportlist}" >> $GITHUB_OUTPUT - - - name: Run port lint for all subports - env: - subportlist: ${{ steps.subportlist.outputs.subportlist }} + - name: Run port lint run: | - set -eu - fail=0 - for subport in $subportlist; do - echo "::group::${subport}" - path=$(port file "$subport") - messagetype="warning" - if ! messages=$(port -q lint "$subport" 2>&1); then - messagetype="error" - fail=1 - fi - if [ -n "$messages" ]; then - echo "$messages" - # See https://github.com/actions/toolkit/issues/193#issuecomment-605394935 - encoded_messages="port lint ${subport}:%0A" - encoded_messages+="$(echo "${messages}" | sed -E 's/$/%0A/g' | tr -d '\n')" - echo "::${messagetype} file=${path#${PWD}/ports/},line=1,col=1::${encoded_messages}" - fi - echo "::endgroup::" - done - exit "$fail" + port -q lint "Sunshine" - - name: Build subports + - name: Build port env: subportlist: ${{ steps.subportlist.outputs.subportlist }} + id: build run: | - set -eu - fail=0 - for subport in $subportlist; do - workdir="/tmp/mpbb/$subport" - mkdir -p "$workdir/logs" - touch "$workdir/logs/dependencies-progress.txt" - echo "::group::Cleaning up between ports" - sudo mpbb --work-dir "$workdir" cleanup - echo "::endgroup::" - echo "::group::Installing dependencies for ${subport}" - sudo mpbb \ - --work-dir "$workdir" \ - install-dependencies \ - "$subport" >"$workdir/logs/install-dependencies.log" 2>&1 & - deps_pid=$! - tail -f "$workdir/logs/dependencies-progress.txt" 2>/dev/null & - tail_pid=$! - set +e - wait "$deps_pid" - deps_exit=$? - set -e - kill "$tail_pid" || true - if [ "$deps_exit" -ne 0 ]; then - echo "::endgroup::" - echo "::error::Failed to install dependencies for ${subport}" - fail=1 - continue - fi - echo "::endgroup::" - echo "::group::Installing ${subport}" - set +e - sudo mpbb \ - --work-dir "$workdir" \ - install-port \ - --source \ - "$subport" - install_exit=$? - set -e - if [ "$install_exit" -ne 0 ]; then - echo "::endgroup::" - echo "::error::Failed to install ${subport}" - fail=1 - continue - fi - echo "::endgroup::" - done - exit "$fail" + subport="Sunshine" + + workdir="/tmp/mpbb/$subport" + mkdir -p "$workdir/logs" + + echo "::group::Installing dependencies" + sudo mpbb \ + --work-dir "$workdir" \ + install-dependencies \ + "$subport" + echo "::endgroup::" + + echo "::group::Installing ${subport}" + sudo mpbb \ + --work-dir "$workdir" \ + install-port \ + --source \ + "$subport" + echo "::endgroup::" + + - name: Build Logs + if: always() + run: | + logfile="/opt/local/var/macports/logs/_Users_runner_work_Sunshine_Sunshine_ports_multimedia_Sunshine/Sunshine/main.log" + cat "$logfile" + sudo mv "${logfile}" "${logfile}.bak" - name: Upload Artifacts - uses: actions/upload-artifact@v3 + if: ${{ matrix.release }} + uses: actions/upload-artifact@v4 with: name: sunshine-macports path: artifacts/ + - name: Fix screen capture permissions + if: ${{ matrix.os_version != 12 }} # macOS-12 is okay + # can be removed if the following is fixed in the runner image + # https://github.com/actions/runner-images/issues/9529 + # https://github.com/actions/runner-images/pull/9530 + run: | + # https://apple.stackexchange.com/questions/362865/macos-list-apps-authorized-for-full-disk-access + + # permissions for screen capture + values="'kTCCServiceScreenCapture','/opt/off/opt/runner/provisioner/provisioner',1,2,4,1,NULL,NULL,0,'UNUSED',NULL,0,1687786159" + if [[ "${{ matrix.os_version }}" == "14" ]]; then + # TCC access table in Sonoma has extra 4 columns: pid, pid_version, boot_uuid, last_reminded + values="${values},NULL,NULL,'UNUSED',${values##*,}" + fi + + # system and user databases + dbPaths=( + "/Library/Application Support/com.apple.TCC/TCC.db" + "$HOME/Library/Application Support/com.apple.TCC/TCC.db" + ) + + sqlQuery="INSERT OR IGNORE INTO access VALUES($values);" + + for dbPath in "${dbPaths[@]}"; do + echo "Column names for $dbPath" + echo "-------------------" + sudo sqlite3 "$dbPath" "PRAGMA table_info(access);" + echo "Current permissions for $dbPath" + echo "-------------------" + sudo sqlite3 "$dbPath" "SELECT * FROM access WHERE service='kTCCServiceScreenCapture';" + sudo sqlite3 "$dbPath" "$sqlQuery" + echo "Updated permissions for $dbPath" + echo "-------------------" + sudo sqlite3 "$dbPath" "SELECT * FROM access WHERE service='kTCCServiceScreenCapture';" + done + + - name: Run tests + id: test + timeout-minutes: 10 + run: | + sudo port test "Sunshine" + + - name: Test Logs + if: always() + run: | + logfile="/opt/local/var/macports/logs/_Users_runner_work_Sunshine_Sunshine_ports_multimedia_Sunshine/Sunshine/main.log" + cat "$logfile" + + - name: Generate gcov report + # any except canceled or skipped + if: always() && (steps.test.outcome == 'success' || steps.test.outcome == 'failure') + id: test_report + working-directory: + /opt/local/var/macports/build/_Users_runner_work_Sunshine_Sunshine_ports_multimedia_Sunshine/Sunshine/work + run: | + base_dir=$(pwd) + build_dir=${base_dir}/build + + # get the directory name that starts with Sunshine-* + dir=$(ls -d Sunshine-*) + + cd ${build_dir} + ${{ steps.python.outputs.python-path }} -m pip install gcovr + sudo ${{ steps.python.outputs.python-path }} -m gcovr -r ../${dir} \ + --exclude-noncode-lines \ + --exclude-throw-branches \ + --exclude-unreachable-branches \ + --exclude '.*${dir}/tests/.*' \ + --exclude '.*${dir}/third-party/.*' \ + --gcov-object-directory $(pwd) \ + --verbose \ + --xml-pretty \ + -o ${{ github.workspace }}/build/coverage.xml + + - name: Upload coverage + # any except canceled or skipped + if: >- + always() && + (steps.test_report.outcome == 'success') && + startsWith(github.repository, 'LizardByte/') + uses: codecov/codecov-action@v4 + with: + disable_search: true + fail_ci_if_error: false # todo: re-enable this when action is fixed + files: ./build/coverage.xml + flags: ${{ runner.os }}-${{ matrix.os_version }} + token: ${{ secrets.CODECOV_TOKEN }} + verbose: true + - name: Create/Update GitHub Release - if: ${{ needs.setup_release.outputs.create_release == 'true' }} - uses: ncipollo/release-action@v1 + if: ${{ needs.setup_release.outputs.publish_release == 'true' }} + uses: LizardByte/create-release-action@v2024.524.143912 with: - name: ${{ needs.setup_release.outputs.release_name }} - tag: ${{ needs.setup_release.outputs.release_tag }} - commit: ${{ needs.setup_release.outputs.release_commit }} - artifacts: "*artifacts/*" - token: ${{ secrets.GH_BOT_TOKEN }} allowUpdates: true - body: ${{ needs.setup_release.outputs.release_body }} discussionCategory: announcements - prerelease: ${{ needs.setup_release.outputs.pre_release }} + generateReleaseNotes: true + name: ${{ needs.setup_release.outputs.release_tag }} + prerelease: true + tag: ${{ needs.setup_release.outputs.release_tag }} + token: ${{ secrets.GH_BOT_TOKEN }} build_win: name: Windows runs-on: windows-2019 - needs: [check_changelog, setup_release] + needs: [setup_release] steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: recursive + - name: Prepare tests + id: prepare-tests + if: false # todo: DirectX11 is not available, so even software encoder fails + run: | + # function to download and extract a zip file + function DownloadAndExtract { + param ( + [string]$Uri, + [string]$OutFile + ) + + $maxRetries = 5 + $retryCount = 0 + $success = $false + + while (-not $success -and $retryCount -lt $maxRetries) { + $retryCount++ + Write-Host "Downloading $Uri to $OutFile, attempt $retryCount of $maxRetries" + try { + Invoke-WebRequest -Uri $Uri -OutFile $OutFile + $success = $true + } catch { + Write-Host "Attempt $retryCount of $maxRetries failed with error: $($_.Exception.Message). Retrying..." + Start-Sleep -Seconds 5 + } + } + + if (-not $success) { + Write-Host "Failed to download the file after $maxRetries attempts." + exit 1 + } + + # use .NET to get the base name of the file + $baseName = (Get-Item $OutFile).BaseName + + # Extract the zip file + Expand-Archive -Path $OutFile -DestinationPath $baseName + } + + # virtual display driver + DownloadAndExtract ` + -Uri "https://www.amyuni.com/downloads/usbmmidd_v2.zip" ` + -OutFile "usbmmidd_v2.zip" + + # install + Set-Location -Path usbmmidd_v2/usbmmidd_v2 + ./deviceinstaller64 install usbmmidd.inf usbmmidd + + # create the virtual display + ./deviceinstaller64 enableidd 1 + + # move up a directory + Set-Location -Path ../.. + + # install devcon + DownloadAndExtract ` + -Uri "https://github.com/Drawbackz/DevCon-Installer/releases/download/1.4-rc/Devcon.Installer.zip" ` + -OutFile "Devcon.Installer.zip" + Set-Location -Path Devcon.Installer + # hash needs to match OS version + # https://github.com/Drawbackz/DevCon-Installer/blob/master/devcon_sources.json + Start-Process -FilePath "./Devcon Installer.exe" -Wait -ArgumentList ` + 'install', ` + '-hash', '54004C83EE34F6A55380528A8B29F4C400E61FBB947A19E0AB9E5A193D7D961E', ` + '-addpath', ` + '-update', ` + '-dir', 'C:\Windows\System32' + + # disable Hyper-V Video + # https://stackoverflow.com/a/59490940 + C:\Windows\System32\devcon.exe disable "VMBUS\{da0a7802-e377-4aac-8e77-0558eb1073f8}" + + # move up a directory + Set-Location -Path .. + + # multi monitor tool + DownloadAndExtract ` + -Uri "http://www.nirsoft.net/utils/multimonitortool-x64.zip" ` + -OutFile "multimonitortool.zip" + + # enable the virtual display + # http://www.nirsoft.net/utils/multi_monitor_tool.html + Set-Location -Path multimonitortool + + # Original Hyper-V is \\.\DISPLAY1, it will recreate itself as \\.\DISPLAY6 (or something higher than 2) + # USB Mobile Monitor Virtual Display is \\.\DISPLAY2 + + # these don't seem to work if not using runAs + # todo: do they work if not using runAs? + Start-Process powershell -Verb runAs -ArgumentList '-Command ./MultiMonitorTool.exe /enable \\.\DISPLAY2' + Start-Process powershell -Verb runAs -ArgumentList '-Command ./MultiMonitorTool.exe /SetPrimary \\.\DISPLAY2' + + # wait a few seconds + Start-Sleep -s 5 + + # list monitors + ./MultiMonitorTool.exe /stext monitor_list.txt + + # wait a few seconds + Start-Sleep -s 5 + + # print the monitor list + Get-Content -Path monitor_list.txt + - name: Setup Dependencies Windows uses: msys2/setup-msys2@v2 with: + msystem: ucrt64 update: true install: >- - base-devel - diffutils - git - make - mingw-w64-x86_64-binutils - mingw-w64-x86_64-boost - mingw-w64-x86_64-cmake - mingw-w64-x86_64-curl - mingw-w64-x86_64-libmfx - mingw-w64-x86_64-nsis - mingw-w64-x86_64-openssl - mingw-w64-x86_64-opus - mingw-w64-x86_64-toolchain - nasm wget - yasm - - name: Install npm packages + - name: Update Windows dependencies + shell: msys2 {0} + run: | + # download working curl + wget https://repo.msys2.org/mingw/ucrt64/mingw-w64-ucrt-x86_64-curl-8.8.0-1-any.pkg.tar.zst + + # install dependencies + pacman -U --noconfirm mingw-w64-ucrt-x86_64-curl-8.8.0-1-any.pkg.tar.zst + pacman -Syu --noconfirm --ignore=mingw-w64-ucrt-x86_64-curl \ + doxygen \ + git \ + mingw-w64-ucrt-x86_64-boost \ + mingw-w64-ucrt-x86_64-cmake \ + mingw-w64-ucrt-x86_64-cppwinrt \ + mingw-w64-ucrt-x86_64-graphviz \ + mingw-w64-ucrt-x86_64-miniupnpc \ + mingw-w64-ucrt-x86_64-nlohmann-json \ + mingw-w64-ucrt-x86_64-nodejs \ + mingw-w64-ucrt-x86_64-nsis \ + mingw-w64-ucrt-x86_64-onevpl \ + mingw-w64-ucrt-x86_64-openssl \ + mingw-w64-ucrt-x86_64-opus \ + mingw-w64-ucrt-x86_64-toolchain + + - name: Setup python + # use this instead of msys2 python due to known issues using wheels, https://www.msys2.org/docs/python/ + id: setup-python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Python Path + id: python-path + shell: msys2 {0} run: | - npm install + # replace backslashes with double backslashes + python_path=$(echo "${{ steps.setup-python.outputs.python-path }}" | sed 's/\\/\\\\/g') + + # step output + echo "python-path=${python_path}" + echo "python-path=${python_path}" >> $GITHUB_OUTPUT - name: Build Windows shell: msys2 {0} env: BRANCH: ${{ github.head_ref || github.ref_name }} - BUILD_VERSION: ${{ needs.check_changelog.outputs.next_version_bare }} - COMMIT: ${{ github.event.pull_request.head.sha || github.sha }} + BUILD_VERSION: ${{ needs.setup_release.outputs.release_tag }} + COMMIT: ${{ needs.setup_release.outputs.release_commit }} run: | mkdir build cd build - cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo \ + cmake \ + -DBUILD_WERROR=ON \ + -DCMAKE_BUILD_TYPE=RelWithDebInfo \ -DSUNSHINE_ASSETS_DIR=assets \ + -DTESTS_PYTHON_EXECUTABLE='${{ steps.python-path.outputs.python-path }}' \ + -DTESTS_SOFTWARE_ENCODER_UNAVAILABLE='skip' \ -G "MinGW Makefiles" \ .. mingw32-make -j$(nproc) @@ -801,45 +1015,72 @@ jobs: mv ./cpack_artifacts/Sunshine.exe ../artifacts/sunshine-windows-installer.exe mv ./cpack_artifacts/Sunshine.zip ../artifacts/sunshine-windows-portable.zip + - name: Run tests + id: test + shell: msys2 {0} + working-directory: build/tests + run: | + ./test_sunshine.exe --gtest_color=yes + + - name: Generate gcov report + # any except canceled or skipped + if: always() && (steps.test.outcome == 'success' || steps.test.outcome == 'failure') + id: test_report + shell: msys2 {0} + working-directory: build + run: | + ${{ steps.python-path.outputs.python-path }} -m pip install gcovr + ${{ steps.python-path.outputs.python-path }} -m gcovr -r .. \ + --exclude-noncode-lines \ + --exclude-throw-branches \ + --exclude-unreachable-branches \ + --exclude '.*tests/.*' \ + --exclude '.*third-party/.*' \ + --xml-pretty \ + -o coverage.xml + + - name: Upload coverage + # any except canceled or skipped + if: >- + always() && + (steps.test_report.outcome == 'success') && + startsWith(github.repository, 'LizardByte/') + uses: codecov/codecov-action@v4 + with: + disable_search: true + fail_ci_if_error: true + files: ./build/coverage.xml + flags: ${{ runner.os }} + token: ${{ secrets.CODECOV_TOKEN }} + verbose: true + - name: Package Windows Debug Info working-directory: build run: | - # save the original binaries with debug info + # use .dbg file extension for binaries to avoid confusion with real packages + Get-ChildItem -File -Recurse | ` + % { Rename-Item -Path $_.PSPath -NewName $_.Name.Replace(".exe",".dbg") } + + # save the binaries with debug info 7z -r ` "-xr!CMakeFiles" ` "-xr!cpack_artifacts" ` - a "../artifacts/sunshine-debuginfo-win32.zip" "*.exe" + a "../artifacts/sunshine-win32-debuginfo.7z" "*.dbg" - name: Upload Artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: sunshine-windows path: artifacts/ - name: Create/Update GitHub Release - if: ${{ needs.setup_release.outputs.create_release == 'true' }} - uses: ncipollo/release-action@v1 + if: ${{ needs.setup_release.outputs.publish_release == 'true' }} + uses: LizardByte/create-release-action@v2024.524.143912 with: - name: ${{ needs.setup_release.outputs.release_name }} - tag: ${{ needs.setup_release.outputs.release_tag }} - commit: ${{ needs.setup_release.outputs.release_commit }} - artifacts: "*artifacts/*" - token: ${{ secrets.GH_BOT_TOKEN }} allowUpdates: true - body: ${{ needs.setup_release.outputs.release_body }} discussionCategory: announcements - prerelease: ${{ needs.setup_release.outputs.pre_release }} - - release-winget: - name: Release to WinGet - needs: [setup_release, build_win] - if: ${{ needs.setup_release.outputs.create_release == 'true' && github.ref == 'refs/heads/master' }} - runs-on: windows-latest # the required action can only be run on Windows - steps: - - name: Release to WinGet - uses: vedantmgoyal2009/winget-releaser@v2 - with: - identifier: LizardByte.Sunshine - release-tag: ${{ needs.setup_release.outputs.release_tag }} - installers-regex: '\.exe$' # only .exe files + generateReleaseNotes: true + name: ${{ needs.setup_release.outputs.release_tag }} + prerelease: true + tag: ${{ needs.setup_release.outputs.release_tag }} token: ${{ secrets.GH_BOT_TOKEN }} diff --git a/.github/workflows/auto-create-pr.yml b/.github/workflows/auto-create-pr.yml deleted file mode 100644 index 811747c6517..00000000000 --- a/.github/workflows/auto-create-pr.yml +++ /dev/null @@ -1,35 +0,0 @@ ---- -# This action is centrally managed in https://github.com//.github/ -# Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in -# the above-mentioned repo. - -# This workflow creates a PR automatically when anything is merged/pushed into the `nightly` branch. The PR is created -# against the `master` (default) branch. - -name: Auto create PR - -on: - push: - branches: - - 'nightly' - -jobs: - create_pr: - if: startsWith(github.repository, 'LizardByte/') - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Create Pull Request - uses: repo-sync/pull-request@v2 - with: - source_branch: "" # should be "nightly" as it's the triggering branch - destination_branch: "master" - pr_title: "Pulling ${{ github.ref_name }} into master" - pr_template: ".github/pr_release_template.md" - pr_assignee: "${{ secrets.GH_BOT_NAME }}" - pr_draft: true - pr_allow_empty: false - github_token: ${{ secrets.GH_BOT_TOKEN }} diff --git a/.github/workflows/automerge.yml b/.github/workflows/automerge.yml deleted file mode 100644 index 49ddebf4ec1..00000000000 --- a/.github/workflows/automerge.yml +++ /dev/null @@ -1,64 +0,0 @@ ---- -# This action is centrally managed in https://github.com//.github/ -# Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in -# the above-mentioned repo. - -# This workflow will, first, automatically approve PRs created by @LizardByte-bot. Then it will automerge relevant PRs. - -name: Automerge PR - -on: - pull_request: - types: - - opened - - synchronize - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - autoapprove: - if: >- - contains(fromJson('["LizardByte-bot"]'), github.event.pull_request.user.login) && - contains(fromJson('["LizardByte-bot"]'), github.actor) && - startsWith(github.repository, 'LizardByte/') - runs-on: ubuntu-latest - steps: - - name: Autoapproving - uses: hmarr/auto-approve-action@v3 - with: - github-token: "${{ secrets.GITHUB_TOKEN }}" - - - name: Label autoapproved - uses: actions/github-script@v6 - with: - github-token: ${{ secrets.GH_BOT_TOKEN }} - script: | - github.rest.issues.addLabels({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - labels: ['autoapproved', 'autoupdate'] - }) - - automerge: - if: startsWith(github.repository, 'LizardByte/') - needs: [autoapprove] - runs-on: ubuntu-latest - - steps: - - name: Automerging - uses: pascalgn/automerge-action@v0.15.6 - env: - BASE_BRANCHES: nightly - GITHUB_TOKEN: ${{ secrets.GH_BOT_TOKEN }} - GITHUB_LOGIN: ${{ secrets.GH_BOT_NAME }} - MERGE_LABELS: "!dependencies" - MERGE_METHOD: "squash" - MERGE_COMMIT_MESSAGE: "{pullRequest.title} (#{pullRequest.number})" - MERGE_DELETE_BRANCH: true - MERGE_ERROR_FAIL: true - MERGE_FILTER_AUTHOR: ${{ secrets.GH_BOT_NAME }} - MERGE_RETRIES: "240" # 1 hour - MERGE_RETRY_SLEEP: "15000" # 15 seconds diff --git a/.github/workflows/autoupdate-labeler.yml b/.github/workflows/autoupdate-labeler.yml deleted file mode 100644 index 974c9fa7fd1..00000000000 --- a/.github/workflows/autoupdate-labeler.yml +++ /dev/null @@ -1,72 +0,0 @@ ---- -# This action is centrally managed in https://github.com//.github/ -# Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in -# the above-mentioned repo. - -# Label PRs with `autoupdate` if various conditions are met, otherwise, remove the label. - -name: Label PR autoupdate - -on: - pull_request_target: - types: - - edited - - opened - - reopened - - synchronize - -jobs: - label_pr: - if: >- - startsWith(github.repository, 'LizardByte/') && - contains(github.event.pull_request.body, fromJSON('"] I want maintainers to keep my branch updated"')) - runs-on: ubuntu-latest - env: - GH_TOKEN: ${{ github.token }} - steps: - - name: Check if member - id: org_member - run: | - status="true" - gh api \ - -H "Accept: application/vnd.github+json" \ - /orgs/${{ github.repository_owner }}/members/${{ github.actor }} || status="false" - - echo "result=${status}" >> $GITHUB_OUTPUT - - - name: Label autoupdate - if: >- - steps.org_member.outputs.result == 'true' && - contains(github.event.pull_request.labels.*.name, 'autoupdate') == false && - contains(github.event.pull_request.body, - fromJSON('"\n- [x] I want maintainers to keep my branch updated"')) == true - uses: actions/github-script@v6 - with: - github-token: ${{ secrets.GH_BOT_TOKEN }} - script: | - github.rest.issues.addLabels({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - labels: ['autoupdate'] - }) - - - name: Unlabel autoupdate - if: >- - contains(github.event.pull_request.labels.*.name, 'autoupdate') && - ( - (github.event.action == 'synchronize' && steps.org_member.outputs.result == 'false') || - (contains(github.event.pull_request.body, - fromJSON('"\n- [x] I want maintainers to keep my branch updated"')) == false - ) - ) - uses: actions/github-script@v6 - with: - github-token: ${{ secrets.GH_BOT_TOKEN }} - script: | - github.rest.issues.removeLabel({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - name: ['autoupdate'] - }) diff --git a/.github/workflows/autoupdate.yml b/.github/workflows/autoupdate.yml deleted file mode 100644 index 83f4e161824..00000000000 --- a/.github/workflows/autoupdate.yml +++ /dev/null @@ -1,51 +0,0 @@ ---- -# This action is centrally managed in https://github.com//.github/ -# Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in -# the above-mentioned repo. - -# This workflow is designed to work with the following workflows: -# - automerge -# - autoupdate-labeler - -# It uses an action that auto-updates pull requests branches, when changes are pushed to their destination branch. -# Auto-updating to the latest destination branch works only in the context of upstream repo and not forks. -# Dependabot PRs are updated by an action that comments `@depdenabot rebase` on dependabot PRs. (disabled) - -name: autoupdate - -on: - push: - branches: - - 'nightly' - -jobs: - autoupdate: - name: Autoupdate autoapproved PR created in the upstream - if: startsWith(github.repository, 'LizardByte/') - runs-on: ubuntu-latest - steps: - - name: Update - uses: docker://chinthakagodawita/autoupdate-action:v1 - env: - EXCLUDED_LABELS: "central_dependency,dependencies" - GITHUB_TOKEN: '${{ secrets.GH_BOT_TOKEN }}' - PR_FILTER: "labelled" - PR_LABELS: "autoupdate" - PR_READY_STATE: "all" - MERGE_CONFLICT_ACTION: "fail" - -# Disabled due to: -# - no major version tag, resulting in constant nagging to update this action -# - additionally, the code is sketchy, 16k+ lines of code? -# https://github.com/bbeesley/gha-auto-dependabot-rebase/blob/main/dist/main.cjs -# -# dependabot-rebase: -# name: Dependabot Rebase -# if: >- -# startsWith(github.repository, 'LizardByte/') -# runs-on: ubuntu-latest -# steps: -# - name: rebase -# uses: "bbeesley/gha-auto-dependabot-rebase@v1.3.18" -# env: -# GITHUB_TOKEN: ${{ secrets.GH_BOT_TOKEN }} diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml index 1082c941182..02e3265d409 100644 --- a/.github/workflows/ci-docker.yml +++ b/.github/workflows/ci-docker.yml @@ -22,14 +22,14 @@ name: CI Docker on: pull_request: - branches: [master, nightly] + branches: [master] types: [opened, synchronize, reopened] push: - branches: [master, nightly] + branches: [master] workflow_dispatch: concurrency: - group: ${{ github.workflow }}-${{ github.ref }} + group: "${{ github.workflow }}-${{ github.ref }}" cancel-in-progress: true jobs: @@ -38,7 +38,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Find dockerfiles id: find @@ -74,82 +74,49 @@ jobs: echo $matrix | jq . echo "matrix=$matrix" >> $GITHUB_OUTPUT + - name: Find dotnet solution file + id: find_dotnet + run: | + solution=$(find . -maxdepth 1 -type f -iname "*.sln") + + echo "found solution: ${solution}" + + # do not quote to keep this as a single line + echo solution=${solution} >> $GITHUB_OUTPUT + + if [[ $solution != "" ]]; then + echo "dotnet=true" >> $GITHUB_OUTPUT + else + echo "dotnet=false" >> $GITHUB_OUTPUT + fi + outputs: dockerfiles: ${{ steps.find.outputs.dockerfiles }} matrix: ${{ steps.find.outputs.matrix }} - - check_changelog: - name: Check Changelog - needs: [check_dockerfiles] - if: ${{ needs.check_dockerfiles.outputs.dockerfiles }} - runs-on: ubuntu-latest - steps: - - name: Checkout - if: ${{ github.ref == 'refs/heads/master' || github.base_ref == 'master' }} - uses: actions/checkout@v3 - - - name: Verify Changelog - id: verify_changelog - if: ${{ github.ref == 'refs/heads/master' || github.base_ref == 'master' }} - # base_ref for pull request check, ref for push - uses: LizardByte/.github/actions/verify_changelog@master - with: - token: ${{ secrets.GITHUB_TOKEN }} - outputs: - next_version: ${{ steps.verify_changelog.outputs.changelog_parser_version }} - next_version_bare: ${{ steps.verify_changelog.outputs.changelog_parser_version_bare }} - last_version: ${{ steps.verify_changelog.outputs.latest_release_tag_name }} - release_body: ${{ steps.verify_changelog.outputs.changelog_parser_description }} + dotnet: ${{ steps.find_dotnet.outputs.dotnet }} + solution: ${{ steps.find_dotnet.outputs.solution }} setup_release: + if: ${{ needs.check_dockerfiles.outputs.dockerfiles }} name: Setup Release - needs: check_changelog + needs: + - check_dockerfiles + outputs: + publish_release: ${{ steps.setup_release.outputs.publish_release }} + release_commit: ${{ steps.setup_release.outputs.release_commit }} + release_tag: ${{ steps.setup_release.outputs.release_tag }} + release_version: ${{ steps.setup_release.outputs.release_version }} runs-on: ubuntu-latest steps: - - name: Set release details - id: release_details - env: - RELEASE_BODY: ${{ needs.check_changelog.outputs.release_body }} - run: | - # determine to create a release or not - if [[ $GITHUB_EVENT_NAME == "push" ]]; then - RELEASE=true - else - RELEASE=false - fi - - # set the release tag - COMMIT=${{ github.sha }} - if [[ $GITHUB_REF == refs/heads/master ]]; then - TAG="${{ needs.check_changelog.outputs.next_version }}" - RELEASE_NAME="${{ needs.check_changelog.outputs.next_version }}" - RELEASE_BODY="$RELEASE_BODY" - PRE_RELEASE="false" - elif [[ $GITHUB_REF == refs/heads/nightly ]]; then - TAG="nightly-dev" - RELEASE_NAME="nightly" - RELEASE_BODY="automated nightly release - $(date -u +'%Y-%m-%dT%H:%M:%SZ') - ${COMMIT}" - PRE_RELEASE="true" - fi - - echo "create_release=${RELEASE}" >> $GITHUB_OUTPUT - echo "release_tag=${TAG}" >> $GITHUB_OUTPUT - echo "release_commit=${COMMIT}" >> $GITHUB_OUTPUT - echo "release_name=${RELEASE_NAME}" >> $GITHUB_OUTPUT - echo "pre_release=${PRE_RELEASE}" >> $GITHUB_OUTPUT - - # this is stupid but works for multiline strings - echo "RELEASE_BODY<> $GITHUB_ENV - echo "$RELEASE_BODY" >> $GITHUB_ENV - echo "EOF" >> $GITHUB_ENV + - name: Checkout + uses: actions/checkout@v4 - outputs: - create_release: ${{ steps.release_details.outputs.create_release }} - release_tag: ${{ steps.release_details.outputs.release_tag }} - release_commit: ${{ steps.release_details.outputs.release_commit }} - release_name: ${{ steps.release_details.outputs.release_name }} - release_body: ${{ env.RELEASE_BODY }} - pre_release: ${{ steps.release_details.outputs.pre_release }} + - name: Setup Release + id: setup_release + uses: LizardByte/setup-release-action@v2024.524.1411 + with: + dotnet: ${{ needs.check_dockerfiles.outputs.dotnet }} + github_token: ${{ secrets.GITHUB_TOKEN }} lint_dockerfile: needs: [check_dockerfiles] @@ -162,7 +129,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Hadolint id: hadolint @@ -180,7 +147,7 @@ jobs: cat "./hadolint.log" >> $GITHUB_STEP_SUMMARY docker: - needs: [check_dockerfiles, check_changelog, setup_release] + needs: [check_dockerfiles, setup_release] if: ${{ needs.check_dockerfiles.outputs.dockerfiles }} runs-on: ubuntu-latest permissions: @@ -192,32 +159,38 @@ jobs: name: Docker${{ matrix.tag }} steps: + - name: Maximize build space + uses: easimon/maximize-build-space@v10 + with: + root-reserve-mb: 30720 # https://github.com/easimon/maximize-build-space#caveats + remove-dotnet: 'true' + remove-android: 'true' + remove-haskell: 'true' + remove-codeql: 'true' + remove-docker-images: 'true' + - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: recursive - name: Prepare id: prepare env: - NV: ${{ needs.check_changelog.outputs.next_version }} + NV: ${{ needs.setup_release.outputs.release_tag }} run: | # get branch name BRANCH=${GITHUB_HEAD_REF} - RELEASE=false + RELEASE=${{ needs.setup_release.outputs.publish_release }} + COMMIT=${{ needs.setup_release.outputs.release_commit }} if [ -z "$BRANCH" ]; then echo "This is a PUSH event" BRANCH=${{ github.ref_name }} - COMMIT=${{ github.sha }} CLONE_URL=${{ github.event.repository.clone_url }} - if [[ $BRANCH == "master" ]]; then - RELEASE=true - fi else echo "This is a PULL REQUEST event" - COMMIT=${{ github.event.pull_request.head.sha }} CLONE_URL=${{ github.event.pull_request.head.repo.clone_url }} fi @@ -237,8 +210,6 @@ jobs: if [[ $GITHUB_REF == refs/heads/master ]]; then TAGS="${TAGS},${BASE_TAG}:latest${{ matrix.tag }},ghcr.io/${BASE_TAG}:latest${{ matrix.tag }}" TAGS="${TAGS},${BASE_TAG}:master${{ matrix.tag }},ghcr.io/${BASE_TAG}:master${{ matrix.tag }}" - elif [[ $GITHUB_REF == refs/heads/nightly ]]; then - TAGS="${TAGS},${BASE_TAG}:nightly${{ matrix.tag }},ghcr.io/${BASE_TAG}:nightly${{ matrix.tag }}" else TAGS="${TAGS},${BASE_TAG}:test${{ matrix.tag }},ghcr.io/${BASE_TAG}:test${{ matrix.tag }}" fi @@ -250,7 +221,7 @@ jobs: # parse custom directives out of dockerfile # try to get the platforms from the dockerfile custom directive, i.e. `# platforms: xxx,yyy` # directives for PR event, i.e. not push event - if [[ ${PUSH} == "false" ]]; then + if [[ ${RELEASE} == "false" ]]; then while read -r line; do if [[ $line == "# platforms_pr: "* && $PLATFORMS == "" ]]; then # echo the line and use `sed` to remove the custom directive @@ -289,24 +260,21 @@ jobs: echo "branch=${BRANCH}" >> $GITHUB_OUTPUT echo "build_date=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT - echo "commit=${COMMIT}" >> $GITHUB_OUTPUT echo "clone_url=${CLONE_URL}" >> $GITHUB_OUTPUT - echo "release=${RELEASE}" >> $GITHUB_OUTPUT echo "artifacts=${ARTIFACTS}" >> $GITHUB_OUTPUT echo "no_cache_filters=${NO_CACHE_FILTERS}" >> $GITHUB_OUTPUT echo "platforms=${PLATFORMS}" >> $GITHUB_OUTPUT - echo "push=${PUSH}" >> $GITHUB_OUTPUT echo "tags=${TAGS}" >> $GITHUB_OUTPUT - name: Set Up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 id: buildx - name: Cache Docker Layers - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: /tmp/.buildx-cache key: Docker-buildx${{ matrix.tag }}-${{ github.sha }} @@ -314,15 +282,15 @@ jobs: Docker-buildx${{ matrix.tag }}- - name: Log in to Docker Hub - if: ${{ steps.prepare.outputs.push == 'true' }} # PRs do not have access to secrets - uses: docker/login-action@v2 + if: ${{ needs.setup_release.outputs.publish_release == 'true' }} # PRs do not have access to secrets + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - name: Log in to the Container registry - if: ${{ steps.prepare.outputs.push == 'true' }} # PRs do not have access to secrets - uses: docker/login-action@v2 + if: ${{ needs.setup_release.outputs.publish_release == 'true' }} # PRs do not have access to secrets + uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ secrets.GH_BOT_NAME }} @@ -331,7 +299,7 @@ jobs: - name: Build artifacts if: ${{ steps.prepare.outputs.artifacts == 'true' }} id: build_artifacts - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: context: ./ file: ${{ matrix.dockerfile }} @@ -342,10 +310,10 @@ jobs: build-args: | BRANCH=${{ steps.prepare.outputs.branch }} BUILD_DATE=${{ steps.prepare.outputs.build_date }} - BUILD_VERSION=${{ needs.check_changelog.outputs.next_version }} - COMMIT=${{ steps.prepare.outputs.commit }} + BUILD_VERSION=${{ needs.setup_release.outputs.release_tag }} + COMMIT=${{ needs.setup_release.outputs.release_commit }} CLONE_URL=${{ steps.prepare.outputs.clone_url }} - RELEASE=${{ steps.prepare.outputs.release }} + RELEASE=${{ needs.setup_release.outputs.publish_release }} tags: ${{ steps.prepare.outputs.tags }} cache-from: type=local,src=/tmp/.buildx-cache cache-to: type=local,dest=/tmp/.buildx-cache @@ -353,19 +321,19 @@ jobs: - name: Build and push id: build - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: context: ./ file: ${{ matrix.dockerfile }} - push: ${{ steps.prepare.outputs.push }} + push: ${{ needs.setup_release.outputs.publish_release }} platforms: ${{ steps.prepare.outputs.platforms }} build-args: | BRANCH=${{ steps.prepare.outputs.branch }} BUILD_DATE=${{ steps.prepare.outputs.build_date }} - BUILD_VERSION=${{ needs.check_changelog.outputs.next_version }} - COMMIT=${{ steps.prepare.outputs.commit }} + BUILD_VERSION=${{ needs.setup_release.outputs.release_tag }} + COMMIT=${{ needs.setup_release.outputs.release_commit }} CLONE_URL=${{ steps.prepare.outputs.clone_url }} - RELEASE=${{ steps.prepare.outputs.release }} + RELEASE=${{ needs.setup_release.outputs.publish_release }} tags: ${{ steps.prepare.outputs.tags }} cache-from: type=local,src=/tmp/.buildx-cache cache-to: type=local,dest=/tmp/.buildx-cache @@ -385,28 +353,27 @@ jobs: - name: Upload Artifacts if: ${{ steps.prepare.outputs.artifacts == 'true' }} - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: Docker${{ matrix.tag }} path: artifacts/ - name: Create/Update GitHub Release - if: ${{ needs.setup_release.outputs.create_release == 'true' && steps.prepare.outputs.artifacts == 'true' }} - uses: ncipollo/release-action@v1 + if: ${{ needs.setup_release.outputs.publish_release == 'true' && steps.prepare.outputs.artifacts == 'true' }} + uses: LizardByte/create-release-action@v2024.524.143912 with: - name: ${{ needs.setup_release.outputs.release_name }} - tag: ${{ needs.setup_release.outputs.release_tag }} - commit: ${{ needs.setup_release.outputs.release_commit }} - artifacts: "*artifacts/*" - token: ${{ secrets.GH_BOT_TOKEN }} allowUpdates: true - body: ${{ needs.setup_release.outputs.release_body }} + artifacts: "*artifacts/*" discussionCategory: announcements - prerelease: ${{ needs.setup_release.outputs.pre_release }} + generateReleaseNotes: true + name: ${{ needs.setup_release.outputs.release_tag }} + prerelease: true + tag: ${{ needs.setup_release.outputs.release_tag }} + token: ${{ secrets.GH_BOT_TOKEN }} - name: Update Docker Hub Description if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }} - uses: peter-evans/dockerhub-description@v3 + uses: peter-evans/dockerhub-description@v4 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_PASSWORD }} # token is not currently supported diff --git a/.github/workflows/ci-qodana.yml b/.github/workflows/ci-qodana.yml deleted file mode 100644 index 91feb59fae8..00000000000 --- a/.github/workflows/ci-qodana.yml +++ /dev/null @@ -1,292 +0,0 @@ ---- -# This action is centrally managed in https://github.com//.github/ -# Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in -# the above-mentioned repo. - -name: Qodana - -on: - pull_request: - branches: [master, nightly] - types: [opened, synchronize, reopened] - push: - branches: [master, nightly] - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - qodana_initial_check: - name: Qodana Initial Check - permissions: - actions: write # required to use workflow dispatch on fork PRs - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Prepare - id: prepare - run: | - # check the branch variable - if [ "${{ github.event_name }}" == "push" ] - then - echo "This is a PUSH event" - # use the branch name - destination=${{ github.ref_name }} - target=${{ github.ref_name }} - else - echo "This is a PR event" - # use the PR number - destination=${{ github.event.pull_request.number }} - target=${{ github.event.pull_request.base.ref }} - fi - - echo "checkout_repo=$checkout_repo" >> $GITHUB_OUTPUT - echo "checkout_ref=$checkout_ref" >> $GITHUB_OUTPUT - echo "destination=$destination" >> $GITHUB_OUTPUT - echo "target=$target" >> $GITHUB_OUTPUT - - # prepare urls - base=https://${{ github.repository_owner }}.github.io - report_url=${base}/qodana-reports/${{ github.event.repository.name }}/${destination} - echo "report_url=$report_url" >> $GITHUB_OUTPUT - - # build matrix - files=$(find . -type f -iname "qodana*.yaml") - - echo "files: ${files}" - - # do not quote to keep this as a single line - echo files=${files} >> $GITHUB_OUTPUT - - MATRIX_COMBINATIONS="" - REPORTS_MARKDOWN="" - for FILE in ${files}; do - # extract the language from file name after `qodana-` and before `.yaml` - language=$(echo $FILE | sed -r -z -e 's/(\.\/)*.*\/(qodana.yaml)/default/gm') - if [[ $language != "default" ]]; then - language=$(echo $FILE | sed -r -z -e 's/(\.\/)*.*qodana-(.*).yaml/\2/gm') - fi - MATRIX_COMBINATIONS="$MATRIX_COMBINATIONS {\"file\": \"$FILE\", \"language\": \"$language\"}," - REPORTS_MARKDOWN="$REPORTS_MARKDOWN
- [${language}](${report_url}/${language})" - done - - # removes the last character (i.e. comma) - MATRIX_COMBINATIONS=${MATRIX_COMBINATIONS::-1} - - # setup matrix for later jobs - matrix=$(( - echo "{ \"include\": [$MATRIX_COMBINATIONS] }" - ) | jq -c .) - - echo $matrix - echo $matrix | jq . - echo "matrix=$matrix" >> $GITHUB_OUTPUT - - echo "reports_markdown=$REPORTS_MARKDOWN" >> $GITHUB_OUTPUT - - - name: Setup initial notification inputs - id: inputs - if: >- - startsWith(github.event_name, 'pull_request') && - steps.prepare.outputs.files != '' - run: | - # workflow logs - workflow_url_a=https://github.com/${{ github.repository_owner }}/${{ github.event.repository.name }} - workflow_url=${workflow_url_a}/actions/runs/${{ github.run_id }} - - # multiline message - message=$(cat <<- EOF - :warning: **Qodana is checking this PR** :warning: - Live results available [here](${workflow_url}) - EOF - ) - - # escape json control characters - message=$(jq -n --arg message "$message" '$message' | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g') - - secondary_inputs=$(echo '{ - "issue_message": "'"${message}"'", - "issue_message_id": "'"qodana"'", - "issue_number": "'"${{ github.event.number }}"'", - "issue_repo_owner": "'"${{ github.repository_owner }}"'", - "issue_repo_name": "'"${{ github.event.repository.name }}"'" - }' | jq -r .) - - #escape json control characters - secondary_inputs=$(jq -n --arg secondary_inputs "$secondary_inputs" '$secondary_inputs' \ - | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g') - - echo $secondary_inputs - - # secondary input as string, not JSON - # todo - change dispatch_ref to master instead of nightly - primary_inputs=$(echo '{ - "dispatch_repository": "'"${{ github.repository_owner }}/.github"'", - "dispatch_workflow": "'"dispatch-issue-comment.yml"'", - "dispatch_ref": "'"nightly"'", - "dispatch_inputs": "'"${secondary_inputs}"'" - }' | jq -c .) - - echo $primary_inputs - echo $primary_inputs | jq . - echo "primary_inputs=$primary_inputs" >> $GITHUB_OUTPUT - - - name: Workflow Dispatch - if: >- - startsWith(github.event_name, 'pull_request') && - steps.prepare.outputs.files != '' - uses: benc-uk/workflow-dispatch@v1.2.2 - continue-on-error: true # this might error if the workflow is not found, but we still want to run the next job - with: - ref: ${{ github.base_ref || github.ref_name }} # base ref for PR and branch name for push - workflow: dispatcher.yml - inputs: ${{ steps.inputs.outputs.primary_inputs }} - token: ${{ github.token }} - - outputs: - destination: ${{ steps.prepare.outputs.destination }} - target: ${{ steps.prepare.outputs.target }} - files: ${{ steps.prepare.outputs.files }} - reports_markdown: ${{ steps.prepare.outputs.reports_markdown }} - matrix: ${{ steps.prepare.outputs.matrix }} - - qodana: - if: ${{ needs.qodana_initial_check.outputs.files != '' }} - needs: [qodana_initial_check] - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: ${{ fromJson(needs.qodana_initial_check.outputs.matrix) }} - name: Qodana-Scan-${{ matrix.language }} - continue-on-error: true - steps: - - name: Checkout - uses: actions/checkout@v3 - with: - submodules: recursive - - - name: Get baseline - id: baseline - run: | - # check if destination is not an integer - if ! [[ "${{ needs.qodana_initial_check.outputs.destination }}" =~ ^[0-9]+$ ]] - then - echo "Running for a branch update" - echo "baseline_args=" >> $GITHUB_OUTPUT - else - echo "Running for a PR" - - sarif_file=qodana.sarif.json - repo=${{ github.event.repository.name }} - target=${{ needs.qodana_initial_check.outputs.target }} - language=${{ matrix.language }} - - baseline_file="${repo}/${target}/${language}/results/${sarif_file}" - baseline_file_url="https://lizardbyte.github.io/qodana-reports/${baseline_file}" - - # don't fail if file does not exist - wget ${baseline_file_url} || true - - # check if file exists - if [ -f ${sarif_file} ] - then - echo "baseline exists" - echo "baseline_args=--baseline,${sarif_file}" >> $GITHUB_OUTPUT - else - echo "baseline does not exist" - echo "baseline_args=" >> $GITHUB_OUTPUT - fi - fi - - - name: Rename Qodana config file - id: rename - run: | - # rename the file - if [ "${{ matrix.file }}" != "./qodana.yaml" ] - then - mv -f ${{ matrix.file }} ./qodana.yaml - fi - - - name: Qodana - id: qodana - continue-on-error: true # ensure dispatch-qodana job is run - uses: JetBrains/qodana-action@v2022.3.4 - with: - additional-cache-hash: ${{ github.ref }}-${{ matrix.language }} - artifact-name: qodana-${{ matrix.language }} # yamllint disable-line rule:line-length - args: '--print-problems,${{ steps.baseline.outputs.baseline_args }}' - pr-mode: false - upload-result: true - use-caches: true - - - name: Set output status - id: status - run: | - # check if qodana failed - echo "qodana_status=${{ steps.qodana.outcome }}" >> $GITHUB_OUTPUT - - outputs: - qodana_status: ${{ steps.status.outputs.qodana_status }} - - dispatch-qodana: - # trigger qodana-reports to download artifacts from the matrix runs - needs: [qodana_initial_check, qodana] - runs-on: ubuntu-latest - name: Dispatch Qodana - permissions: - actions: write # required to use workflow dispatch on fork PRs - if: ${{ needs.qodana_initial_check.outputs.files != '' }} - steps: - - name: Setup qodana publish inputs - id: inputs - run: | - # get the artifacts - artifacts=${{ toJson(steps.artifacts.outputs.result) }} - artifacts=$(echo $artifacts | jq -c .) - - # get the target branch - target=${{ needs.qodana_initial_check.outputs.target }} - - # get the destination branch - destination=${{ needs.qodana_initial_check.outputs.destination }} - - # client payload - secondary_inputs=$(echo '{ - "destination": "'"${destination}"'", - "ref": "'"${{ github.ref }}"'", - "repo": "'"${{ github.repository }}"'", - "repo_name": "'"${{ github.event.repository.name }}"'", - "run_id": "'"${{ github.run_id }}"'", - "reports_markdown": "'"${{ needs.qodana_initial_check.outputs.reports_markdown }}"'", - "status": "'"${{ needs.qodana.outputs.qodana_status }}"'" - }' | jq -r .) - - #escape json control characters - secondary_inputs=$(jq -n --arg secondary_inputs "$secondary_inputs" '$secondary_inputs' \ - | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g') - - echo $secondary_inputs - - primary_inputs=$(echo '{ - "dispatch_repository": "'"${{ github.repository_owner }}/qodana-reports"'", - "dispatch_workflow": "'"dispatch-qodana.yml"'", - "dispatch_ref": "'"master"'", - "dispatch_inputs": "'"$secondary_inputs"'" - }' | jq -c .) - - echo $primary_inputs - echo $primary_inputs | jq . - echo "primary_inputs=$primary_inputs" >> $GITHUB_OUTPUT - - - name: Workflow Dispatch - uses: benc-uk/workflow-dispatch@v1.2.2 - continue-on-error: true # this might error if the workflow is not found, but we don't want to fail the workflow - with: - ref: ${{ github.base_ref || github.ref_name }} # base ref for PR and branch name for push - workflow: dispatcher.yml - inputs: ${{ steps.inputs.outputs.primary_inputs }} - token: ${{ github.token }} diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000000..4ff15026b3b --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,212 @@ +--- +# This action is centrally managed in https://github.com//.github/ +# Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in +# the above-mentioned repo. + +# This workflow will analyze all supported languages in the repository using CodeQL Analysis. + +name: "CodeQL" + +on: + push: + branches: ["master"] + pull_request: + branches: ["master"] + schedule: + - cron: '00 12 * * 0' # every Sunday at 12:00 UTC + +concurrency: + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +jobs: + languages: + name: Get language matrix + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.lang.outputs.result }} + continue: ${{ steps.continue.outputs.result }} + steps: + - name: Get repo languages + uses: actions/github-script@v7 + id: lang + with: + script: | + // CodeQL supports ['cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby', 'swift'] + // Use only 'java' to analyze code written in Java, Kotlin or both + // Use only 'javascript' to analyze code written in JavaScript, TypeScript or both + // Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + const supported_languages = ['cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby', 'swift'] + + const remap_languages = { + 'c++': 'cpp', + 'c#': 'csharp', + 'kotlin': 'java', + 'typescript': 'javascript', + } + + const repo = context.repo + const response = await github.rest.repos.listLanguages(repo) + let matrix = { + "include": [] + } + + for (let [key, value] of Object.entries(response.data)) { + // remap language + if (remap_languages[key.toLowerCase()]) { + console.log(`Remapping language: ${key} to ${remap_languages[key.toLowerCase()]}`) + key = remap_languages[key.toLowerCase()] + } + if (supported_languages.includes(key.toLowerCase())) { + console.log(`Found supported language: ${key}`) + let osList = ['ubuntu-latest']; + if (key.toLowerCase() === 'swift') { + osList = ['macos-latest']; + } else if (key.toLowerCase() === 'cpp') { + // TODO: update macos to latest after the below issue is resolved + // https://github.com/github/codeql-action/issues/2266 + osList = ['macos-13', 'ubuntu-latest', 'windows-latest']; + } + for (let os of osList) { + // set name for matrix + if (osList.length == 1) { + name = key.toLowerCase() + } else { + name = `${key.toLowerCase()}, ${os}` + } + + // add to matrix + matrix['include'].push({"language": key.toLowerCase(), "os": os, "name": name}) + } + } + } + + // print languages + console.log(`matrix: ${JSON.stringify(matrix)}`) + + return matrix + + - name: Continue + uses: actions/github-script@v7 + id: continue + with: + script: | + // if matrix['include'] is an empty list return false, otherwise true + const matrix = ${{ steps.lang.outputs.result }} // this is already json encoded + + if (matrix['include'].length == 0) { + return false + } else { + return true + } + + analyze: + name: Analyze (${{ matrix.name }}) + if: ${{ needs.languages.outputs.continue == 'true' }} + defaults: + run: + shell: ${{ matrix.os == 'windows-latest' && 'msys2 {0}' || 'bash' }} + env: + GITHUB_CODEQL_BUILD: true + needs: [languages] + runs-on: ${{ matrix.os || 'ubuntu-latest' }} + timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.languages.outputs.matrix) }} + + steps: + - name: Maximize build space + if: >- + runner.os == 'Linux' && + matrix.language == 'cpp' + uses: easimon/maximize-build-space@v10 + with: + root-reserve-mb: 30720 + remove-dotnet: ${{ (matrix.language == 'csharp' && 'false') || 'true' }} + remove-android: 'true' + remove-haskell: 'true' + remove-codeql: 'false' + remove-docker-images: 'true' + + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Setup msys2 + if: >- + runner.os == 'Windows' && + matrix.language == 'cpp' + uses: msys2/setup-msys2@v2 + with: + msystem: ucrt64 + update: true + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # yamllint disable-line rule:line-length + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + config: | + paths-ignore: + - node_modules + - third-party + + # Pre autobuild + # create a file named .codeql-prebuild-${{ matrix.language }}.sh in the root of your repository + # create a file named .codeql-build-${{ matrix.language }}.sh in the root of your repository + - name: Prebuild + id: prebuild + run: | + # check if prebuild script exists + filename=".codeql-prebuild-${{ matrix.language }}-${{ runner.os }}.sh" + if [ -f "./${filename}" ]; then + echo "Running prebuild script: ${filename}" + ./${filename} + fi + + # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). + - name: Autobuild + if: steps.prebuild.outputs.skip_autobuild != 'true' + uses: github/codeql-action/autobuild@v3 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" + output: sarif-results + upload: failure-only + + - name: filter-sarif + uses: advanced-security/filter-sarif@v1 + with: + input: sarif-results/${{ matrix.language }}.sarif + output: sarif-results/${{ matrix.language }}.sarif + patterns: | + -node_modules/** + -third\-party/** + + - name: Upload SARIF + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: sarif-results/${{ matrix.language }}.sarif + + - name: Upload loc as a Build Artifact + uses: actions/upload-artifact@v4 + with: + name: sarif-results-${{ matrix.language }}-${{ runner.os }} + path: sarif-results + retention-days: 1 diff --git a/.github/workflows/cpp-lint.yml b/.github/workflows/cpp-lint.yml index fb1bf642dea..5d0df5ad76c 100644 --- a/.github/workflows/cpp-lint.yml +++ b/.github/workflows/cpp-lint.yml @@ -9,11 +9,11 @@ name: C++ Lint on: pull_request: - branches: [master, nightly] + branches: [master] types: [opened, synchronize, reopened] concurrency: - group: ${{ github.workflow }}-${{ github.ref }} + group: "${{ github.workflow }}-${{ github.ref }}" cancel-in-progress: true jobs: @@ -23,34 +23,52 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Find cpp files - id: cpp_files + id: find_files run: | - cpp_files=$(find . -type f -iname "*.cpp" -o -iname "*.h" -o -iname "*.m" -o -iname "*.mm") - - echo "found cpp files: ${cpp_files}" + # find files + found_files=$(find . -type f -iname "*.cpp" -o -iname "*.h" -o -iname "*.m" -o -iname "*.mm") + ignore_files=$(find . -type f -iname ".clang-format-ignore") + + # Loop through each C++ file + for file in $found_files; do + for ignore_file in $ignore_files; do + ignore_directory=$(dirname "$ignore_file") + # if directory of ignore_file is beginning of file + if [[ "$file" == "$ignore_directory"* ]]; then + echo "ignoring file: ${file}" + found_files="${found_files//${file}/}" + break 1 + fi + done + done + + # remove empty lines + found_files=$(echo "$found_files" | sed '/^\s*$/d') + + echo "found cpp files: ${found_files}" # do not quote to keep this as a single line - echo cpp_files=${cpp_files} >> $GITHUB_OUTPUT + echo found_files=${found_files} >> $GITHUB_OUTPUT - name: Clang format lint - if: ${{ steps.cpp_files.outputs.cpp_files }} - uses: DoozyX/clang-format-lint-action@v0.15 + if: ${{ steps.find_files.outputs.found_files }} + uses: DoozyX/clang-format-lint-action@v0.17 with: - source: ${{ steps.cpp_files.outputs.cpp_files }} + source: ${{ steps.find_files.outputs.found_files }} extensions: 'cpp,h,m,mm' - clangFormatVersion: 15 + clangFormatVersion: 16 style: file inplace: false - name: Upload Artifacts if: failure() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: clang-format-fixes - path: ${{ steps.cpp_files.outputs.cpp_files }} + path: ${{ steps.find_files.outputs.found_files }} cmake-lint: name: CMake Lint @@ -58,10 +76,10 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.11' @@ -70,15 +88,33 @@ jobs: python -m pip install --upgrade pip setuptools cmakelang - name: Find cmake files - id: cmake_files + id: find_files run: | - cmake_files=$(find . -type f -iname "CMakeLists.txt" -o -iname "*.cmake") - - echo "found cmake files: ${cmake_files}" + # find files + found_files=$(find . -type f -iname "CMakeLists.txt" -o -iname "*.cmake") + ignore_files=$(find . -type f -iname ".cmake-lint-ignore") + + # Loop through each C++ file + for file in $found_files; do + for ignore_file in $ignore_files; do + ignore_directory=$(dirname "$ignore_file") + # if directory of ignore_file is beginning of file + if [[ "$file" == "$ignore_directory"* ]]; then + echo "ignoring file: ${file}" + found_files="${found_files//${file}/}" + break 1 + fi + done + done + + # remove empty lines + found_files=$(echo "$found_files" | sed '/^\s*$/d') + + echo "found cmake files: ${found_files}" # do not quote to keep this as a single line - echo cmake_files=${cmake_files} >> $GITHUB_OUTPUT + echo found_files=${found_files} >> $GITHUB_OUTPUT - name: Test with cmake-lint run: | - cmake-lint --line-width 120 --tab-size 4 ${{ steps.cmake_files.outputs.cmake_files }} + cmake-lint --line-width 120 --tab-size 4 ${{ steps.find_files.outputs.found_files }} diff --git a/.github/workflows/dispatcher.yml b/.github/workflows/dispatcher.yml deleted file mode 100644 index c83a233ecea..00000000000 --- a/.github/workflows/dispatcher.yml +++ /dev/null @@ -1,69 +0,0 @@ ---- -# This action is centrally managed in https://github.com//.github/ -# Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in -# the above-mentioned repo. - -# This action receives a dispatch event and passes it through to another repo. This is a workaround to avoid issues -# where fork PRs do not have access to secrets. - -name: Dispatcher - -on: - workflow_dispatch: - inputs: - dispatch_repository: - description: 'Repository to dispatch to' - required: true - dispatch_workflow: - description: 'Workflow to dispatch to' - required: true - dispatch_ref: - description: 'Ref/branch to dispatch to' - required: true - dispatch_inputs: - description: 'Inputs to send' - required: true - -jobs: - dispatcher: - name: Repository Dispatch - runs-on: ubuntu-latest - steps: - - name: Unescape JSON control characters - id: inputs - run: | - # get the inputs - dispatch_inputs=${{ github.event.inputs.dispatch_inputs }} - echo "$dispatch_inputs" - - # temporarily replace newlines with a placeholder - dispatch_inputs=$(echo ${dispatch_inputs} | sed 's/\\\\n/_!new_line!_/g') - - # remove newline characters - dispatch_inputs=$(echo ${dispatch_inputs} | sed 's/\\n//g') - - # replace placeholder with newline - dispatch_inputs=$(echo ${dispatch_inputs} | sed 's/_!new_line!_/\\n/g') - - # replace escaped quotes with unescaped quotes - dispatch_inputs=$(echo ${dispatch_inputs} | sed 's/\\"//g') - - # debug echo - echo "$dispatch_inputs" - - # parse as JSON - dispatch_inputs=$(echo "$dispatch_inputs" | jq -c .) - - # debug echo - echo "$dispatch_inputs" - - echo "dispatch_inputs=$dispatch_inputs" >> $GITHUB_OUTPUT - - - name: Workflow Dispatch - uses: benc-uk/workflow-dispatch@v1.2.2 - with: - repo: ${{ github.event.inputs.dispatch_repository }} - ref: ${{ github.event.inputs.dispatch_ref || 'master' }} # default to master if not specified - workflow: ${{ github.event.inputs.dispatch_workflow }} - inputs: ${{ steps.inputs.outputs.dispatch_inputs }} - token: ${{ secrets.GH_BOT_TOKEN || github.token }} # fallback to default token if not specified diff --git a/.github/workflows/issues-stale.yml b/.github/workflows/issues-stale.yml index c168034ee3b..deb3d74b961 100644 --- a/.github/workflows/issues-stale.yml +++ b/.github/workflows/issues-stale.yml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Stale - uses: actions/stale@v8 + uses: actions/stale@v9 with: close-issue-message: > This issue was closed because it has been stalled for 10 days with no activity. @@ -31,16 +31,19 @@ jobs: exempt-pr-labels: 'dependencies,l10n' stale-issue-label: 'stale' stale-issue-message: > - This issue is stale because it has been open for 90 days with no activity. - Comment or remove the stale label, otherwise this will be closed in 10 days. + It seems this issue hasn't had any activity in the past 90 days. + If it's still something you'd like addressed, please let us know by leaving a comment. + Otherwise, to help keep our backlog tidy, we'll be closing this issue in 10 days. Thanks! stale-pr-label: 'stale' stale-pr-message: > - This PR is stale because it has been open for 90 days with no activity. - Comment or remove the stale label, otherwise this will be closed in 10 days. + It looks like this PR has been idle for 90 days. + If it's still something you're working on or would like to pursue, + please leave a comment or update your branch. + Otherwise, we'll be closing this PR in 10 days to reduce our backlog. Thanks! repo-token: ${{ secrets.GH_BOT_TOKEN }} - name: Invalid Template - uses: actions/stale@v8 + uses: actions/stale@v9 with: close-issue-message: > This issue was closed because the the template was not completed after 5 days. @@ -48,7 +51,6 @@ jobs: This PR was closed because the the template was not completed after 5 days. days-before-stale: 0 days-before-close: 5 - exempt-pr-labels: 'dependencies,l10n' only-labels: 'invalid:template-incomplete' stale-issue-label: 'invalid:template-incomplete' stale-issue-message: > diff --git a/.github/workflows/issues.yml b/.github/workflows/issues.yml index d7a1025cdce..aec6006c870 100644 --- a/.github/workflows/issues.yml +++ b/.github/workflows/issues.yml @@ -20,6 +20,6 @@ jobs: runs-on: ubuntu-latest steps: - name: Label Actions - uses: dessant/label-actions@v3 + uses: dessant/label-actions@v4 with: github-token: ${{ secrets.GH_BOT_TOKEN }} diff --git a/.github/workflows/localize.yml b/.github/workflows/localize.yml index d2a236c54e5..da0997a32bc 100644 --- a/.github/workflows/localize.yml +++ b/.github/workflows/localize.yml @@ -3,7 +3,7 @@ name: localize on: push: - branches: [nightly] + branches: [master] paths: # prevents workflow from running unless these files change - '.github/workflows/localize.yml' - 'src/**' @@ -20,10 +20,10 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Python 3.9 - uses: actions/setup-python@v4 # https://github.com/actions/setup-python + uses: actions/setup-python@v5 # https://github.com/actions/setup-python with: python-version: '3.9' @@ -77,7 +77,7 @@ jobs: run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT - name: Create/Update Pull Request - uses: peter-evans/create-pull-request@v5 + uses: peter-evans/create-pull-request@v6 with: add-paths: | locale/*.po @@ -85,7 +85,7 @@ jobs: commit-message: New localization template branch: localize/update delete-branch: true - base: nightly + base: master title: New Babel Updates body: | Update report diff --git a/.github/workflows/pull-requests.yml b/.github/workflows/pull-requests.yml deleted file mode 100644 index 58243872498..00000000000 --- a/.github/workflows/pull-requests.yml +++ /dev/null @@ -1,32 +0,0 @@ ---- -# This action is centrally managed in https://github.com//.github/ -# Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in -# the above-mentioned repo. - -# Ensure PRs are made against `nightly` branch. - -name: Pull Requests - -on: - pull_request_target: - types: [opened, synchronize, edited, reopened] - -# no concurrency for pull_request_target events - -jobs: - check-pull-request: - name: Check Pull Request - if: startsWith(github.repository, 'LizardByte/') - runs-on: ubuntu-latest - steps: - - uses: Vankka/pr-target-branch-action@v2 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - target: master - exclude: nightly # Don't prevent going from nightly -> master - change-to: nightly - comment: | - Your PR was set to `master`, PRs should be sent to `nightly`. - The base branch of this PR has been automatically changed to `nightly`. - Please check that there are no merge conflicts diff --git a/.github/workflows/python-flake8.yml b/.github/workflows/python-flake8.yml index 19bcdb9d8a2..61e23f741b7 100644 --- a/.github/workflows/python-flake8.yml +++ b/.github/workflows/python-flake8.yml @@ -9,11 +9,11 @@ name: flake8 on: pull_request: - branches: [master, nightly] + branches: [master] types: [opened, synchronize, reopened] concurrency: - group: ${{ github.workflow }}-${{ github.ref }} + group: "${{ github.workflow }}-${{ github.ref }}" cancel-in-progress: true jobs: @@ -21,10 +21,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 # https://github.com/actions/setup-python + uses: actions/setup-python@v5 # https://github.com/actions/setup-python with: python-version: '3.10' diff --git a/.github/workflows/release-notifier-moonlight.yml b/.github/workflows/release-notifier-moonlight.yml index 9ac896f376a..9369a7fabc5 100644 --- a/.github/workflows/release-notifier-moonlight.yml +++ b/.github/workflows/release-notifier-moonlight.yml @@ -3,10 +3,15 @@ name: Release Notifications (Moonlight) on: release: - types: [published] + types: + - released # this triggers when a release is published, but does not include prereleases or drafts jobs: discord: + if: >- + startsWith(github.repository, 'LizardByte/') && + !github.event.release.prerelease && + !github.event.release.draft runs-on: ubuntu-latest steps: - name: discord diff --git a/.github/workflows/release-notifier.yml b/.github/workflows/release-notifier.yml index ed7b3ef20ff..60608394ba1 100644 --- a/.github/workflows/release-notifier.yml +++ b/.github/workflows/release-notifier.yml @@ -9,16 +9,19 @@ name: Release Notifications on: release: - types: [published] - # https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#onevent_nametypes + types: + - released # this triggers when a release is published, but does not include prereleases or drafts jobs: discord: - if: startsWith(github.repository, 'LizardByte/') + if: >- + startsWith(github.repository, 'LizardByte/') && + !github.event.release.prerelease && + !github.event.release.draft runs-on: ubuntu-latest steps: - name: discord - uses: sarisia/actions-status-discord@v1 # https://github.com/sarisia/actions-status-discord + uses: sarisia/actions-status-discord@v1 with: webhook: ${{ secrets.DISCORD_RELEASE_WEBHOOK }} nodetail: true @@ -30,11 +33,14 @@ jobs: color: 0xFF4500 facebook_group: - if: startsWith(github.repository, 'LizardByte/') + if: >- + startsWith(github.repository, 'LizardByte/') && + !github.event.release.prerelease && + !github.event.release.draft runs-on: ubuntu-latest steps: - name: facebook-post-action - uses: ReenigneArcher/facebook-post-action@v1 # https://github.com/ReenigneArcher/facebook-post-action + uses: ReenigneArcher/facebook-post-action@v1 with: page_id: ${{ secrets.FACEBOOK_GROUP_ID }} access_token: ${{ secrets.FACEBOOK_ACCESS_TOKEN }} @@ -44,11 +50,14 @@ jobs: url: ${{ github.event.release.html_url }} facebook_page: - if: startsWith(github.repository, 'LizardByte/') + if: >- + startsWith(github.repository, 'LizardByte/') && + !github.event.release.prerelease && + !github.event.release.draft runs-on: ubuntu-latest steps: - name: facebook-post-action - uses: ReenigneArcher/facebook-post-action@v1 # https://github.com/ReenigneArcher/facebook-post-action + uses: ReenigneArcher/facebook-post-action@v1 with: page_id: ${{ secrets.FACEBOOK_PAGE_ID }} access_token: ${{ secrets.FACEBOOK_ACCESS_TOKEN }} @@ -58,11 +67,14 @@ jobs: url: ${{ github.event.release.html_url }} reddit: - if: startsWith(github.repository, 'LizardByte/') + if: >- + startsWith(github.repository, 'LizardByte/') && + !github.event.release.prerelease && + !github.event.release.draft runs-on: ubuntu-latest steps: - name: reddit - uses: bluwy/release-for-reddit-action@v2 # https://github.com/bluwy/release-for-reddit-action + uses: bluwy/release-for-reddit-action@v2 with: username: ${{ secrets.REDDIT_USERNAME }} password: ${{ secrets.REDDIT_PASSWORD }} @@ -75,14 +87,17 @@ jobs: comment: ${{ github.event.release.body }} twitter: - if: startsWith(github.repository, 'LizardByte/') + if: >- + startsWith(github.repository, 'LizardByte/') && + !github.event.release.prerelease && + !github.event.release.draft runs-on: ubuntu-latest steps: - name: twitter - uses: ethomson/send-tweet-action@v1 # https://github.com/ethomson/send-tweet-action + uses: nearform-actions/github-action-notify-twitter@v1 with: - consumer-key: ${{ secrets.TWITTER_API_KEY }} - consumer-secret: ${{ secrets.TWITTER_API_SECRET }} - access-token: ${{ secrets.TWITTER_ACCESS_TOKEN }} - access-token-secret: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }} - status: ${{ github.event.release.html_url }} + message: ${{ github.event.release.html_url }} + twitter-app-key: ${{ secrets.TWITTER_API_KEY }} + twitter-app-secret: ${{ secrets.TWITTER_API_SECRET }} + twitter-access-token: ${{ secrets.TWITTER_ACCESS_TOKEN }} + twitter-access-token-secret: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }} diff --git a/.github/workflows/update-changelog.yml b/.github/workflows/update-changelog.yml new file mode 100644 index 00000000000..d5bbed67100 --- /dev/null +++ b/.github/workflows/update-changelog.yml @@ -0,0 +1,31 @@ +--- +# This action is centrally managed in https://github.com//.github/ +# Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in +# the above-mentioned repo. + +# Update changelog on release events. + +name: Update changelog + +on: + release: + types: [created, edited, deleted] + workflow_dispatch: + +concurrency: + group: "${{ github.workflow }}" + cancel-in-progress: true + +jobs: + update-changelog: + if: >- + github.event_name == 'workflow_dispatch' || + (!github.event.release.prerelease && !github.event.release.draft) + runs-on: ubuntu-latest + steps: + - name: Update Changelog + uses: LizardByte/update-changelog-action@v2024.520.183314 + with: + changelogBranch: changelog + changelogFile: CHANGELOG.md + token: ${{ secrets.GH_BOT_TOKEN }} diff --git a/.github/workflows/update-homebrew-release.yml b/.github/workflows/update-homebrew-release.yml new file mode 100644 index 00000000000..c75fc1c41e2 --- /dev/null +++ b/.github/workflows/update-homebrew-release.yml @@ -0,0 +1,71 @@ +--- +# This action is a candidate to centrally manage in https://github.com//.github/ +# If more Homebrew applications are developed, consider moving this action to the organization's .github repository, +# using the `homebrew-pkg` repository label to identify repositories that should trigger have this workflow. + +# Update Homebrew on release events. + +name: Update Homebrew release + +on: + release: + types: [created, edited] + +concurrency: + group: "${{ github.workflow }}-${{ github.event.release.tag_name }}" + cancel-in-progress: true + +jobs: + update-homebrew-release: + if: >- + github.repository_owner == 'LizardByte' && + !github.event.release.draft && !github.event.release.prerelease + runs-on: ubuntu-latest + steps: + - name: Check if Homebrew repo + env: + TOPIC: homebrew-pkg + id: check + uses: actions/github-script@v7 + with: + script: | + const topic = process.env.TOPIC; + console.log(`Checking if repo has topic: ${topic}`); + + const repoTopics = await github.rest.repos.getAllTopics({ + owner: context.repo.owner, + repo: context.repo.repo + }); + console.log(`Repo topics: ${repoTopics.data.names}`); + + const hasTopic = repoTopics.data.names.includes(topic); + console.log(`Has topic: ${hasTopic}`); + + core.setOutput('hasTopic', hasTopic); + + - name: Download release asset + id: download + if: >- + steps.check.outputs.hasTopic == 'true' + uses: robinraju/release-downloader@v1.10 + with: + repository: "${{ github.repository }}" + tag: "${{ github.event.release.tag_name }}" + fileName: "*.rb" + tarBall: false + zipBall: false + out-file-path: "release_downloads" + extract: false + + - name: Publish Homebrew Formula + if: >- + steps.check.outputs.hasTopic == 'true' && + fromJson(steps.download.outputs.downloaded_files)[0] + uses: LizardByte/homebrew-release-action@v2024.522.222851 + with: + formula_file: ${{ fromJson(steps.download.outputs.downloaded_files)[0] }} + git_email: ${{ secrets.GH_BOT_EMAIL }} + git_username: ${{ secrets.GH_BOT_NAME }} + publish: true + token: ${{ secrets.GH_BOT_TOKEN }} + validate: false diff --git a/.github/workflows/update-pages.yml b/.github/workflows/update-pages.yml new file mode 100644 index 00000000000..5db0f94da68 --- /dev/null +++ b/.github/workflows/update-pages.yml @@ -0,0 +1,62 @@ +--- +name: Build GH-Pages + +on: + pull_request: + branches: [master] + types: [opened, synchronize, reopened] + push: + branches: [master] + workflow_dispatch: + +concurrency: + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +jobs: + update_pages: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Checkout gh-pages + uses: actions/checkout@v4 + with: + ref: gh-pages + path: gh-pages + persist-credentials: false # otherwise, the token used is the GITHUB_TOKEN, instead of the personal token + fetch-depth: 0 # otherwise, will fail to push refs to dest repo + + - name: Prepare gh-pages + run: | + # empty contents + rm -f -r ./gh-pages/* + + # copy template back to pages + cp -f -r ./gh-pages-template/. ./gh-pages/ + + - name: Upload Artifacts + if: ${{ github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' }} + uses: actions/upload-artifact@v4 + with: + name: gh-pages + if-no-files-found: error # 'warn' or 'ignore' are also available, defaults to `warn` + path: | + ${{ github.workspace }}/gh-pages + !**/*.git + + - name: Deploy to gh-pages + if: >- + (github.event_name == 'push' && github.ref == 'refs/heads/master') || + (github.event_name == 'workflow_dispatch') + uses: actions-js/push@v1.5 + with: + github_token: ${{ secrets.GH_BOT_TOKEN }} + author_email: ${{ secrets.GH_BOT_EMAIL }} + author_name: ${{ secrets.GH_BOT_NAME }} + directory: gh-pages + branch: gh-pages + force: false + message: sync gh-pages to ${{ github.sha }} diff --git a/.github/workflows/update-winget-release.yml b/.github/workflows/update-winget-release.yml new file mode 100644 index 00000000000..266bbcad08a --- /dev/null +++ b/.github/workflows/update-winget-release.yml @@ -0,0 +1,69 @@ +--- +# This action is a candidate to centrally manage in https://github.com//.github/ +# If more Winget applications are developed, consider moving this action to the organization's .github repository, +# using the `winget-pkg` repository label to identify repositories that should trigger have this workflow. + +# Update Winget on release events. + +name: Update Winget release + +on: + release: + types: [created, edited] + +concurrency: + group: "${{ github.workflow }}-${{ github.event.release.tag_name }}" + cancel-in-progress: true + +jobs: + update-winget-release: + if: >- + github.repository_owner == 'LizardByte' && + !github.event.release.draft && !github.event.release.prerelease + runs-on: ubuntu-latest + steps: + - name: Check if Winget repo + env: + TOPIC: winget-pkg + id: check + uses: actions/github-script@v7 + with: + script: | + const topic = process.env.TOPIC; + console.log(`Checking if repo has topic: ${topic}`); + + const repoTopics = await github.rest.repos.getAllTopics({ + owner: context.repo.owner, + repo: context.repo.repo + }); + console.log(`Repo topics: ${repoTopics.data.names}`); + + const hasTopic = repoTopics.data.names.includes(topic); + console.log(`Has topic: ${hasTopic}`); + + core.setOutput('hasTopic', hasTopic); + + - name: Download release asset + id: download + if: >- + steps.check.outputs.hasTopic == 'true' + uses: robinraju/release-downloader@v1.10 + with: + repository: "${{ github.repository }}" + tag: "${{ github.event.release.tag_name }}" + fileName: "*.exe" + tarBall: false + zipBall: false + out-file-path: "release_downloads" + extract: false + + - name: Release to WinGet + if: >- + steps.check.outputs.hasTopic == 'true' && + fromJson(steps.download.outputs.downloaded_files)[0] + uses: vedantmgoyal2009/winget-releaser@v2 + with: + identifier: "${{ github.repository_owner }}.${{ github.event.repository.name }}" + release-tag: ${{ github.event.release.tag_name }} + installers-regex: '\.exe$' + token: ${{ secrets.GH_BOT_TOKEN }} diff --git a/.github/workflows/yaml-lint.yml b/.github/workflows/yaml-lint.yml index 6327d5d6326..023b836c0ce 100644 --- a/.github/workflows/yaml-lint.yml +++ b/.github/workflows/yaml-lint.yml @@ -9,11 +9,11 @@ name: yaml lint on: pull_request: - branches: [master, nightly] + branches: [master] types: [opened, synchronize, reopened] concurrency: - group: ${{ github.workflow }}-${{ github.ref }} + group: "${{ github.workflow }}-${{ github.ref }}" cancel-in-progress: true jobs: @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Find additional files id: find-files diff --git a/.gitignore b/.gitignore index c3a04d9652d..ccc8b0c7487 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,45 @@ -build -cmake-build* -.DS_Store -.vscode -.vs -*.swp -*.kdev4 +# Prerequisites +*.d + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib -.cache -.idea +# Executables +*.exe +*.out +*.app + +# JetBrains IDE +.idea/ + +# VSCode IDE +.vscode/ + +# build directories +build/ +cmake-*/ # npm node_modules/ @@ -16,3 +48,6 @@ package-lock.json # Translations *.mo *.pot + +# Dummy macOS files +.DS_Store diff --git a/.gitmodules b/.gitmodules index a3e39729c0a..17365eb8ca6 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,52 +1,56 @@ +[submodule "packaging/linux/flatpak/deps/org.flatpak.Builder.BaseApp"] + path = packaging/linux/flatpak/deps/org.flatpak.Builder.BaseApp + url = https://github.com/flathub/org.flatpak.Builder.BaseApp + branch = branch/23.08 +[submodule "packaging/linux/flatpak/deps/shared-modules"] + path = packaging/linux/flatpak/deps/shared-modules + url = https://github.com/flathub/shared-modules + branch = master +[submodule "third-party/build-deps"] + path = third-party/build-deps + url = https://github.com/LizardByte/build-deps.git + branch = dist +[submodule "third-party/googletest"] + path = third-party/googletest + url = https://github.com/google/googletest/ + branch = v1.14.x [submodule "third-party/moonlight-common-c"] path = third-party/moonlight-common-c url = https://github.com/moonlight-stream/moonlight-common-c.git branch = master -[submodule "third-party/Simple-Web-Server"] - path = third-party/Simple-Web-Server - url = https://gitlab.com/eidheim/Simple-Web-Server.git - branch = master -[submodule "third-party/ViGEmClient"] - path = third-party/ViGEmClient - url = https://github.com/ViGEm/ViGEmClient - branch = master -[submodule "third-party/miniupnp"] - path = third-party/miniupnp - url = https://github.com/miniupnp/miniupnp +[submodule "third-party/nanors"] + path = third-party/nanors + url = https://github.com/sleepybishop/nanors.git branch = master [submodule "third-party/nv-codec-headers"] path = third-party/nv-codec-headers url = https://github.com/FFmpeg/nv-codec-headers - branch = sdk/11.1 + branch = sdk/12.0 +[submodule "third-party/nvapi-open-source-sdk"] + path = third-party/nvapi-open-source-sdk + url = https://github.com/LizardByte/nvapi-open-source-sdk + branch = sdk +[submodule "third-party/Simple-Web-Server"] + path = third-party/Simple-Web-Server + url = https://gitlab.com/eidheim/Simple-Web-Server.git + branch = master [submodule "third-party/TPCircularBuffer"] path = third-party/TPCircularBuffer url = https://github.com/michaeltyson/TPCircularBuffer branch = master -[submodule "third-party/ffmpeg-windows-x86_64"] - path = third-party/ffmpeg-windows-x86_64 - url = https://github.com/LizardByte/build-deps - branch = ffmpeg-windows-x86_64 -[submodule "third-party/ffmpeg-macos-x86_64"] - path = third-party/ffmpeg-macos-x86_64 - url = https://github.com/LizardByte/build-deps - branch = ffmpeg-macos-x86_64 -[submodule "third-party/ffmpeg-linux-x86_64"] - path = third-party/ffmpeg-linux-x86_64 - url = https://github.com/LizardByte/build-deps - branch = ffmpeg-linux-x86_64 -[submodule "third-party/ffmpeg-linux-aarch64"] - path = third-party/ffmpeg-linux-aarch64 - url = https://github.com/LizardByte/build-deps - branch = ffmpeg-linux-aarch64 -[submodule "third-party/ffmpeg-macos-aarch64"] - path = third-party/ffmpeg-macos-aarch64 - url = https://github.com/LizardByte/build-deps - branch = ffmpeg-macos-aarch64 -[submodule "third-party/nanors"] - path = third-party/nanors - url = https://github.com/sleepybishop/nanors.git - branch = master [submodule "third-party/tray"] path = third-party/tray - url = https://github.com/dmikushin/tray + url = https://github.com/LizardByte/tray + branch = master +[submodule "third-party/ViGEmClient"] + path = third-party/ViGEmClient + url = https://github.com/LizardByte/Virtual-Gamepad-Emulation-Client.git + branch = master +[submodule "third-party/wayland-protocols"] + path = third-party/wayland-protocols + url = https://gitlab.freedesktop.org/wayland/wayland-protocols.git + branch = main +[submodule "third-party/wlr-protocols"] + path = third-party/wlr-protocols + url = https://gitlab.freedesktop.org/wlroots/wlr-protocols.git branch = master diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 3a0f243378b..4a6aefb36d5 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -12,7 +12,13 @@ build: tools: python: "3.11" apt_packages: - - graphviz + - graphviz # required to build diagrams + - libboost-locale-dev # required for rstcheck in cpp code block + jobs: + post_build: + - find ./third-party -iname "*.rst" -type f -delete # find and delete rst files in third-party + - rstcheck -r . # lint rst files + # - rstfmt --check --diff -w 120 . # check rst formatting # submodules required for include statements submodules: @@ -31,4 +37,3 @@ formats: all python: install: - requirements: ./docs/requirements.txt - system_packages: true diff --git a/.rstcheck.cfg b/.rstcheck.cfg new file mode 100644 index 00000000000..ca6beba81ae --- /dev/null +++ b/.rstcheck.cfg @@ -0,0 +1,10 @@ +# configuration file for rstcheck, an rst linting tool +# https://rstcheck.readthedocs.io/en/latest/usage/config + +[rstcheck] +ignore_directives = + doxygenfile, + include, + mdinclude, + tab, + todo, diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 419e16025e0..00000000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,492 +0,0 @@ -# Changelog - -## [0.20.0] - 2023-05-28 -**Breaking** -- (Windows) The Windows installer version of Sunshine is now always launched by the Sunshine Service. Manually launching Sunshine.exe from Program Files is no longer supported. This was necessary to address security issues caused by non-admin users having access to Sunshine's config data. If you have set up Task Scheduler or other mechanisms to launch Sunshine automatically, remove those from your system before updating. -- (Windows) The Start Menu shortcut has been redesigned for use with the Sunshine Service. It now launches Sunshine in the background (if not already running) and opens the Web UI, avoiding the persistent Command Prompt window present in prior versions. The Start Menu shortcut is now the recommended method for opening the Web UI and launching Sunshine. -- (Network/UPnP) If the Moonlight Internet Hosting Tool is installed alongside Sunshine, you must remove it or upgrade to v5.6 or later to prevent conflicts with Sunshine's UPnP support. As a reminder, the Moonlight Internet Hosting Tool is not required to stream over the Internet with Sunshine. Instead, simply enable UPnP in the Sunshine Web UI. -- (Windows) If Steam is installed, the Steam Streaming Speakers driver will be automatically installed when starting a stream for the first time. This behavior can be disabled in the Audio/Video tab of the Web UI. This Steam driver enables support for surround sound and muting host audio without requiring any manual configuration. -- (Input) The Back Button Timeout option has been renamed to Guide Button Emulation Timeout and has been disabled by default to ensure long presses on the Back button work by default. The previous behavior can be restored by setting the Guide Button Emulation Timeout to 2000. -- (Windows) The service name of SunshineSvc has been changed to SunshineService to address a false positive in MalwareBytes. If you have any scripts or other logic on your system that is using the service name, you will need to update that for the new name. -- (Windows) To support new installer features, install-service.bat no longer sets the service to auto-start by default. Users executing install-service.bat manually on the Sunshine portable build must also execute autostart-service.bat to start Sunshine on boot. However, installing the service manually like this is not recommended. Instead, use the Sunshine installer which handles service installation and configuration automatically. -- (Linux/Fedora) Fedora 36 builds are removed due to upstream end of support - -**Added** -- (Windows) Added an option to launch apps and prep/undo commands as administrator -- (Installer/Windows) Added an option to choose whether Sunshine launches on boot. If not configured to launch on boot, use the Start Menu shortcut to start Sunshine when desired. -- (Input/Windows) Added option to send VK codes instead of scancodes for compatibility with iOS/Android devices using non-English keyboard layouts -- (UI) The Apply/Restart option is now available in the Web UI for all platforms -- (Systray) Added a Restart option to the system tray context menu -- (Video/Windows) Added host latency data to video frames. This requires future updates to Moonlight to display in the on-screen overlay. -- (Audio) Added support for matching Audio Sink and Virtual Sink values on device names -- (Client) Added friendly error messages for clients when streaming fails to start -- (Video/Windows) Added warning log messages when Windows is hiding DRM-protected content from display capture -- (Interop/Windows) Added warning log messages when GeForce Experience is currently using the same ports as Sunshine -- (Linux/Fedora) Fedora 38 builds are now available - -**Changed** -- (Video) Encoder selection now happens at each stream start for more reliable GPU detection -- (Video/Windows) The host display now stays on while clients are actively streaming -- (Audio) Streaming will no longer fail if audio capture is unavailable -- (Audio/Windows) Sunshine will automatically switch back to the Virtual Sink if the default audio device is changed while streaming -- (Audio) Changes to the host audio playback option will now take effect when resuming a session from Moonlight -- (Audio/Windows) Sunshine will switch to a different default audio device if Steam Streaming Speakers are the default when Sunshine starts. This handles cases where Sunshine terminates unexpectedly without restoring the default audio device. -- (Apps) The Connection Terminated dialog will no longer appear in Moonlight when the app on the host exits normally -- (Systray/Windows) Quitting Sunshine via the system tray will now stop the Sunshine Service rather than leaving it running and allowing it to restart Sunshine -- (UI) The 100.64.0.0/10 CGN IP address range is now treated as a LAN address range -- (Video) Removed a workaround for some versions of Moonlight prior to mid-2022 -- (UI) The PIN field is now cleared after successfully pairing -- (UI) User names are now treated as case-insensitive -- (Linux) Changed udev rule to automatically grant access to virtual input devices -- (UI) Several item descriptions were adjusted to reflect newer configuration recommendations -- (UI) Placeholder text opacity has been reduced to improve contrast with non-placeholder text -- (Video/Windows) Minor capture performance improvements - -**Fixed** -- (Video) VRAM usage while streaming is significantly reduced, particularly with higher display resolutions and HDR -- (Network/UPnP) UPnP support was rewritten to fix several major issues handling router restarts, IP address changes, and port forwarding expiration -- (Input) Fixed modifier keys from the software keyboard on Android clients -- (Audio) Fixed handling of default audio device changes while streaming -- (Windows) Fixed streaming after Microsoft Remote Desktop or Fast User Switching has been used -- (Input) Fixed some games not recognizing emulated Guide button presses -- (Video/Windows) Fixed incorrect gamma when using an Advanced Color SDR display on the host -- (Installer/Windows) The installer is no longer blurry on High DPI systems -- (Systray/Windows) Fixed multiple system tray icons appearing if Sunshine exits unexpectedly -- (Systray/Windows) Fixed the system tray icon not appearing in several situations -- (Windows) Fixed hang on shutdown/restart if mDNS registration fails -- (UI) Fixed missing response headers -- (Stability) Fixed several possible crashes in cases where the client did not successfully connect -- (Stability) Fixed several memory leaks -- (Input/Windows) Back/Select input now correctly generates the Share button when emulating DS4 controllers -- (Audio/Windows) Fixed various bugs in audio-info.exe that led to inaccurate output on some systems -- (Video/Windows) Fixed incorrect resolution values from dxgi-info.exe on High DPI systems -- (Video/Linux) Fixed poor quality encoding from H.264 on Intel encoders -- (Config) Fixed a couple of typos in predefined resolutions - -**Dependencies** -- Bump sphinx-copybutton from 0.5.1 to 0.5.2 -- Bump sphinx from 6.13 to 7.0.1 -- Bump third-party/nv-codec-headers from 2055784 to 2cd175b -- Bump furo from 2023.3.27 to 2023.5.20 - -**Misc** -- (Build/Linux) Add X11 to PLATFORM_LIBARIES when found -- (Build/macOS) Fix compilation with Clang 14 -- (Docs) Fix nvlax URL -- (Docs) Add diagrams using graphviz -- (Docs) Improvements to source code documentation -- (Build) Unpin docker dependencies -- (Build/Linux) Honor install prefix for tray icon -- (Build/Windows) Unstripped binaries are now provided as a debuginfo package to support crash dump debugging -- (Config) Config directories are now created recursively - -## [0.19.1] - 2023-03-30 -**Fixed** -- (Audio) Fixed no audio issue introduced in v0.19.0 - -## [0.19.0] - 2023-03-29 -**Breaking** -- (Linux/Flatpak) Moved Flatpak to org.freedesktop.Platform 22.08 and Cuda 12.0.0 - This will drop support for Nvidia GPUs with compute capability 3.5 - -**Added** -- (Input) Added option to suppress input from gamepads, keyboards, or mice -- (Input/Linux) Added unicode support for remote pasting (may not work on all DEs) -- (Input/Linux) Added XTest input fallback -- (UI) Added version notifications to web UI -- (Linux/Windows) Add system tray icon -- (Windows) Added ability to safely elevate commands that fail due to insufficient permissions when running as a service -- (Config) Added global prep commands, and ability to exclude an app from using global prep commands -- (Installer/Windows) Automatically install ViGEmBus if selected - -**Changed** -- (Logging) Changed client verified messages to debug to prevent spamming the log -- (Config) Only save non default config values -- (Service/Linux) Use xdg-desktop-autostart for systemd service -- (Linux) Added config option to force capture method -- (Windows) Execute prep command in context of current user -- (Linux) Allow disconnected X11 outputs - -**Fixed** -- (Input/Windows) Fix issue where internation keys were not translated correct, and modifier keys appeared stuck -- (Linux) Fixed startup when /dev/dri didn't exist -- (UI) Changes software encoding settings to select menu instead of text input -- (Initialization) Do not terminate upon failure, allowing access to the web UI - -**Dependencies** -- Bump third-party/moonlight-common-c from 07beb0f to c9426a6 -- Bump babel from 2.11.0 to 2.12.1 -- Bump @fortawesome/fontawesome-free from 6.2.1 to 6.4.0 -- Bump third-party/ViGEmClient from 9e842ba to 726404e -- Bump ffmpeg -- Bump third-party/miniupnp from 014c9df to e439318 -- Bump furo from 2022.12.7 to 2023.3.27 -- Bump third-party/nanors from 395e5ad to e9e242e - -**Misc** -- (GitHub) Shared feature request board with Moonlight -- (Docs) Improved application examples -- (Docs) Added WIP documentation for source code using Doxygen and Breathe -- (Build) Fix linux clang build errors -- (Build/Archlinux) Skip irrelevant submodules -- (Build/Archlinux) Disable download timeout -- (Build/macOS) Support compiling for earlier releases of macOS -- (Docs) Add favicon -- (Docs) Add missing config default values -- (Build) Fix compiler warnings due to depreciated elements in C++17 -- (Build) Fix libcurl link errors -- (Clang) Adjusted formatting rules - -## [0.18.4] - 2023-02-20 -**Fixed** -- (Linux/AUR) Drop support of AUR package -- (Docker) General enhancements to docker images - -## [0.18.3] - 2023-02-13 -**Added** -- (Linux) Added PKGBUILD for Archlinux based distros to releases -- (Linux) Added precompiled package for Archlinux based distros to releases -- (Docker) Added archlinux docker image (x86_64 only) - -## [0.18.2] - 2023-02-13 -**Fixed** -- (Video/KMV/Linux) Fixed wayland capture on Nvidia for KMS -- (Video/Linux) Implement vaSyncBuffer stuf for libva <2.9.0 -- (UI) Fix issue where mime type was not being set for node_modules when using a reverse proxy -- (UI/macOS) Added missing audio sink config options -- (Linux) Specify correct Boost dependency versions -- (Video/AMF) Add missing encoder tunables - -## [0.18.1] - 2023-01-31 -**Fixed** -- (Linux) Fixed missing dependencies for deb and rpm packages -- (Linux) Use dynamic boost - -## [0.18.0] - 2023-01-29 -Attention, this release contains critical security fixes. Please update as soon as possible. Additionally, we are -encouraging users to change your Sunshine password, especially if you expose the web UI (i.e. port 47790 by default) -to the internet, or have ever uploaded your logs with verbose output to a public resource. - -**Added** -- (Windows) Add support for Intel QuickSync -- (Linux) Added aarch64 deb and rpm packages -- (Windows) Add support for hybrid graphics systems, such as laptops with both integrated and discrete GPUs -- (Linux) Add support for streaming from Steam Deck Gaming Mode -- (Windows) Add HDR support, see https://docs.lizardbyte.dev/projects/sunshine/en/latest/about/usage.html#hdr-support - -**Fixed** -- (Network) Refactor code for UPnP port forwarding -- (Video) Enforce 10 FPS encoding frame rate minimum to improve static image quality -- (Linux) deb and rpm packages are now specific to destination distro and version -- (Docs) Add nvidia/nvenc preset migration guide -- (Network) Performance optimizations -- (Video/Windows) Fix streaming to multiple clients from hardware encoder -- (Linux) Fix child process spawning -- (Security) Fix security vulnerability in implementation of SimpleWebServer -- (Misc) Rename "Steam BigPicture" to "Steam Big Picture" in default apps.json -- (Security) Scrub basic authorization header from logs -- (Linux) The systemd service will now restart in the event of a crash -- (Video/KMS/Linux) Fixed error: `couldn't import RGB Image: 00003002 and 00003004` -- (Video/Windows) Fix stream freezing triggered by the resolution changed -- (Installer/Windows) Fixes silent installation and other miscellaneous improvements -- (CPU) Significantly improved CPU usage - -## [0.17.0] - 2023-01-08 -If you are running Sunshine as a service on Windows, we are strongly urging you to update to v0.17.0 as soon as -possible. Older Windows versions of Sunshine had a security flaw in which the binary was located in a user-writable -location which is problematic when running as a service or on a multi-user system. Additionally, when running Sunshine -as a service, games and applications were launched as SYSTEM. This could lead to issues with save files and other game -settings. In v0.17.0, games now run under your user account without elevated privileges. - -**Breaking** -- (Apps) Removed automatic desktop entry (Re-add by adding an empty application named "Desktop" with no commands, "desktop.png" can be added as the image.) -- (Windows) Improved user upgrade experience (Suggest to manually uninstall existing Sunshine version before this upgrade. Do NOT select to remove everything, if prompted. Make a backup of config files before uninstall.) -- (Windows) Move config files to specific directory (files will be migrated automatically if using Windows installer) -- (Dependencies) Fix npm path (breaking change for package maintainers) - -**Added** -- (macOS) Added initial support for arm64 on macOS through Macports portfile -- (Input) Added support for foreign keyboard input -- (Misc) Logs inside the WebUI and log to file -- (UI/Windows) Added an Apply button to configuration page when running as a service -- (Input/Windows) Enable Mouse Keys while streaming for systems with no physical mouse - -**Fixed** -- (Video) Improved capture performance -- (Audio) Improved audio bitrate and quality handling -- (Apps/Windows) Fixed PATH environment variable handling -- (Apps/Windows) Use the proper environment variable for the Program Files (x86) folder -- (Service/Windows) Fix SunshineSvc hanging if an error occurs during startup -- (Service/Windows) Spawn Sunshine.exe in a job object, so it is terminated if SunshineSvc.exe dies -- (Video) windows/vram: fix fringing in NV12 colour conversion -- (Apps/Windows) Launch games under the correct user account -- (Video) nvenc, amdvce: rework all user presets/options -- (Network) Generate certificates with unique serial numbers -- (Service/Windows) Graceful termination on shutdown, logoff, and service stop -- (Apps/Windows) Fix launching apps when Sunshine is running as admin -- (Misc) Remove/fix calls to std::abort() -- (Misc) Remove prompt to press enter after Sunshine exits -- (Misc) Make log priority consistent for execution messages -- (Apps) Applications in Moonlight clients are now updated automatically after editing -- (Video/Linux) Fix wayland capture on nvidia -- (Audio) Fix 7.1 surround channel mapping -- (Video) Fix NVENC profile values not applying -- (Network) Fix origin_web_ui_allowed binding -- (Service/Windows) Self terminate/restart service if process hangs for 10 seconds -- (Input/Windows) Fix Windows masked cursor blending with GPU encoders -- (Video) Color conversion fixes and BT.2020 support - -**Dependencies** -- Bump ffmpeg from 4.4 to 5.1 -- ffmpeg_patches: add amfenc delay/buffering fix -- CBS moved to ffmpeg submodules -- Migrate to upstream Simple-Web-Server submodule -- Bump third-party/TPCircularBuffer from `bce9170` to `8833b3a` -- Bump third-party/moonlight-common-c from `8169a31` to `ef9ad52` -- Bump third-party/miniupnp from `6f848ae` to `207cf44` -- Bump third-party/ViGEmClient from `f719a1d` to `9e842ba` -- Bump bootstrap from 5.0.0 to 5.2.3 -- Bump @fortawesome/fontawesome-free from 6.2.0 to 6.2.1 - -## [0.16.0] - 2022-12-13 -**Added** -- Add cover finder -- (Docker) Add arm64 docker image -- (Flatpak) Add installation helper scripts -- (Windows) Add support for Unicode input messages - -**Fixed** -- (Linux) Reintroduce Ubuntu 20.04 and 22.04 specific deb packages -- (Linux) Fixed udev and systemd file locations - -**Dependencies** -- Bump babel from 2.10.3 to 2.11.0 -- Bump sphinx-copybutton from 0.5.0 to 0.5.1 -- Bump KSXGitHub/github-actions-deploy-aur from 2.5.0 to 2.6.0 -- Use npm for web dependencies (breaking change for third-party package maintainers) -- Update moonlight-common-c -- Use pre-built ffmpeg from LizardByte/build-deps for all sunshine builds (breaking change for third-party package maintainers) -- Bump furo from 2022.9.29 to 2022.12.7 - -**Misc** -- Misc org level workflow updates -- Fix misc typos in docs -- Fix winget release - -## [0.15.0] - 2022-10-30 -**Added** -- (Windows) Add firewall rules scripts -- (Windows) Automatically add and remove firewall rules at install/uninstall -- (Windows) Automatically add and remove service at install/uninstall -- (Docker) Official image added -- (Linux) Add aarch64 flatpak package - -**Changed** -- (Windows/Linux/MacOS) - Move default config and apps file to assets directory -- (MacOS) Bump boost to 1.80 for macport builds -- (Linux) Remove backup and restore of config files - -**Fixed** -- (Linux) - Create sunshine config directory if it doesn't exist -- (Linux) Remove portable home and config directories for AppImage -- (Windows) Include service install and uninstall scripts again -- (Windows) Automatically delete start menu entry upon uninstall -- (Windows) Automatically delete program install directory upon uninstall, with user prompt -- (Linux) Handle the case of no default audio sink -- (Windows/Linux/MacOS) Fix default image paths -- (Linux) Fix CUDA RGBA to NV12 conversion - -## [0.14.1] - 2022-08-09 -**Added** -- (Linux) Flatpak package added -- (Linux) AUR package automated updates -- (Windows) Winget package automated updates - -**Changed** -- (General) Moved repo to @LizardByte GitHub org -- (WebUI) Fixed button spacing on home page -- (WebUI) Added Discord WidgetBot Crate - -**Fixed** -- (Linux/Mac) Default config and app files now copied to user home directory -- (Windows) Default config and app files now copied to working directory - -## [0.14.0] - 2022-06-15 - -**Added** -- (Documentation) Added Sphinx documentation available at https://sunshinestream.readthedocs.io/en/latest/ -- (Development) Initial support for Localization -- (Linux) Add rpm package as release asset -- (macOS) Add Portfile as release asset -- (Windows) Add DwmFlush() call to improve capture -- (Windows) Add Windows installer - -**Fixed** -- (AMD) Fixed hwdevice being destroyed before context -- (Linux) Added missing dependencies to AppImage -- (Linux) Fixed rumble events causing game to freeze -- (Linux) Improved Pulse/Pipewire compatibility -- (Linux) Moved to single deb package -- (macOS) Fixed missing TPCircularBuffer submodule -- (Stream) Properly catch exceptions in stream broadcast handlers -- (Stream/Video) AVPacket fix - -## [0.13.0] - 2022-02-27 -**Added** -- (macOS) Initial support for macOS (#40) - -## [0.12.0] - 2022-02-13 -**Added** -- New command line argument `--version` -- Custom png poster support - -**Changed** -- Correct software bitrate calculation -- Increase vbv-bufsize to 1/10 of requested bitrate -- Improvements to Web UI - -## [0.11.1] - 2021-10-04 -**Changed** -- (Linux) Fix search path for config file and assets - -## [0.11.0] - 2021-10-04 -**Added** -- (Linux) Added support for wlroots based compositors on Wayland. -- (Windows) Added an icon for the executable - -**Changed** -- Fixed a bug causing segfault when connecting multiple controllers. -- (Linux) Improved NVENC, it now offloads converting images from RGB to NV12 -- (Linux) Fixed a bug causes stuttering - -## [0.10.1] - 2021-08-21 -**Changed** -- (Linux) Re-enabled KMS - -## [0.10.0] - 2021-08-20 -**Added** -- Added support for Rumble with gamepads. -- Added support for keyboard shortcuts <--- See the README for details. -- (Windows) A very basic script has been added in Sunshine-Windows\tools <-- This will start Sunshine at boot with the highest privileges which is needed to display the login prompt. - -**Changed** -- Some cosmetic changes to the WebUI. -- The first time the WebUI is opened, it will request the creation of a username/password pair from the user. -- Fixed audio crackling introduced in version 0.8.0 -- (Linux) VAAPI hardware encoding now works on Intel i7-6700 at least. <-- For the best experience, using ffmpeg version 4.3 or higher is recommended. -- (Windows) Installing from debian package shouldn't overwrite your configuration files anymore. <-- It's recommended that you back up `/etc/sunshine/` before testing this. - -## [0.9.0] - 2021-07-11 -**Added** -- Added audio encryption -- (Linux) Added basic NVENC support on Linux -- (Windows) The Windows version can now capture the lock screen and the UAC prompt as long as it's run through `PsExec.exe` https://docs.microsoft.com/en-us/sysinternals/downloads/psexec - -**Changed** -- Sunshine will now accept expired or not-yet-valid certificates, as long as they are signed properly. -- Fixed compatibility with iOS version of Moonlight -- Drastically reduced chance of being forced to skip error correction due to video frame size -- (Linux) sunshine.service will be installed automatically. - -## [0.8.0] - 2021-06-30 -**Added** -- Added mDNS support: Moonlight will automatically find Sunshine. -- Added UPnP support. It's off by default. - -## [0.7.7] - 2021-06-24 -**Added** -- (Linux) Added installation package for Debian - -**Changed** -- Fixed incorrect scaling for absolute mouse coordinates when using multiple monitors. -- Fixed incorrect colors when scaling for software encoder - -## [0.7.1] - 2021-06-18 -**Changed** -- (Linux) Fixed an issue where it was impossible to start sunshine on ubuntu 20.04 - -## [0.7.0] - 2021-06-16 -**Added** -- Added a Web Manager. Accessible through: https://localhost:47990 or https://:47990 -- (Linux) Added hardware encoding support for AMD on Linux - -**Changed** -- (Linux) Moved certificates and saved pairings generated during runtime to .config/sunshine on Linux - -## [0.6.0] - 2021-05-26 -**Added** -- Added support for surround audio - -**Changed** -- Maintain aspect ratio when scaling video -- Fix issue where Sunshine is forced to drop frames when they are too large - -## [0.5.0] - 2021-05-13 -**Added** -- Added support for absolute mouse coordinates -- (Linux) Added support for streaming specific monitor on Linux -- (Windows) Added support for AMF on Windows - -## [0.4.0] - 2020-05-03 -**Changed** -- prep-cmd is now optional in apps.json -- Fixed bug causing video artifacts -- Fixed bug preventing Moonlight from closing app on exit -- Fixed bug causing preventing keyboard keys from repeating on latest version of Moonlight -- Fixed bug causing segfault when another session of sunshine was already running -- Fixed bug causing crash when monitor has resolution 1366x768 - -## [0.3.1] - 2020-04-24 -**Changed** -- Fix a memory leak. - -## [0.3.0] - 2020-04-23 -**Changed** -- Hardware acceleration on NVidia GPU's for Video encoding on Windows - -## [0.2.0] - 2020-03-21 -**Changed** -- Multicasting is now supported: You can set the maximum simultaneous connections with the configurable option: channels -- Configuration variables can be overwritten on the command line: "name=value" --> it can be useful to set min_log_level=debug without modifying the configuration file -- Switches to make testing the pairing mechanism more convenient has been added, see "sunshine --help" for details - -## [0.1.1] - 2020-01-30 -**Added** -- (Linux) Added deb package and service for Linux - -## [0.1.0] - 2020-01-27 -**Added** -- The first official release for Sunshine! - -[0.1.0]: https://github.com/LizardByte/Sunshine/releases/tag/v0.1.0 -[0.1.1]: https://github.com/LizardByte/Sunshine/releases/tag/v0.1.1 -[0.2.0]: https://github.com/LizardByte/Sunshine/releases/tag/v0.2.0 -[0.3.0]: https://github.com/LizardByte/Sunshine/releases/tag/v0.3.0 -[0.3.1]: https://github.com/LizardByte/Sunshine/releases/tag/v0.3.1 -[0.4.0]: https://github.com/LizardByte/Sunshine/releases/tag/v0.4.0 -[0.5.0]: https://github.com/LizardByte/Sunshine/releases/tag/0.5.0 -[0.6.0]: https://github.com/LizardByte/Sunshine/releases/tag/v0.6.0 -[0.7.0]: https://github.com/LizardByte/Sunshine/releases/tag/v0.7.0 -[0.7.1]: https://github.com/LizardByte/Sunshine/releases/tag/v0.7.1 -[0.7.7]: https://github.com/LizardByte/Sunshine/releases/tag/v0.7.7 -[0.8.0]: https://github.com/LizardByte/Sunshine/releases/tag/v0.8.0 -[0.9.0]: https://github.com/LizardByte/Sunshine/releases/tag/v0.9.0 -[0.10.0]: https://github.com/LizardByte/Sunshine/releases/tag/v0.10.0 -[0.10.1]: https://github.com/LizardByte/Sunshine/releases/tag/v0.10.1 -[0.11.0]: https://github.com/LizardByte/Sunshine/releases/tag/v0.11.0 -[0.11.1]: https://github.com/LizardByte/Sunshine/releases/tag/v0.11.1 -[0.12.0]: https://github.com/LizardByte/Sunshine/releases/tag/v0.12.0 -[0.13.0]: https://github.com/LizardByte/Sunshine/releases/tag/v0.13.0 -[0.14.0]: https://github.com/LizardByte/Sunshine/releases/tag/v0.14.0 -[0.14.1]: https://github.com/LizardByte/Sunshine/releases/tag/v0.14.1 -[0.15.0]: https://github.com/LizardByte/Sunshine/releases/tag/v0.15.0 -[0.16.0]: https://github.com/LizardByte/Sunshine/releases/tag/v0.16.0 -[0.17.0]: https://github.com/LizardByte/Sunshine/releases/tag/v0.17.0 -[0.18.0]: https://github.com/LizardByte/Sunshine/releases/tag/v0.18.0 -[0.18.1]: https://github.com/LizardByte/Sunshine/releases/tag/v0.18.1 -[0.18.2]: https://github.com/LizardByte/Sunshine/releases/tag/v0.18.2 -[0.18.3]: https://github.com/LizardByte/Sunshine/releases/tag/v0.18.3 -[0.18.4]: https://github.com/LizardByte/Sunshine/releases/tag/v0.18.4 -[0.19.0]: https://github.com/LizardByte/Sunshine/releases/tag/v0.19.0 -[0.19.1]: https://github.com/LizardByte/Sunshine/releases/tag/v0.19.1 -[0.20.0]: https://github.com/LizardByte/Sunshine/releases/tag/v0.20.0 diff --git a/CMakeLists.txt b/CMakeLists.txt index ccca6fcec60..fabe5938b30 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,959 +1,58 @@ cmake_minimum_required(VERSION 3.18) # `CMAKE_CUDA_ARCHITECTURES` requires 3.18 +# set_source_files_properties requires 3.18 +# todo - set this conditionally -# todo - set version to 0.0.0 once confident in automated versioning -project(Sunshine VERSION 0.20.0 - DESCRIPTION "Sunshine is a self-hosted game stream host for Moonlight." - HOMEPAGE_URL "https://app.lizardbyte.dev") +project(Sunshine VERSION 0.0.0 + DESCRIPTION "Self-hosted game stream host for Moonlight" + HOMEPAGE_URL "https://app.lizardbyte.dev/Sunshine") + +set(PROJECT_LICENSE "GPL-3.0") set(PROJECT_LONG_DESCRIPTION "Offering low latency, cloud gaming server capabilities with support for AMD, Intel, \ and Nvidia GPUs for hardware encoding. Software encoding is also available. You can connect to Sunshine from any \ Moonlight client on a variety of devices. A web UI is provided to allow configuration, and client pairing, from \ your favorite web browser. Pair from the local server or any mobile device.") -# Check if env vars are defined before attempting to access them, variables will be defined even if blank -if((DEFINED ENV{BRANCH}) AND (DEFINED ENV{BUILD_VERSION}) AND (DEFINED ENV{COMMIT})) # cmake-lint: disable=W0106 - if(($ENV{BRANCH} STREQUAL "master") AND (NOT $ENV{BUILD_VERSION} STREQUAL "")) - # If BRANCH is "master" and BUILD_VERSION is not empty, then we are building a master branch - MESSAGE("Got from CI master branch and version $ENV{BUILD_VERSION}") - set(PROJECT_VERSION $ENV{BUILD_VERSION}) - elseif((DEFINED ENV{BRANCH}) AND (DEFINED ENV{COMMIT})) - # If BRANCH is set but not BUILD_VERSION we are building nightly, we gather only the commit hash - MESSAGE("Got from CI $ENV{BRANCH} branch and commit $ENV{COMMIT}") - set(PROJECT_VERSION ${PROJECT_VERSION}.$ENV{COMMIT}) - endif() -# Generate Sunshine Version based of the git tag -# https://github.com/nocnokneo/cmake-git-versioning-example/blob/master/LICENSE -else() - find_package(Git) - if(GIT_EXECUTABLE) - MESSAGE("${CMAKE_CURRENT_SOURCE_DIR}") - get_filename_component(SRC_DIR ${CMAKE_CURRENT_SOURCE_DIR} DIRECTORY) - #Get current Branch - execute_process( - COMMAND ${GIT_EXECUTABLE} rev-parse --abbrev-ref HEAD - #WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} - OUTPUT_VARIABLE GIT_DESCRIBE_BRANCH - RESULT_VARIABLE GIT_DESCRIBE_ERROR_CODE - OUTPUT_STRIP_TRAILING_WHITESPACE - ) - # Gather current commit - execute_process( - COMMAND ${GIT_EXECUTABLE} rev-parse --short HEAD - #WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} - OUTPUT_VARIABLE GIT_DESCRIBE_VERSION - RESULT_VARIABLE GIT_DESCRIBE_ERROR_CODE - OUTPUT_STRIP_TRAILING_WHITESPACE - ) - # Check if Dirty - execute_process( - COMMAND ${GIT_EXECUTABLE} diff --quiet --exit-code - #WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} - RESULT_VARIABLE GIT_IS_DIRTY - OUTPUT_STRIP_TRAILING_WHITESPACE - ) - if(NOT GIT_DESCRIBE_ERROR_CODE) - MESSAGE("Sunshine Branch: ${GIT_DESCRIBE_BRANCH}") - if(NOT GIT_DESCRIBE_BRANCH STREQUAL "master") - set(PROJECT_VERSION ${PROJECT_VERSION}.${GIT_DESCRIBE_VERSION}) - MESSAGE("Sunshine Version: ${GIT_DESCRIBE_VERSION}") - endif() - if(GIT_IS_DIRTY) - set(PROJECT_VERSION ${PROJECT_VERSION}.dirty) - MESSAGE("Git tree is dirty!") - endif() - else() - MESSAGE(ERROR ": Got git error while fetching tags: ${GIT_DESCRIBE_ERROR_CODE}") - endif() - else() - MESSAGE(WARNING ": Git not found, cannot find git version") - endif() -endif() - -option(SUNSHINE_CONFIGURE_APPIMAGE "Configuration specific for AppImage." OFF) -option(SUNSHINE_CONFIGURE_AUR "Configure files required for AUR." OFF) -option(SUNSHINE_CONFIGURE_FLATPAK_MAN "Configure manifest file required for Flatpak build." OFF) -option(SUNSHINE_CONFIGURE_FLATPAK "Configuration specific for Flatpak." OFF) -option(SUNSHINE_CONFIGURE_PORTFILE "Configure macOS Portfile." OFF) -option(SUNSHINE_CONFIGURE_ONLY "Configure special files only, then exit." OFF) - if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) message(STATUS "Setting build type to 'Release' as none was specified.") set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Choose the type of build." FORCE) endif() -if(${SUNSHINE_CONFIGURE_APPIMAGE}) - configure_file(packaging/linux/sunshine.desktop sunshine.desktop @ONLY) -elseif(${SUNSHINE_CONFIGURE_AUR}) - configure_file(packaging/linux/aur/PKGBUILD PKGBUILD @ONLY) -elseif(${SUNSHINE_CONFIGURE_FLATPAK_MAN}) - configure_file(packaging/linux/flatpak/dev.lizardbyte.sunshine.yml dev.lizardbyte.sunshine.yml @ONLY) -elseif(${SUNSHINE_CONFIGURE_PORTFILE}) - configure_file(packaging/macos/Portfile Portfile @ONLY) -endif() - -# return if configure only is set -if(${SUNSHINE_CONFIGURE_ONLY}) - return() -endif() - -set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake) -set(SUNSHINE_SOURCE_ASSETS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/src_assets") - -if(APPLE) - # ADD_FRAMEWORK: args = `fwname`, `appname` - macro(ADD_FRAMEWORK fwname appname) - find_library(FRAMEWORK_${fwname} - NAMES ${fwname} - PATHS ${CMAKE_OSX_SYSROOT}/System/Library - PATH_SUFFIXES Frameworks - NO_DEFAULT_PATH) - if( ${FRAMEWORK_${fwname}} STREQUAL FRAMEWORK_${fwname}-NOTFOUND) - MESSAGE(ERROR ": Framework ${fwname} not found") - else() - TARGET_LINK_LIBRARIES(${appname} "${FRAMEWORK_${fwname}}/${fwname}") - MESSAGE(STATUS "Framework ${fwname} found at ${FRAMEWORK_${fwname}}") - endif() - endmacro(ADD_FRAMEWORK) -endif() - -add_subdirectory(third-party/moonlight-common-c/enet) -add_subdirectory(third-party/Simple-Web-Server) - -set(UPNPC_BUILD_SHARED OFF CACHE BOOL "no shared libraries") -set(UPNPC_BUILD_TESTS OFF CACHE BOOL "Don't build tests for miniupnpc") -set(UPNPC_BUILD_SAMPLE OFF CACHE BOOL "Don't build samples for miniupnpc") -set(UPNPC_NO_INSTALL ON CACHE BOOL "Don't install any libraries build for miniupnpc") -add_subdirectory(third-party/miniupnp/miniupnpc) -include_directories(SYSTEM third-party/miniupnp/miniupnpc/include) - -find_package(Threads REQUIRED) -find_package(OpenSSL REQUIRED) -find_package(PkgConfig REQUIRED) -pkg_check_modules(CURL REQUIRED libcurl) - -if(WIN32) - set(Boost_USE_STATIC_LIBS ON) # cmake-lint: disable=C0103 -endif() - -find_package(Boost COMPONENTS locale log filesystem program_options REQUIRED) - -list(APPEND SUNSHINE_COMPILE_OPTIONS -Wall -Wno-sign-compare) - -# enable system tray, we will disable this later if we cannot find the required package config on linux -set(SUNSHINE_TRAY 1) - -if(WIN32) - enable_language(RC) - set(CMAKE_RC_COMPILER windres) - set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static") - - add_definitions(-DCURL_STATICLIB) - include_directories(SYSTEM ${CURL_STATIC_INCLUDE_DIRS}) - link_directories(${CURL_STATIC_LIBRARY_DIRS}) - - add_compile_definitions(SUNSHINE_PLATFORM="windows") - add_subdirectory(tools) # This is temporary, only tools for Windows are needed, for now - - include_directories(SYSTEM third-party/ViGEmClient/include) - - if(NOT DEFINED SUNSHINE_ICON_PATH) - set(SUNSHINE_ICON_PATH "${CMAKE_CURRENT_SOURCE_DIR}/sunshine.ico") - endif() - configure_file(src/platform/windows/windows.rs.in windows.rc @ONLY) - set(PLATFORM_TARGET_FILES - "${CMAKE_CURRENT_BINARY_DIR}/windows.rc" - src/platform/windows/publish.cpp - src/platform/windows/misc.h - src/platform/windows/misc.cpp - src/platform/windows/input.cpp - src/platform/windows/display.h - src/platform/windows/display_base.cpp - src/platform/windows/display_vram.cpp - src/platform/windows/display_ram.cpp - src/platform/windows/audio.cpp - third-party/tray/tray_windows.c - third-party/ViGEmClient/src/ViGEmClient.cpp - third-party/ViGEmClient/include/ViGEm/Client.h - third-party/ViGEmClient/include/ViGEm/Common.h - third-party/ViGEmClient/include/ViGEm/Util.h - third-party/ViGEmClient/include/ViGEm/km/BusShared.h) - - set(OPENSSL_LIBRARIES - libssl.a - libcrypto.a) - - list(PREPEND PLATFORM_LIBRARIES - libstdc++.a - libwinpthread.a - libssp.a - ksuser - wsock32 - ws2_32 - d3d11 dxgi D3DCompiler - setupapi - dwmapi - userenv - synchronization.lib - ${CURL_STATIC_LIBRARIES}) - - set_source_files_properties(third-party/ViGEmClient/src/ViGEmClient.cpp - PROPERTIES COMPILE_DEFINITIONS "UNICODE=1;ERROR_INVALID_DEVICE_OBJECT_PARAMETER=650") - set_source_files_properties(third-party/ViGEmClient/src/ViGEmClient.cpp - PROPERTIES COMPILE_FLAGS "-Wno-unknown-pragmas -Wno-misleading-indentation -Wno-class-memaccess") -elseif(APPLE) - add_compile_definitions(SUNSHINE_PLATFORM="macos") - - option(SUNSHINE_MACOS_PACKAGE "Should only be used when creating a MACOS package/dmg." OFF) - - link_directories(/opt/local/lib) - link_directories(/usr/local/lib) - ADD_DEFINITIONS(-DBOOST_LOG_DYN_LINK) - - FIND_LIBRARY(APP_SERVICES_LIBRARY ApplicationServices ) - FIND_LIBRARY(AV_FOUNDATION_LIBRARY AVFoundation ) - FIND_LIBRARY(COCOA Cocoa REQUIRED ) # tray icon - FIND_LIBRARY(CORE_MEDIA_LIBRARY CoreMedia ) - FIND_LIBRARY(CORE_VIDEO_LIBRARY CoreVideo ) - FIND_LIBRARY(VIDEO_TOOLBOX_LIBRARY VideoToolbox ) - FIND_LIBRARY(FOUNDATION_LIBRARY Foundation ) - list(APPEND SUNSHINE_EXTERNAL_LIBRARIES - ${APP_SERVICES_LIBRARY} - ${AV_FOUNDATION_LIBRARY} - ${COCOA} - ${CORE_MEDIA_LIBRARY} - ${CORE_VIDEO_LIBRARY} - ${VIDEO_TOOLBOX_LIBRARY} - ${FOUNDATION_LIBRARY}) - - set(PLATFORM_INCLUDE_DIRS - ${Boost_INCLUDE_DIR}) - - set(APPLE_PLIST_FILE ${SUNSHINE_SOURCE_ASSETS_DIR}/macos/assets/Info.plist) - - set(PLATFORM_TARGET_FILES - src/platform/macos/av_audio.h - src/platform/macos/av_audio.m - src/platform/macos/av_img_t.h - src/platform/macos/av_video.h - src/platform/macos/av_video.m - src/platform/macos/display.mm - src/platform/macos/input.cpp - src/platform/macos/microphone.mm - src/platform/macos/misc.mm - src/platform/macos/misc.h - src/platform/macos/nv12_zero_device.cpp - src/platform/macos/nv12_zero_device.h - src/platform/macos/publish.cpp - third-party/TPCircularBuffer/TPCircularBuffer.c - third-party/TPCircularBuffer/TPCircularBuffer.h - third-party/tray/tray_darwin.m - ${APPLE_PLIST_FILE}) -else() - add_compile_definitions(SUNSHINE_PLATFORM="linux") - - option(SUNSHINE_ENABLE_DRM "Enable KMS grab if available" ON) - option(SUNSHINE_ENABLE_X11 "Enable X11 grab if available" ON) - option(SUNSHINE_ENABLE_WAYLAND "Enable building wayland specific code" ON) - option(SUNSHINE_ENABLE_CUDA "Enable cuda specific code" ON) - option(SUNSHINE_ENABLE_TRAY "Enable tray icon" ON) - - if(${SUNSHINE_ENABLE_X11}) - find_package(X11) - else() - set(X11_FOUND OFF) - endif() - - set(CUDA_FOUND OFF) - if(${SUNSHINE_ENABLE_CUDA}) - include(CheckLanguage) - check_language(CUDA) - - if(CMAKE_CUDA_COMPILER) - set(CUDA_FOUND ON) - enable_language(CUDA) - - message(STATUS "CUDA Compiler Version: ${CMAKE_CUDA_COMPILER_VERSION}") - set(CMAKE_CUDA_ARCHITECTURES "") - - # https://tech.amikelive.com/node-930/cuda-compatibility-of-nvidia-display-gpu-drivers/ - if(CMAKE_CUDA_COMPILER_VERSION VERSION_LESS 6.5) - list(APPEND CMAKE_CUDA_ARCHITECTURES 10) - # set(CUDA_NVCC_FLAGS "${CUDA_NVCC_FLAGS} -gencode arch=compute_10,code=sm_10") - elseif(CMAKE_CUDA_COMPILER_VERSION VERSION_GREATER_EQUAL 6.5) - list(APPEND CMAKE_CUDA_ARCHITECTURES 50 52) - # set(CUDA_NVCC_FLAGS "${CUDA_NVCC_FLAGS} -gencode arch=compute_50,code=sm_50") - # set(CUDA_NVCC_FLAGS "${CUDA_NVCC_FLAGS} -gencode arch=compute_52,code=sm_52") - endif() - - if(CMAKE_CUDA_COMPILER_VERSION VERSION_LESS 7.0) - list(APPEND CMAKE_CUDA_ARCHITECTURES 11) - # set(CUDA_NVCC_FLAGS "${CUDA_NVCC_FLAGS} -gencode arch=compute_11,code=sm_11") - elseif(CMAKE_CUDA_COMPILER_VERSION VERSION_GREATER 7.6) - list(APPEND CMAKE_CUDA_ARCHITECTURES 60 61 62) - # set(CUDA_NVCC_FLAGS "${CUDA_NVCC_FLAGS} -gencode arch=compute_60,code=sm_60") - # set(CUDA_NVCC_FLAGS "${CUDA_NVCC_FLAGS} -gencode arch=compute_61,code=sm_61") - # set(CUDA_NVCC_FLAGS "${CUDA_NVCC_FLAGS} -gencode arch=compute_62,code=sm_62") - endif() - - if(CMAKE_CUDA_COMPILER_VERSION VERSION_LESS 9.0) - list(APPEND CMAKE_CUDA_ARCHITECTURES 20) - # set(CUDA_NVCC_FLAGS "${CUDA_NVCC_FLAGS} -gencode arch=compute_20,code=sm_20") - elseif(CMAKE_CUDA_COMPILER_VERSION VERSION_GREATER_EQUAL 9.0) - list(APPEND CMAKE_CUDA_ARCHITECTURES 70) - # set(CUDA_NVCC_FLAGS "${CUDA_NVCC_FLAGS} -gencode arch=compute_70,code=sm_70") - endif() - - if(CMAKE_CUDA_COMPILER_VERSION VERSION_GREATER_EQUAL 10.0) - list(APPEND CMAKE_CUDA_ARCHITECTURES 75) - # set(CUDA_NVCC_FLAGS "${CUDA_NVCC_FLAGS} -gencode arch=compute_75,code=sm_75") - endif() - - if(CMAKE_CUDA_COMPILER_VERSION VERSION_LESS 11.0) - list(APPEND CMAKE_CUDA_ARCHITECTURES 30) - # set(CUDA_NVCC_FLAGS "${CUDA_NVCC_FLAGS} -gencode arch=compute_30,code=sm_30") - elseif(CMAKE_CUDA_COMPILER_VERSION VERSION_GREATER_EQUAL 11.0) - list(APPEND CMAKE_CUDA_ARCHITECTURES 80) - # set(CUDA_NVCC_FLAGS "${CUDA_NVCC_FLAGS} -gencode arch=compute_80,code=sm_80") - endif() - - if(CMAKE_CUDA_COMPILER_VERSION VERSION_GREATER_EQUAL 11.1) - list(APPEND CMAKE_CUDA_ARCHITECTURES 86) - # set(CUDA_NVCC_FLAGS "${CUDA_NVCC_FLAGS} -gencode arch=compute_86,code=sm_86") - endif() - - if(CMAKE_CUDA_COMPILER_VERSION VERSION_GREATER_EQUAL 11.8) - list(APPEND CMAKE_CUDA_ARCHITECTURES 90) - # set(CUDA_NVCC_FLAGS "${CUDA_NVCC_FLAGS} -gencode arch=compute_90,code=sm_90") - endif() - - if(CMAKE_CUDA_COMPILER_VERSION VERSION_LESS 12.0) - list(APPEND CMAKE_CUDA_ARCHITECTURES 35) - # set(CUDA_NVCC_FLAGS "${CUDA_NVCC_FLAGS} -gencode arch=compute_35,code=sm_35") - endif() - - # message(STATUS "CUDA NVCC Flags: ${CUDA_NVCC_FLAGS}") - message(STATUS "CUDA Architectures: ${CMAKE_CUDA_ARCHITECTURES}") - endif() - endif() - if(${SUNSHINE_ENABLE_DRM}) - find_package(LIBDRM) - find_package(LIBCAP) - else() - set(LIBDRM_FOUND OFF) - set(LIBCAP_FOUND OFF) - endif() - if(${SUNSHINE_ENABLE_WAYLAND}) - find_package(Wayland) - else() - set(WAYLAND_FOUND OFF) - endif() - - if(X11_FOUND) - add_compile_definitions(SUNSHINE_BUILD_X11) - include_directories(SYSTEM ${X11_INCLUDE_DIR}) - list(APPEND PLATFORM_LIBRARIES ${X11_LIBRARIES}) - list(APPEND PLATFORM_TARGET_FILES src/platform/linux/x11grab.cpp) - endif() - - if(CUDA_FOUND) - include_directories(SYSTEM third-party/nvfbc) - list(APPEND PLATFORM_TARGET_FILES - src/platform/linux/cuda.cu - src/platform/linux/cuda.cpp - third-party/nvfbc/NvFBC.h) - - add_compile_definitions(SUNSHINE_BUILD_CUDA) - endif() - - if(LIBDRM_FOUND AND LIBCAP_FOUND) - add_compile_definitions(SUNSHINE_BUILD_DRM) - include_directories(SYSTEM ${LIBDRM_INCLUDE_DIRS} ${LIBCAP_INCLUDE_DIRS}) - list(APPEND PLATFORM_LIBRARIES ${LIBDRM_LIBRARIES} ${LIBCAP_LIBRARIES}) - list(APPEND PLATFORM_TARGET_FILES src/platform/linux/kmsgrab.cpp) - list(APPEND SUNSHINE_DEFINITIONS EGL_NO_X11=1) - elseif(LIBDRM_FOUND) - message(WARNING "Found libdrm, yet there is no libcap") - elseif(LIBDRM_FOUND) - message(WARNING "Found libcap, yet there is no libdrm") - endif() - - if(WAYLAND_FOUND) - add_compile_definitions(SUNSHINE_BUILD_WAYLAND) - # GEN_WAYLAND: args = `filename` - macro(GEN_WAYLAND filename) - file(MAKE_DIRECTORY ${CMAKE_BINARY_DIR}/generated-src) - - message("wayland-scanner private-code \ -${CMAKE_SOURCE_DIR}/third-party/wayland-protocols/${filename}.xml \ -${CMAKE_BINARY_DIR}/generated-src/${filename}.c") - message("wayland-scanner client-header \ -${CMAKE_SOURCE_DIR}/third-party/wayland-protocols/${filename}.xml \ -${CMAKE_BINARY_DIR}/generated-src/${filename}.h") - execute_process( - COMMAND wayland-scanner private-code - ${CMAKE_SOURCE_DIR}/third-party/wayland-protocols/${filename}.xml - ${CMAKE_BINARY_DIR}/generated-src/${filename}.c - COMMAND wayland-scanner client-header - ${CMAKE_SOURCE_DIR}/third-party/wayland-protocols/${filename}.xml - ${CMAKE_BINARY_DIR}/generated-src/${filename}.h - - RESULT_VARIABLE EXIT_INT - ) - - if(NOT ${EXIT_INT} EQUAL 0) - message(FATAL_ERROR "wayland-scanner failed") - endif() - - list(APPEND PLATFORM_TARGET_FILES - ${CMAKE_BINARY_DIR}/generated-src/${filename}.c - ${CMAKE_BINARY_DIR}/generated-src/${filename}.h) - endmacro() - - GEN_WAYLAND(xdg-output-unstable-v1) - GEN_WAYLAND(wlr-export-dmabuf-unstable-v1) - - include_directories( - SYSTEM - ${WAYLAND_INCLUDE_DIRS} - ${CMAKE_BINARY_DIR}/generated-src - ) - - list(APPEND PLATFORM_LIBRARIES ${WAYLAND_LIBRARIES}) - list(APPEND PLATFORM_TARGET_FILES - src/platform/linux/wlgrab.cpp - src/platform/linux/wayland.cpp) - endif() - if(NOT ${X11_FOUND} AND NOT (${LIBDRM_FOUND} AND ${LIBCAP_FOUND}) AND NOT ${WAYLAND_FOUND}) - message(FATAL_ERROR "Couldn't find either x11, wayland, cuda or (libdrm and libcap)") - endif() - - # tray icon - if(${SUNSHINE_ENABLE_TRAY}) - pkg_check_modules(APPINDICATOR appindicator3-0.1) - if(NOT APPINDICATOR_FOUND) - message(WARNING "Couldn't find appindicator, disabling tray icon") - set(SUNSHINE_TRAY 0) - else() - include_directories(SYSTEM ${APPINDICATOR_INCLUDE_DIRS}) - link_directories(${APPINDICATOR_LIBRARY_DIRS}) - - list(APPEND PLATFORM_TARGET_FILES third-party/tray/tray_linux.c) - list(APPEND SUNSHINE_EXTERNAL_LIBRARIES ${APPINDICATOR_LIBRARIES}) - endif() - else() - set(SUNSHINE_TRAY 0) - endif() - - list(APPEND PLATFORM_TARGET_FILES - src/platform/linux/publish.cpp - src/platform/linux/vaapi.h - src/platform/linux/vaapi.cpp - src/platform/linux/cuda.h - src/platform/linux/graphics.h - src/platform/linux/graphics.cpp - src/platform/linux/misc.h - src/platform/linux/misc.cpp - src/platform/linux/audio.cpp - src/platform/linux/input.cpp - src/platform/linux/x11grab.h - src/platform/linux/wayland.h - third-party/glad/src/egl.c - third-party/glad/src/gl.c - third-party/glad/include/EGL/eglplatform.h - third-party/glad/include/KHR/khrplatform.h - third-party/glad/include/glad/gl.h - third-party/glad/include/glad/egl.h) - - list(APPEND PLATFORM_LIBRARIES - Boost::dynamic_linking - dl - evdev - numa - pulse - pulse-simple) - - include_directories( - SYSTEM - /usr/include/libevdev-1.0 - third-party/nv-codec-headers/include - third-party/glad/include) - - if(NOT DEFINED SUNSHINE_EXECUTABLE_PATH) - set(SUNSHINE_EXECUTABLE_PATH "sunshine") - endif() - configure_file(sunshine.service.in sunshine.service @ONLY) -endif() - -configure_file(src/version.h.in version.h @ONLY) -include_directories(${CMAKE_CURRENT_BINARY_DIR}) - -set(SUNSHINE_TARGET_FILES - third-party/nanors/rs.c - third-party/nanors/rs.h - third-party/moonlight-common-c/src/Input.h - third-party/moonlight-common-c/src/Rtsp.h - third-party/moonlight-common-c/src/RtspParser.c - third-party/moonlight-common-c/src/Video.h - third-party/tray/tray.h - src/upnp.cpp - src/upnp.h - src/cbs.cpp - src/utility.h - src/uuid.h - src/config.h - src/config.cpp - src/main.cpp - src/main.h - src/crypto.cpp - src/crypto.h - src/nvhttp.cpp - src/nvhttp.h - src/httpcommon.cpp - src/httpcommon.h - src/confighttp.cpp - src/confighttp.h - src/rtsp.cpp - src/rtsp.h - src/stream.cpp - src/stream.h - src/video.cpp - src/video.h - src/input.cpp - src/input.h - src/audio.cpp - src/audio.h - src/platform/common.h - src/process.cpp - src/process.h - src/network.cpp - src/network.h - src/move_by_copy.h - src/system_tray.cpp - src/system_tray.h - src/task_pool.h - src/thread_pool.h - src/thread_safe.h - src/sync.h - src/round_robin.h - src/stat_trackers.h - src/stat_trackers.cpp - ${PLATFORM_TARGET_FILES}) - -set_source_files_properties(src/upnp.cpp PROPERTIES COMPILE_FLAGS -Wno-pedantic) - -set_source_files_properties(third-party/nanors/rs.c - PROPERTIES COMPILE_FLAGS "-include deps/obl/autoshim.h -ftree-vectorize") - -list(APPEND SUNSHINE_DEFINITIONS SUNSHINE_TRAY=${SUNSHINE_TRAY}) - -# Pre-compiled binaries -if(WIN32) - set(FFMPEG_PREPARED_BINARIES "${CMAKE_CURRENT_SOURCE_DIR}/third-party/ffmpeg-windows-x86_64") - set(FFMPEG_PLATFORM_LIBRARIES mfplat ole32 strmiids mfuuid mfx) -elseif(APPLE) - if (CMAKE_SYSTEM_PROCESSOR STREQUAL "arm64") - set(FFMPEG_PREPARED_BINARIES "${CMAKE_CURRENT_SOURCE_DIR}/third-party/ffmpeg-macos-aarch64") - else() - set(FFMPEG_PREPARED_BINARIES "${CMAKE_CURRENT_SOURCE_DIR}/third-party/ffmpeg-macos-x86_64") - endif() -else() - set(FFMPEG_PLATFORM_LIBRARIES va va-drm va-x11 vdpau X11) - if (CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64") - set(FFMPEG_PREPARED_BINARIES "${CMAKE_CURRENT_SOURCE_DIR}/third-party/ffmpeg-linux-aarch64") - else() - set(FFMPEG_PREPARED_BINARIES "${CMAKE_CURRENT_SOURCE_DIR}/third-party/ffmpeg-linux-x86_64") - list(APPEND FFMPEG_PLATFORM_LIBRARIES mfx) - set(CPACK_DEB_PLATFORM_PACKAGE_DEPENDS "libmfx1,") - set(CPACK_RPM_PLATFORM_PACKAGE_REQUIRES "intel-mediasdk >= 22.3.0,") - endif() -endif() -set(FFMPEG_INCLUDE_DIRS - ${FFMPEG_PREPARED_BINARIES}/include) -if(EXISTS ${FFMPEG_PREPARED_BINARIES}/lib/libhdr10plus.a) - set(HDR10_PLUS_LIBRARY - ${FFMPEG_PREPARED_BINARIES}/lib/libhdr10plus.a) -endif() -set(FFMPEG_LIBRARIES - ${FFMPEG_PREPARED_BINARIES}/lib/libavcodec.a - ${FFMPEG_PREPARED_BINARIES}/lib/libavutil.a - ${FFMPEG_PREPARED_BINARIES}/lib/libcbs.a - ${FFMPEG_PREPARED_BINARIES}/lib/libSvtAv1Enc.a - ${FFMPEG_PREPARED_BINARIES}/lib/libswscale.a - ${FFMPEG_PREPARED_BINARIES}/lib/libx264.a - ${FFMPEG_PREPARED_BINARIES}/lib/libx265.a - ${HDR10_PLUS_LIBRARY} - ${FFMPEG_PLATFORM_LIBRARIES}) - -include_directories(${CMAKE_CURRENT_SOURCE_DIR}) - -include_directories( - SYSTEM - ${CMAKE_CURRENT_SOURCE_DIR}/third-party - ${CMAKE_CURRENT_SOURCE_DIR}/third-party/moonlight-common-c/enet/include - ${CMAKE_CURRENT_SOURCE_DIR}/third-party/nanors - ${CMAKE_CURRENT_SOURCE_DIR}/third-party/nanors/deps/obl - ${FFMPEG_INCLUDE_DIRS} - ${PLATFORM_INCLUDE_DIRS} -) - -string(TOUPPER "x${CMAKE_BUILD_TYPE}" BUILD_TYPE) -if("${BUILD_TYPE}" STREQUAL "XDEBUG") - if(WIN32) - set_source_files_properties(src/nvhttp.cpp PROPERTIES COMPILE_FLAGS -O2) - endif() -else() - add_definitions(-DNDEBUG) -endif() - -# setup assets directory -if(NOT SUNSHINE_ASSETS_DIR) - set(SUNSHINE_ASSETS_DIR "assets") -endif() -if(UNIX) - set(SUNSHINE_ASSETS_DIR "${CMAKE_INSTALL_PREFIX}/${SUNSHINE_ASSETS_DIR}") -endif() - -# use relative assets path for AppImage... maybe for all unix -if(${SUNSHINE_CONFIGURE_APPIMAGE}) - string(REPLACE "${CMAKE_INSTALL_PREFIX}" ".${CMAKE_INSTALL_PREFIX}" SUNSHINE_ASSETS_DIR_DEF ${SUNSHINE_ASSETS_DIR}) -else() - set(SUNSHINE_ASSETS_DIR_DEF "${SUNSHINE_ASSETS_DIR}") -endif() -list(APPEND SUNSHINE_DEFINITIONS SUNSHINE_ASSETS_DIR="${SUNSHINE_ASSETS_DIR_DEF}") - -list(APPEND SUNSHINE_EXTERNAL_LIBRARIES - libminiupnpc-static - ${CMAKE_THREAD_LIBS_INIT} - enet - opus - ${FFMPEG_LIBRARIES} - ${Boost_LIBRARIES} - ${OPENSSL_LIBRARIES} - ${CURL_LIBRARIES} - ${PLATFORM_LIBRARIES}) - -if(NOT WIN32) - list(APPEND SUNSHINE_EXTERNAL_LIBRARIES Boost::log) -endif() - -add_executable(sunshine ${SUNSHINE_TARGET_FILES}) - -if(WIN32) - set_target_properties(sunshine PROPERTIES LINK_SEARCH_START_STATIC 1) - set(CMAKE_FIND_LIBRARY_SUFFIXES ".dll") - find_library(ZLIB ZLIB1) - list(APPEND SUNSHINE_EXTERNAL_LIBRARIES - Wtsapi32.lib) -endif() - -target_link_libraries(sunshine ${SUNSHINE_EXTERNAL_LIBRARIES} ${EXTRA_LIBS}) -target_compile_definitions(sunshine PUBLIC ${SUNSHINE_DEFINITIONS}) -set_target_properties(sunshine PROPERTIES CXX_STANDARD 17 - VERSION ${PROJECT_VERSION} - SOVERSION ${PROJECT_VERSION_MAJOR}) - -if(NOT DEFINED CMAKE_CUDA_STANDARD) - set(CMAKE_CUDA_STANDARD 17) - set(CMAKE_CUDA_STANDARD_REQUIRED ON) -endif() - -if(APPLE) - target_link_options(sunshine PRIVATE LINKER:-sectcreate,__TEXT,__info_plist,${APPLE_PLIST_FILE}) - # Tell linker to dynamically load these symbols at runtime, in case they're unavailable: - target_link_options(sunshine PRIVATE -Wl,-U,_CGPreflightScreenCaptureAccess -Wl,-U,_CGRequestScreenCaptureAccess) -endif() - -foreach(flag IN LISTS SUNSHINE_COMPILE_OPTIONS) - list(APPEND SUNSHINE_COMPILE_OPTIONS_CUDA "$<$:--compiler-options=${flag}>") -endforeach() - -target_compile_options(sunshine PRIVATE $<$:${SUNSHINE_COMPILE_OPTIONS}>;$<$:${SUNSHINE_COMPILE_OPTIONS_CUDA};-std=c++17>) # cmake-lint: disable=C0301 - -# CPACK / Packaging - -# Common options -set(CPACK_PACKAGE_NAME "Sunshine") -set(CPACK_PACKAGE_VENDOR "LizardByte") -set(CPACK_PACKAGE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/cpack_artifacts) -set(CPACK_PACKAGE_CONTACT "https://app.lizardbyte.dev") -set(CPACK_DEBIAN_PACKAGE_MAINTAINER "https://github.com/LizardByte") -set(CPACK_PACKAGE_DESCRIPTION ${CMAKE_PROJECT_DESCRIPTION}) -set(CPACK_PACKAGE_HOMEPAGE_URL ${CMAKE_PROJECT_HOMEPAGE_URL}) -set(CPACK_RESOURCE_FILE_LICENSE ${PROJECT_SOURCE_DIR}/LICENSE) -set(CPACK_PACKAGE_ICON ${PROJECT_SOURCE_DIR}/sunshine.png) -set(CPACK_PACKAGE_FILE_NAME "${CMAKE_PROJECT_NAME}") -set(CPACK_STRIP_FILES YES) - -# install npm modules -install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/node_modules" - DESTINATION "${SUNSHINE_ASSETS_DIR}/web") - -# Platform specific options -if(WIN32) # see options at: https://cmake.org/cmake/help/latest/cpack_gen/nsis.html - install(TARGETS sunshine RUNTIME DESTINATION "." COMPONENT application) - - # Hardening: include zlib1.dll (loaded via LoadLibrary() in openssl's libcrypto.a) - install(FILES "${ZLIB}" DESTINATION "." COMPONENT application) - - # Adding tools - install(TARGETS dxgi-info RUNTIME DESTINATION "tools" COMPONENT dxgi) - install(TARGETS audio-info RUNTIME DESTINATION "tools" COMPONENT audio) - - # Mandatory tools - install(TARGETS ddprobe RUNTIME DESTINATION "tools" COMPONENT application) - install(TARGETS sunshinesvc RUNTIME DESTINATION "tools" COMPONENT application) - - # Mandatory scripts - install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/windows/misc/service/" - DESTINATION "scripts" - COMPONENT assets) - install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/windows/misc/migration/" - DESTINATION "scripts" - COMPONENT assets) - - # Configurable options for the service - install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/windows/misc/autostart/" - DESTINATION "scripts" - COMPONENT autostart) - - # scripts - install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/windows/misc/firewall/" - DESTINATION "scripts" - COMPONENT firewall) - install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/windows/misc/vigembus/" - DESTINATION "scripts" - COMPONENT vigembus) - - # Sunshine assets - install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/common/assets/" - DESTINATION "${SUNSHINE_ASSETS_DIR}" - COMPONENT assets) - install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/windows/assets/" - DESTINATION "${SUNSHINE_ASSETS_DIR}" - COMPONENT assets) - - # set(CPACK_NSIS_MUI_HEADERIMAGE "") # TODO: image should be 150x57 bmp - set(CPACK_PACKAGE_ICON "${CMAKE_CURRENT_SOURCE_DIR}\\\\sunshine.ico") - set(CPACK_NSIS_INSTALLED_ICON_NAME "${PROJECT__DIR}\\\\${PROJECT_EXE}") - # The name of the directory that will be created in C:/Program files/ - set(CPACK_PACKAGE_INSTALL_DIRECTORY "${CPACK_PACKAGE_NAME}") - - # Extra install commands - # Restores permissions on the install directory - # Migrates config files from the root into the new config folder - # Install service - SET(CPACK_NSIS_EXTRA_INSTALL_COMMANDS - "${CPACK_NSIS_EXTRA_INSTALL_COMMANDS} - IfSilent +2 0 - ExecShell 'open' 'https://sunshinestream.readthedocs.io/' - nsExec::ExecToLog 'icacls \\\"$INSTDIR\\\" /reset' - nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\migrate-config.bat\\\"' - nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\add-firewall-rule.bat\\\"' - nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\install-service.bat\\\"' - nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\install-vigembus.bat\\\"' - nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\autostart-service.bat\\\"' - NoController: - ") +# set the module path, used for includes +set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake") - # Extra uninstall commands - # Uninstall service - set(CPACK_NSIS_EXTRA_UNINSTALL_COMMANDS - "${CPACK_NSIS_EXTRA_UNINSTALL_COMMANDS} - nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\delete-firewall-rule.bat\\\"' - nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\uninstall-service.bat\\\"' - MessageBox MB_YESNO|MB_ICONQUESTION \ - 'Do you want to remove ViGEmBus)?' \ - /SD IDNO IDNO NoVigem - nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\uninstall-vigembus.bat\\\"'; skipped if no - NoVigem: - MessageBox MB_YESNO|MB_ICONQUESTION \ - 'Do you want to remove $INSTDIR (this includes the configuration, cover images, and settings)?' \ - /SD IDNO IDNO NoDelete - RMDir /r \\\"$INSTDIR\\\"; skipped if no - NoDelete: - ") +# set version info for this build +include(${CMAKE_MODULE_PATH}/prep/build_version.cmake) - # Adding an option for the start menu - set(CPACK_NSIS_MODIFY_PATH "OFF") - set(CPACK_NSIS_EXECUTABLES_DIRECTORY ".") - # This will be shown on the installed apps Windows settings - set(CPACK_NSIS_INSTALLED_ICON_NAME "${CMAKE_PROJECT_NAME}.exe") - set(CPACK_NSIS_CREATE_ICONS_EXTRA - "${CPACK_NSIS_CREATE_ICONS_EXTRA} - CreateShortCut '\$SMPROGRAMS\\\\$STARTMENU_FOLDER\\\\${CMAKE_PROJECT_NAME}.lnk' \ - '\$INSTDIR\\\\${CMAKE_PROJECT_NAME}.exe' '--shortcut' - ") - set(CPACK_NSIS_DELETE_ICONS_EXTRA - "${CPACK_NSIS_DELETE_ICONS_EXTRA} - Delete '\$SMPROGRAMS\\\\$MUI_TEMP\\\\${CMAKE_PROJECT_NAME}.lnk' - ") +# cmake build flags +include(${CMAKE_MODULE_PATH}/prep/options.cmake) - # Checking for previous installed versions - set(CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL "ON") +# initial prep +include(${CMAKE_MODULE_PATH}/prep/init.cmake) - set(CPACK_NSIS_HELP_LINK "https://sunshinestream.readthedocs.io/about/installation.html") - set(CPACK_NSIS_URL_INFO_ABOUT "${CMAKE_PROJECT_HOMEPAGE_URL}") - set(CPACK_NSIS_CONTACT "${CMAKE_PROJECT_HOMEPAGE_URL}/support") +# configure special package files, such as sunshine.desktop, Flatpak manifest, Portfile , etc. +include(${CMAKE_MODULE_PATH}/prep/special_package_configuration.cmake) - set(CPACK_NSIS_MENU_LINKS - "https://sunshinestream.readthedocs.io" "Sunshine documentation" - "https://app.lizardbyte.dev" "LizardByte Web Site" - "https://app.lizardbyte.dev/support" "LizardByte Support") - set(CPACK_NSIS_MANIFEST_DPI_AWARE true) - - # Setting components groups and dependencies - set(CPACK_COMPONENT_GROUP_CORE_EXPANDED true) - - # sunshine binary - set(CPACK_COMPONENT_APPLICATION_DISPLAY_NAME "${CMAKE_PROJECT_NAME}") - set(CPACK_COMPONENT_APPLICATION_DESCRIPTION "${CMAKE_PROJECT_NAME} main application and required components.") - set(CPACK_COMPONENT_APPLICATION_GROUP "Core") - set(CPACK_COMPONENT_APPLICATION_REQUIRED true) - set(CPACK_COMPONENT_APPLICATION_DEPENDS assets) - - # service auto-start script - set(CPACK_COMPONENT_AUTOSTART_DISPLAY_NAME "Launch on Startup") - set(CPACK_COMPONENT_AUTOSTART_DESCRIPTION "If enabled, launches Sunshine automatically on system startup.") - set(CPACK_COMPONENT_AUTOSTART_GROUP "Core") - - # assets - set(CPACK_COMPONENT_ASSETS_DISPLAY_NAME "Required Assets") - set(CPACK_COMPONENT_ASSETS_DESCRIPTION "Shaders, default box art, and web UI.") - set(CPACK_COMPONENT_ASSETS_GROUP "Core") - set(CPACK_COMPONENT_ASSETS_REQUIRED true) - - # audio tool - set(CPACK_COMPONENT_AUDIO_DISPLAY_NAME "audio-info") - set(CPACK_COMPONENT_AUDIO_DESCRIPTION "CLI tool providing information about sound devices.") - set(CPACK_COMPONENT_AUDIO_GROUP "Tools") - - # display tool - set(CPACK_COMPONENT_DXGI_DISPLAY_NAME "dxgi-info") - set(CPACK_COMPONENT_DXGI_DESCRIPTION "CLI tool providing information about graphics cards and displays.") - set(CPACK_COMPONENT_DXGI_GROUP "Tools") - - # firewall scripts - set(CPACK_COMPONENT_FIREWALL_DISPLAY_NAME "Add Firewall Exclusions") - set(CPACK_COMPONENT_FIREWALL_DESCRIPTION "Scripts to enable or disable firewall rules.") - set(CPACK_COMPONENT_FIREWALL_GROUP "Scripts") - - # vigembus scripts - set(CPACK_COMPONENT_VIGEMBUS_DISPLAY_NAME "Virtual Gamepad Support") - set(CPACK_COMPONENT_VIGEMBUS_DESCRIPTION "Scripts to install and uninstall ViGEmBus for virtual gamepad support.") - set(CPACK_COMPONENT_VIGEMBUS_GROUP "Scripts") -endif() -if(APPLE) - # TODO: bundle doesn't produce a valid .app use cpack -G DragNDrop - set(CPACK_BUNDLE_NAME "${CMAKE_PROJECT_NAME}") - set(CPACK_BUNDLE_PLIST "${APPLE_PLIST_FILE}") - set(CPACK_BUNDLE_ICON "${PROJECT_SOURCE_DIR}/sunshine.icns") - # set(CPACK_BUNDLE_STARTUP_COMMAND "${INSTALL_RUNTIME_DIR}/sunshine") +# Exit early if END_BUILD is ON, i.e. when only generating package manifests +if(${END_BUILD}) + return() endif() -if(APPLE AND SUNSHINE_MACOS_PACKAGE) # TODO - set(MAC_PREFIX "${CMAKE_PROJECT_NAME}.app/Contents") - set(INSTALL_RUNTIME_DIR "${MAC_PREFIX}/MacOS") - - install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/common/assets/" - DESTINATION "${SUNSHINE_ASSETS_DIR}") - install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/macos/assets/" - DESTINATION "${SUNSHINE_ASSETS_DIR}") - - install(TARGETS sunshine - BUNDLE DESTINATION . COMPONENT Runtime - RUNTIME DESTINATION ${INSTALL_RUNTIME_DIR} COMPONENT Runtime) -elseif(UNIX) - # Installation destination dir - set(CPACK_SET_DESTDIR true) - if(NOT CMAKE_INSTALL_PREFIX) - set(CMAKE_INSTALL_PREFIX "/usr/share/sunshine") - endif() - install(TARGETS sunshine RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}") +# project constants +include(${CMAKE_MODULE_PATH}/prep/constants.cmake) - install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/common/assets/" - DESTINATION "${SUNSHINE_ASSETS_DIR}") +# load macros +include(${CMAKE_MODULE_PATH}/macros/common.cmake) - if(APPLE) - install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/macos/assets/" - DESTINATION "${SUNSHINE_ASSETS_DIR}") - install(FILES "${SUNSHINE_SOURCE_ASSETS_DIR}/macos/misc/uninstall_pkg.sh" - DESTINATION "${SUNSHINE_ASSETS_DIR}") - else() - install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/linux/assets/" - DESTINATION "${SUNSHINE_ASSETS_DIR}") - if(${SUNSHINE_CONFIGURE_APPIMAGE} OR ${SUNSHINE_CONFIGURE_FLATPAK}) - install(FILES "${SUNSHINE_SOURCE_ASSETS_DIR}/linux/misc/85-sunshine.rules" - DESTINATION "${SUNSHINE_ASSETS_DIR}/udev/rules.d") - install(FILES "${CMAKE_CURRENT_BINARY_DIR}/sunshine.service" - DESTINATION "${SUNSHINE_ASSETS_DIR}/systemd/user") - else() - install(FILES "${SUNSHINE_SOURCE_ASSETS_DIR}/linux/misc/85-sunshine.rules" - DESTINATION "${CMAKE_INSTALL_PREFIX}/lib/udev/rules.d") - install(FILES "${CMAKE_CURRENT_BINARY_DIR}/sunshine.service" - DESTINATION "${CMAKE_INSTALL_PREFIX}/lib/systemd/user") - endif() +# load dependencies +include(${CMAKE_MODULE_PATH}/dependencies/common.cmake) - # Post install - set(CPACK_DEBIAN_PACKAGE_CONTROL_EXTRA "${SUNSHINE_SOURCE_ASSETS_DIR}/linux/misc/postinst") - set(CPACK_RPM_POST_INSTALL_SCRIPT_FILE "${SUNSHINE_SOURCE_ASSETS_DIR}/linux/misc/postinst") +# setup compile definitions +include(${CMAKE_MODULE_PATH}/compile_definitions/common.cmake) - # Dependencies - set(CPACK_DEB_COMPONENT_INSTALL ON) - set(CPACK_DEBIAN_PACKAGE_DEPENDS "\ - ${CPACK_DEB_PLATFORM_PACKAGE_DEPENDS} \ - libboost-filesystem${Boost_VERSION}, \ - libboost-locale${Boost_VERSION}, \ - libboost-log${Boost_VERSION}, \ - libboost-program-options${Boost_VERSION}, \ - libboost-thread${Boost_VERSION}, \ - libcap2, \ - libcurl4, \ - libdrm2, \ - libevdev2, \ - libnuma1, \ - libopus0, \ - libpulse0, \ - libva2, \ - libva-drm2, \ - libvdpau1, \ - libwayland-client0, \ - libx11-6, \ - openssl | libssl3") - set(CPACK_RPM_PACKAGE_REQUIRES "\ - ${CPACK_RPM_PLATFORM_PACKAGE_REQUIRES} \ - boost-filesystem >= ${Boost_VERSION}, \ - boost-locale >= ${Boost_VERSION}, \ - boost-log >= ${Boost_VERSION}, \ - boost-program-options >= ${Boost_VERSION}, \ - boost-thread >= ${Boost_VERSION}, \ - libcap >= 2.22, \ - libcurl >= 7.0, \ - libdrm >= 2.4.97, \ - libevdev >= 1.5.6, \ - libopusenc >= 0.2.1, \ - libva >= 2.14.0, \ - libvdpau >= 1.5, \ - libwayland-client >= 1.20.0, \ - libX11 >= 1.7.3.1, \ - numactl-libs >= 2.0.14, \ - openssl >= 3.0.2, \ - pulseaudio-libs >= 10.0") - # This should automatically figure out dependencies, doesn't work with the current config - set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS OFF) - - if(${SUNSHINE_TRAY} STREQUAL 1) - install(FILES "${CMAKE_SOURCE_DIR}/sunshine.svg" - DESTINATION "${CMAKE_INSTALL_PREFIX}/share/icons") - - set(CPACK_DEBIAN_PACKAGE_DEPENDS "\ - ${CPACK_DEBIAN_PACKAGE_DEPENDS}, \ - libappindicator3-1") - set(CPACK_RPM_PACKAGE_REQUIRES "\ - ${CPACK_RPM_PACKAGE_REQUIRES}, \ - libappindicator-gtk3 >= 12.10.0") - endif() - endif() -endif() +# target definitions +include(${CMAKE_MODULE_PATH}/targets/common.cmake) -include(CPack) +# packaging +include(${CMAKE_MODULE_PATH}/packaging/common.cmake) diff --git a/DOCKER_README.md b/DOCKER_README.md index 7ac9f5c273b..f15fac2cc9e 100644 --- a/DOCKER_README.md +++ b/DOCKER_README.md @@ -19,7 +19,6 @@ ENTRYPOINT steam && sunshine ### SUNSHINE_VERSION - `latest`, `master`, `vX.X.X` -- `nightly` - commit hash ### SUNSHINE_OS @@ -52,8 +51,9 @@ Create and run the container (substitute your ``): ```bash docker run -d \ + --device /dev/dri/ \ --name= \ - --restart=unless-stopped + --restart=unless-stopped \ -e PUID= \ -e PGID= \ -e TZ= \ @@ -86,6 +86,25 @@ services: - "47998-48000:47998-48000/udp" ``` +### Using podman run +Create and run the container (substitute your ``): + +```bash +podman run -d \ + --device /dev/dri/ \ + --name= \ + --restart=unless-stopped \ + --userns=keep-id \ + -e PUID= \ + -e PGID= \ + -e TZ= \ + -v :/config \ + -p 47984-47990:47984-47990/tcp \ + -p 48010:48010 \ + -p 47998-48000:47998-48000/udp \ + +``` + ### Parameters You must substitute the `` with your own settings. @@ -132,8 +151,9 @@ The architectures supported by these images are shown in the table below. | tag suffix | amd64/x86_64 | arm64/aarch64 | |-----------------|--------------|---------------| | archlinux | ✅ | ❌ | +| debian-bookworm | ✅ | ✅ | | debian-bullseye | ✅ | ✅ | -| fedora-36 | ✅ | ✅ | -| fedora-37 | ✅ | ✅ | +| fedora-38 | ✅ | ✅ | +| fedora-39 | ✅ | ✅ | | ubuntu-20.04 | ✅ | ✅ | | ubuntu-22.04 | ✅ | ✅ | diff --git a/README.rst b/README.rst index 0a8d1eb8c91..a1d7271dd2d 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,6 @@ Overview ======== -LizardByte has the full documentation hosted on `Read the Docs `_. +LizardByte has the full documentation hosted on `Read the Docs `__. About ----- @@ -17,69 +17,48 @@ System Requirements **Minimum Requirements** -+------------+------------------------------------------------------------+ -| GPU | AMD: VCE 1.0 or higher, see `obs-amd hardware support`_ | -| +------------------------------------------------------------+ -| | Intel: VAAPI-compatible, see: `VAAPI hardware support`_ | -| +------------------------------------------------------------+ -| | Nvidia: NVENC enabled cards, see `nvenc support matrix`_ | -+------------+------------------------------------------------------------+ -| CPU | AMD: Ryzen 3 or higher | -| +------------------------------------------------------------+ -| | Intel: Core i3 or higher | -+------------+------------------------------------------------------------+ -| RAM | 4GB or more | -+------------+------------------------------------------------------------+ -| OS | Windows: 10+ (Windows Server not supported) | -| +------------------------------------------------------------+ -| | macOS: 11.7+ | -| +------------------------------------------------------------+ -| | Linux/Debian: 11 (bullseye) | -| +------------------------------------------------------------+ -| | Linux/Fedora: 36+ | -| +------------------------------------------------------------+ -| | Linux/Ubuntu: 20.04+ (focal) | -+------------+------------------------------------------------------------+ -| Network | Host: 5GHz, 802.11ac | -| +------------------------------------------------------------+ -| | Client: 5GHz, 802.11ac | -+------------+------------------------------------------------------------+ +.. csv-table:: + :widths: 15, 60 + + "GPU", "AMD: VCE 1.0 or higher, see: `obs-amd hardware support `_" + "", "Intel: VAAPI-compatible, see: `VAAPI hardware support `_" + "", "Nvidia: NVENC enabled cards, see: `nvenc support matrix `_" + "CPU", "AMD: Ryzen 3 or higher" + "", "Intel: Core i3 or higher" + "RAM", "4GB or more" + "OS", "Windows: 10+ (Windows Server does not support virtual gamepads)" + "", "macOS: 12+" + "", "Linux/Debian: 11 (bullseye)" + "", "Linux/Fedora: 39+" + "", "Linux/Ubuntu: 22.04+ (jammy)" + "Network", "Host: 5GHz, 802.11ac" + "", "Client: 5GHz, 802.11ac" **4k Suggestions** -+------------+------------------------------------------------------------+ -| GPU | AMD: Video Coding Engine 3.1 or higher | -| +------------------------------------------------------------+ -| | Intel: HD Graphics 510 or higher | -| +------------------------------------------------------------+ -| | Nvidia: GeForce GTX 1080 or higher | -+------------+------------------------------------------------------------+ -| CPU | AMD: Ryzen 5 or higher | -| +------------------------------------------------------------+ -| | Intel: Core i5 or higher | -+------------+------------------------------------------------------------+ -| Network | Host: CAT5e ethernet or better | -| +------------------------------------------------------------+ -| | Client: CAT5e ethernet or better | -+------------+------------------------------------------------------------+ +.. csv-table:: + :widths: 15, 60 + + "GPU", "AMD: Video Coding Engine 3.1 or higher" + "", "Intel: HD Graphics 510 or higher" + "", "Nvidia: GeForce GTX 1080 or higher" + "CPU", "AMD: Ryzen 5 or higher" + "", "Intel: Core i5 or higher" + "Network", "Host: CAT5e ethernet or better" + "", "Client: CAT5e ethernet or better" **HDR Suggestions** -+------------+------------------------------------------------------------+ -| GPU | AMD: Video Coding Engine 3.4 or higher | -| +------------------------------------------------------------+ -| | Intel: UHD Graphics 730 or higher | -| +------------------------------------------------------------+ -| | Nvidia: Pascal-based GPU (GTX 10-series) or higher | -+------------+------------------------------------------------------------+ -| CPU | AMD: todo | -| +------------------------------------------------------------+ -| | Intel: todo | -+------------+------------------------------------------------------------+ -| Network | Host: CAT5e ethernet or better | -| +------------------------------------------------------------+ -| | Client: CAT5e ethernet or better | -+------------+------------------------------------------------------------+ +.. csv-table:: + :widths: 15, 60 + + "GPU", "AMD: Video Coding Engine 3.4 or higher" + "", "Intel: UHD Graphics 730 or higher" + "", "Nvidia: Pascal-based GPU (GTX 10-series) or higher" + "CPU", "AMD: todo" + "", "Intel: todo" + "Network", "Host: CAT5e ethernet or better" + "", "Client: CAT5e ethernet or better" Integrations ------------ @@ -88,41 +67,41 @@ Integrations :alt: GitHub Workflow Status (CI) :target: https://github.com/LizardByte/Sunshine/actions/workflows/CI.yml?query=branch%3Amaster -.. image:: https://img.shields.io/github/actions/workflow/status/lizardbyte/sunshine/localize.yml.svg?branch=nightly&label=localize%20build&logo=github&style=for-the-badge +.. image:: https://img.shields.io/github/actions/workflow/status/lizardbyte/sunshine/localize.yml.svg?branch=master&label=localize%20build&logo=github&style=for-the-badge :alt: GitHub Workflow Status (localize) - :target: https://github.com/LizardByte/Sunshine/actions/workflows/localize.yml?query=branch%3Anightly + :target: https://github.com/LizardByte/Sunshine/actions/workflows/localize.yml?query=branch%3Amaster -.. image:: https://img.shields.io/readthedocs/sunshinestream?label=Docs&style=for-the-badge&logo=readthedocs +.. image:: https://img.shields.io/readthedocs/sunshinestream.svg?label=Docs&style=for-the-badge&logo=readthedocs :alt: Read the Docs :target: http://sunshinestream.readthedocs.io/ -.. image:: https://img.shields.io/badge/dynamic/json?color=blue&label=localized&style=for-the-badge&query=%24.progress..data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15178612-503956.json&logo=crowdin - :alt: CrowdIn - :target: https://crowdin.com/project/sunshinestream +.. image:: https://img.shields.io/codecov/c/gh/LizardByte/Sunshine?token=SMGXQ5NVMJ&style=for-the-badge&logo=codecov&label=codecov + :alt: Codecov + :target: https://codecov.io/gh/LizardByte/Sunshine Support ------- Our support methods are listed in our -`LizardByte Docs `_. +`LizardByte Docs `__. Downloads --------- -.. image:: https://img.shields.io/github/downloads/lizardbyte/sunshine/total?style=for-the-badge&logo=github +.. image:: https://img.shields.io/github/downloads/lizardbyte/sunshine/total.svg?style=for-the-badge&logo=github :alt: GitHub Releases :target: https://github.com/LizardByte/Sunshine/releases/latest -.. image:: https://img.shields.io/docker/pulls/lizardbyte/sunshine?style=for-the-badge&logo=docker +.. image:: https://img.shields.io/docker/pulls/lizardbyte/sunshine.svg?style=for-the-badge&logo=docker :alt: Docker :target: https://hub.docker.com/r/lizardbyte/sunshine +.. image:: https://img.shields.io/badge/dynamic/json.svg?color=orange&label=Winget&style=for-the-badge&prefix=v&query=pageProps.app.latestVersion&url=https%3A%2F%2Fwinstall.app%2F_next%2Fdata%2FixSYALJOWdJEOGpVihkFS%2Fapps%2FLizardByte.Sunshine.json&logo=microsoft + :alt: Winget Version + :target: https://github.com/microsoft/winget-pkgs/tree/master/manifests/l/LizardByte/Sunshine + Stats ------ -.. image:: https://img.shields.io/github/stars/lizardbyte/sunshine?logo=github&style=for-the-badge +.. image:: https://img.shields.io/github/stars/lizardbyte/sunshine.svg?logo=github&style=for-the-badge :alt: GitHub stars :target: https://github.com/LizardByte/Sunshine - -.. _nvenc support matrix: https://developer.nvidia.com/video-encode-and-decode-gpu-support-matrix-new -.. _obs-amd hardware support: https://github.com/obsproject/obs-amd-encoder/wiki/Hardware-Support -.. _VAAPI hardware support: https://www.intel.com/content/www/us/en/developer/articles/technical/linuxmedia-vaapi.html diff --git a/cmake/FindLibva.cmake b/cmake/FindLibva.cmake new file mode 100644 index 00000000000..f8dfddc1496 --- /dev/null +++ b/cmake/FindLibva.cmake @@ -0,0 +1,70 @@ +# - Try to find Libva +# This module defines the following variables: +# +# * LIBVA_FOUND - The component was found +# * LIBVA_INCLUDE_DIRS - The component include directory +# * LIBVA_LIBRARIES - The component library Libva +# * LIBVA_DRM_LIBRARIES - The component library Libva DRM + +# Use pkg-config to get the directories and then use these values in the +# find_path() and find_library() calls +# cmake-format: on + +find_package(PkgConfig QUIET) +if(PKG_CONFIG_FOUND) + pkg_check_modules(_LIBVA libva) + pkg_check_modules(_LIBVA_DRM libva-drm) +endif() + +find_path( + LIBVA_INCLUDE_DIR + NAMES va/va.h va/va_drm.h + HINTS ${_LIBVA_INCLUDE_DIRS} + PATHS /usr/include /usr/local/include /opt/local/include) + +find_library( + LIBVA_LIB + NAMES ${_LIBVA_LIBRARIES} libva + HINTS ${_LIBVA_LIBRARY_DIRS} + PATHS /usr/lib /usr/local/lib /opt/local/lib) + +find_library( + LIBVA_DRM_LIB + NAMES ${_LIBVA_DRM_LIBRARIES} libva-drm + HINTS ${_LIBVA_DRM_LIBRARY_DIRS} + PATHS /usr/lib /usr/local/lib /opt/local/lib) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Libva REQUIRED_VARS LIBVA_INCLUDE_DIR LIBVA_LIB LIBVA_DRM_LIB) +mark_as_advanced(LIBVA_INCLUDE_DIR LIBVA_LIB LIBVA_DRM_LIB) + +if(LIBVA_FOUND) + set(LIBVA_INCLUDE_DIRS ${LIBVA_INCLUDE_DIR}) + set(LIBVA_LIBRARIES ${LIBVA_LIB}) + set(LIBVA_DRM_LIBRARIES ${LIBVA_DRM_LIB}) + + if(NOT TARGET Libva::va) + if(IS_ABSOLUTE "${LIBVA_LIBRARIES}") + add_library(Libva::va UNKNOWN IMPORTED) + set_target_properties(Libva::va PROPERTIES IMPORTED_LOCATION "${LIBVA_LIBRARIES}") + else() + add_library(Libva::va INTERFACE IMPORTED) + set_target_properties(Libva::va PROPERTIES IMPORTED_LIBNAME "${LIBVA_LIBRARIES}") + endif() + + set_target_properties(Libva::va PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${LIBVA_INCLUDE_DIRS}") + endif() + + if(NOT TARGET Libva::drm) + if(IS_ABSOLUTE "${LIBVA_DRM_LIBRARIES}") + add_library(Libva::drm UNKNOWN IMPORTED) + set_target_properties(Libva::drm PROPERTIES IMPORTED_LOCATION "${LIBVA_DRM_LIBRARIES}") + else() + add_library(Libva::drm INTERFACE IMPORTED) + set_target_properties(Libva::drm PROPERTIES IMPORTED_LIBNAME "${LIBVA_DRM_LIBRARIES}") + endif() + + set_target_properties(Libva::drm PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${LIBVA_INCLUDE_DIRS}") + endif() + +endif() diff --git a/cmake/FindSystemd.cmake b/cmake/FindSystemd.cmake new file mode 100644 index 00000000000..c41ca64d248 --- /dev/null +++ b/cmake/FindSystemd.cmake @@ -0,0 +1,34 @@ +# - Try to find Systemd +# Once done this will define +# +# SYSTEMD_FOUND - system has systemd +# SYSTEMD_USER_UNIT_INSTALL_DIR - the systemd system unit install directory +# SYSTEMD_SYSTEM_UNIT_INSTALL_DIR - the systemd user unit install directory + +IF (NOT WIN32) + + find_package(PkgConfig QUIET) + if(PKG_CONFIG_FOUND) + pkg_check_modules(SYSTEMD "systemd") + endif() + + if (SYSTEMD_FOUND) + execute_process(COMMAND ${PKG_CONFIG_EXECUTABLE} + --variable=systemduserunitdir systemd + OUTPUT_VARIABLE SYSTEMD_USER_UNIT_INSTALL_DIR) + + string(REGEX REPLACE "[ \t\n]+" "" SYSTEMD_USER_UNIT_INSTALL_DIR + "${SYSTEMD_USER_UNIT_INSTALL_DIR}") + + execute_process(COMMAND ${PKG_CONFIG_EXECUTABLE} + --variable=systemdsystemunitdir systemd + OUTPUT_VARIABLE SYSTEMD_SYSTEM_UNIT_INSTALL_DIR) + + string(REGEX REPLACE "[ \t\n]+" "" SYSTEMD_SYSTEM_UNIT_INSTALL_DIR + "${SYSTEMD_SYSTEM_UNIT_INSTALL_DIR}") + + mark_as_advanced(SYSTEMD_USER_UNIT_INSTALL_DIR SYSTEMD_SYSTEM_UNIT_INSTALL_DIR) + + endif () + +ENDIF () diff --git a/cmake/FindUdev.cmake b/cmake/FindUdev.cmake new file mode 100644 index 00000000000..8343f791d35 --- /dev/null +++ b/cmake/FindUdev.cmake @@ -0,0 +1,28 @@ +# - Try to find Udev +# Once done this will define +# +# UDEV_FOUND - system has udev +# UDEV_RULES_INSTALL_DIR - the udev rules install directory + +IF (NOT WIN32) + + find_package(PkgConfig QUIET) + if(PKG_CONFIG_FOUND) + pkg_check_modules(UDEV "udev") + endif() + + if (UDEV_FOUND) + execute_process(COMMAND ${PKG_CONFIG_EXECUTABLE} + --variable=udevdir udev + OUTPUT_VARIABLE UDEV_RULES_INSTALL_DIR) + + string(REGEX REPLACE "[ \t\n]+" "" UDEV_RULES_INSTALL_DIR + "${UDEV_RULES_INSTALL_DIR}") + + set(UDEV_RULES_INSTALL_DIR "${UDEV_RULES_INSTALL_DIR}/rules.d") + + mark_as_advanced(UDEV_RULES_INSTALL_DIR) + + endif () + +ENDIF () diff --git a/cmake/compile_definitions/common.cmake b/cmake/compile_definitions/common.cmake new file mode 100644 index 00000000000..90a71802cde --- /dev/null +++ b/cmake/compile_definitions/common.cmake @@ -0,0 +1,140 @@ +# common compile definitions +# this file will also load platform specific definitions + +list(APPEND SUNSHINE_COMPILE_OPTIONS -Wall -Wno-sign-compare) +# Wall - enable all warnings +# Werror - treat warnings as errors +# Wno-maybe-uninitialized/Wno-uninitialized - disable warnings for maybe uninitialized variables +# Wno-sign-compare - disable warnings for signed/unsigned comparisons +if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + # GCC specific compile options + + # GCC 12 and higher will complain about maybe-uninitialized + if(CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 12) + list(APPEND SUNSHINE_COMPILE_OPTIONS -Wno-maybe-uninitialized) + endif() +elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + # Clang specific compile options + + # Clang doesn't actually complain about this this, so disabling for now + # list(APPEND SUNSHINE_COMPILE_OPTIONS -Wno-uninitialized) +endif() +if(BUILD_WERROR) + list(APPEND SUNSHINE_COMPILE_OPTIONS -Werror) +endif() + +# setup assets directory +if(NOT SUNSHINE_ASSETS_DIR) + set(SUNSHINE_ASSETS_DIR "assets") +endif() + +# platform specific compile definitions +if(WIN32) + include(${CMAKE_MODULE_PATH}/compile_definitions/windows.cmake) +elseif(UNIX) + include(${CMAKE_MODULE_PATH}/compile_definitions/unix.cmake) + + if(APPLE) + include(${CMAKE_MODULE_PATH}/compile_definitions/macos.cmake) + else() + include(${CMAKE_MODULE_PATH}/compile_definitions/linux.cmake) + endif() +endif() + +include_directories(SYSTEM "${CMAKE_SOURCE_DIR}/third-party/nv-codec-headers/include") +file(GLOB NVENC_SOURCES CONFIGURE_DEPENDS "src/nvenc/*.cpp" "src/nvenc/*.h") +list(APPEND PLATFORM_TARGET_FILES ${NVENC_SOURCES}) + +configure_file("${CMAKE_SOURCE_DIR}/src/version.h.in" version.h @ONLY) +include_directories("${CMAKE_CURRENT_BINARY_DIR}") # required for importing version.h + +set(SUNSHINE_TARGET_FILES + "${CMAKE_SOURCE_DIR}/third-party/nanors/rs.c" + "${CMAKE_SOURCE_DIR}/third-party/nanors/rs.h" + "${CMAKE_SOURCE_DIR}/third-party/moonlight-common-c/src/Input.h" + "${CMAKE_SOURCE_DIR}/third-party/moonlight-common-c/src/Rtsp.h" + "${CMAKE_SOURCE_DIR}/third-party/moonlight-common-c/src/RtspParser.c" + "${CMAKE_SOURCE_DIR}/third-party/moonlight-common-c/src/Video.h" + "${CMAKE_SOURCE_DIR}/third-party/tray/src/tray.h" + "${CMAKE_SOURCE_DIR}/src/upnp.cpp" + "${CMAKE_SOURCE_DIR}/src/upnp.h" + "${CMAKE_SOURCE_DIR}/src/cbs.cpp" + "${CMAKE_SOURCE_DIR}/src/utility.h" + "${CMAKE_SOURCE_DIR}/src/uuid.h" + "${CMAKE_SOURCE_DIR}/src/config.h" + "${CMAKE_SOURCE_DIR}/src/config.cpp" + "${CMAKE_SOURCE_DIR}/src/entry_handler.cpp" + "${CMAKE_SOURCE_DIR}/src/entry_handler.h" + "${CMAKE_SOURCE_DIR}/src/file_handler.cpp" + "${CMAKE_SOURCE_DIR}/src/file_handler.h" + "${CMAKE_SOURCE_DIR}/src/globals.cpp" + "${CMAKE_SOURCE_DIR}/src/globals.h" + "${CMAKE_SOURCE_DIR}/src/logging.cpp" + "${CMAKE_SOURCE_DIR}/src/logging.h" + "${CMAKE_SOURCE_DIR}/src/main.cpp" + "${CMAKE_SOURCE_DIR}/src/main.h" + "${CMAKE_SOURCE_DIR}/src/crypto.cpp" + "${CMAKE_SOURCE_DIR}/src/crypto.h" + "${CMAKE_SOURCE_DIR}/src/nvhttp.cpp" + "${CMAKE_SOURCE_DIR}/src/nvhttp.h" + "${CMAKE_SOURCE_DIR}/src/httpcommon.cpp" + "${CMAKE_SOURCE_DIR}/src/httpcommon.h" + "${CMAKE_SOURCE_DIR}/src/confighttp.cpp" + "${CMAKE_SOURCE_DIR}/src/confighttp.h" + "${CMAKE_SOURCE_DIR}/src/rtsp.cpp" + "${CMAKE_SOURCE_DIR}/src/rtsp.h" + "${CMAKE_SOURCE_DIR}/src/stream.cpp" + "${CMAKE_SOURCE_DIR}/src/stream.h" + "${CMAKE_SOURCE_DIR}/src/video.cpp" + "${CMAKE_SOURCE_DIR}/src/video.h" + "${CMAKE_SOURCE_DIR}/src/video_colorspace.cpp" + "${CMAKE_SOURCE_DIR}/src/video_colorspace.h" + "${CMAKE_SOURCE_DIR}/src/input.cpp" + "${CMAKE_SOURCE_DIR}/src/input.h" + "${CMAKE_SOURCE_DIR}/src/audio.cpp" + "${CMAKE_SOURCE_DIR}/src/audio.h" + "${CMAKE_SOURCE_DIR}/src/platform/common.h" + "${CMAKE_SOURCE_DIR}/src/process.cpp" + "${CMAKE_SOURCE_DIR}/src/process.h" + "${CMAKE_SOURCE_DIR}/src/network.cpp" + "${CMAKE_SOURCE_DIR}/src/network.h" + "${CMAKE_SOURCE_DIR}/src/move_by_copy.h" + "${CMAKE_SOURCE_DIR}/src/system_tray.cpp" + "${CMAKE_SOURCE_DIR}/src/system_tray.h" + "${CMAKE_SOURCE_DIR}/src/task_pool.h" + "${CMAKE_SOURCE_DIR}/src/thread_pool.h" + "${CMAKE_SOURCE_DIR}/src/thread_safe.h" + "${CMAKE_SOURCE_DIR}/src/sync.h" + "${CMAKE_SOURCE_DIR}/src/round_robin.h" + "${CMAKE_SOURCE_DIR}/src/stat_trackers.h" + "${CMAKE_SOURCE_DIR}/src/stat_trackers.cpp" + ${PLATFORM_TARGET_FILES}) + +if(NOT SUNSHINE_ASSETS_DIR_DEF) + set(SUNSHINE_ASSETS_DIR_DEF "${SUNSHINE_ASSETS_DIR}") +endif() +list(APPEND SUNSHINE_DEFINITIONS SUNSHINE_ASSETS_DIR="${SUNSHINE_ASSETS_DIR_DEF}") + +list(APPEND SUNSHINE_DEFINITIONS SUNSHINE_TRAY=${SUNSHINE_TRAY}) + +include_directories("${CMAKE_SOURCE_DIR}") + +include_directories( + SYSTEM + "${CMAKE_SOURCE_DIR}/third-party" + "${CMAKE_SOURCE_DIR}/third-party/moonlight-common-c/enet/include" + "${CMAKE_SOURCE_DIR}/third-party/nanors" + "${CMAKE_SOURCE_DIR}/third-party/nanors/deps/obl" + ${FFMPEG_INCLUDE_DIRS} + ${PLATFORM_INCLUDE_DIRS} +) + +list(APPEND SUNSHINE_EXTERNAL_LIBRARIES + ${MINIUPNP_LIBRARIES} + ${CMAKE_THREAD_LIBS_INIT} + enet + opus + ${FFMPEG_LIBRARIES} + ${Boost_LIBRARIES} + ${OPENSSL_LIBRARIES} + ${PLATFORM_LIBRARIES}) diff --git a/cmake/compile_definitions/linux.cmake b/cmake/compile_definitions/linux.cmake new file mode 100644 index 00000000000..b323eb82e84 --- /dev/null +++ b/cmake/compile_definitions/linux.cmake @@ -0,0 +1,257 @@ +# linux specific compile definitions + +add_compile_definitions(SUNSHINE_PLATFORM="linux") + +# AppImage +if(${SUNSHINE_BUILD_APPIMAGE}) + # use relative assets path for AppImage + string(REPLACE "${CMAKE_INSTALL_PREFIX}" ".${CMAKE_INSTALL_PREFIX}" SUNSHINE_ASSETS_DIR_DEF ${SUNSHINE_ASSETS_DIR}) +endif() + +# cuda +set(CUDA_FOUND OFF) +if(${SUNSHINE_ENABLE_CUDA}) + include(CheckLanguage) + check_language(CUDA) + + if(CMAKE_CUDA_COMPILER) + set(CUDA_FOUND ON) + enable_language(CUDA) + + message(STATUS "CUDA Compiler Version: ${CMAKE_CUDA_COMPILER_VERSION}") + set(CMAKE_CUDA_ARCHITECTURES "") + + # https://tech.amikelive.com/node-930/cuda-compatibility-of-nvidia-display-gpu-drivers/ + if(CMAKE_CUDA_COMPILER_VERSION VERSION_LESS 6.5) + list(APPEND CMAKE_CUDA_ARCHITECTURES 10) + # set(CUDA_NVCC_FLAGS "${CUDA_NVCC_FLAGS} -gencode arch=compute_10,code=sm_10") + elseif(CMAKE_CUDA_COMPILER_VERSION VERSION_GREATER_EQUAL 6.5) + list(APPEND CMAKE_CUDA_ARCHITECTURES 50 52) + # set(CUDA_NVCC_FLAGS "${CUDA_NVCC_FLAGS} -gencode arch=compute_50,code=sm_50") + # set(CUDA_NVCC_FLAGS "${CUDA_NVCC_FLAGS} -gencode arch=compute_52,code=sm_52") + endif() + + if(CMAKE_CUDA_COMPILER_VERSION VERSION_LESS 7.0) + list(APPEND CMAKE_CUDA_ARCHITECTURES 11) + # set(CUDA_NVCC_FLAGS "${CUDA_NVCC_FLAGS} -gencode arch=compute_11,code=sm_11") + elseif(CMAKE_CUDA_COMPILER_VERSION VERSION_GREATER 7.6) + list(APPEND CMAKE_CUDA_ARCHITECTURES 60 61 62) + # set(CUDA_NVCC_FLAGS "${CUDA_NVCC_FLAGS} -gencode arch=compute_60,code=sm_60") + # set(CUDA_NVCC_FLAGS "${CUDA_NVCC_FLAGS} -gencode arch=compute_61,code=sm_61") + # set(CUDA_NVCC_FLAGS "${CUDA_NVCC_FLAGS} -gencode arch=compute_62,code=sm_62") + endif() + + if(CMAKE_CUDA_COMPILER_VERSION VERSION_LESS 9.0) + list(APPEND CMAKE_CUDA_ARCHITECTURES 20) + # set(CUDA_NVCC_FLAGS "${CUDA_NVCC_FLAGS} -gencode arch=compute_20,code=sm_20") + elseif(CMAKE_CUDA_COMPILER_VERSION VERSION_GREATER_EQUAL 9.0) + list(APPEND CMAKE_CUDA_ARCHITECTURES 70) + # set(CUDA_NVCC_FLAGS "${CUDA_NVCC_FLAGS} -gencode arch=compute_70,code=sm_70") + endif() + + if(CMAKE_CUDA_COMPILER_VERSION VERSION_GREATER_EQUAL 10.0) + list(APPEND CMAKE_CUDA_ARCHITECTURES 75) + # set(CUDA_NVCC_FLAGS "${CUDA_NVCC_FLAGS} -gencode arch=compute_75,code=sm_75") + endif() + + if(CMAKE_CUDA_COMPILER_VERSION VERSION_LESS 11.0) + list(APPEND CMAKE_CUDA_ARCHITECTURES 30) + # set(CUDA_NVCC_FLAGS "${CUDA_NVCC_FLAGS} -gencode arch=compute_30,code=sm_30") + elseif(CMAKE_CUDA_COMPILER_VERSION VERSION_GREATER_EQUAL 11.0) + list(APPEND CMAKE_CUDA_ARCHITECTURES 80) + # set(CUDA_NVCC_FLAGS "${CUDA_NVCC_FLAGS} -gencode arch=compute_80,code=sm_80") + endif() + + if(CMAKE_CUDA_COMPILER_VERSION VERSION_GREATER_EQUAL 11.1) + list(APPEND CMAKE_CUDA_ARCHITECTURES 86) + # set(CUDA_NVCC_FLAGS "${CUDA_NVCC_FLAGS} -gencode arch=compute_86,code=sm_86") + endif() + + if(CMAKE_CUDA_COMPILER_VERSION VERSION_GREATER_EQUAL 11.8) + list(APPEND CMAKE_CUDA_ARCHITECTURES 90) + # set(CUDA_NVCC_FLAGS "${CUDA_NVCC_FLAGS} -gencode arch=compute_90,code=sm_90") + endif() + + if(CMAKE_CUDA_COMPILER_VERSION VERSION_LESS 12.0) + list(APPEND CMAKE_CUDA_ARCHITECTURES 35) + # set(CUDA_NVCC_FLAGS "${CUDA_NVCC_FLAGS} -gencode arch=compute_35,code=sm_35") + endif() + + # sort the architectures + list(SORT CMAKE_CUDA_ARCHITECTURES COMPARE NATURAL) + + # message(STATUS "CUDA NVCC Flags: ${CUDA_NVCC_FLAGS}") + message(STATUS "CUDA Architectures: ${CMAKE_CUDA_ARCHITECTURES}") + endif() +endif() +if(CUDA_FOUND) + include_directories(SYSTEM "${CMAKE_SOURCE_DIR}/third-party/nvfbc") + list(APPEND PLATFORM_TARGET_FILES + "${CMAKE_SOURCE_DIR}/src/platform/linux/cuda.h" + "${CMAKE_SOURCE_DIR}/src/platform/linux/cuda.cu" + "${CMAKE_SOURCE_DIR}/src/platform/linux/cuda.cpp" + "${CMAKE_SOURCE_DIR}/third-party/nvfbc/NvFBC.h") + + add_compile_definitions(SUNSHINE_BUILD_CUDA) +endif() + +# drm +if(${SUNSHINE_ENABLE_DRM}) + find_package(LIBDRM) + find_package(LIBCAP) +else() + set(LIBDRM_FOUND OFF) + set(LIBCAP_FOUND OFF) +endif() +if(LIBDRM_FOUND AND LIBCAP_FOUND) + add_compile_definitions(SUNSHINE_BUILD_DRM) + include_directories(SYSTEM ${LIBDRM_INCLUDE_DIRS} ${LIBCAP_INCLUDE_DIRS}) + list(APPEND PLATFORM_LIBRARIES ${LIBDRM_LIBRARIES} ${LIBCAP_LIBRARIES}) + list(APPEND PLATFORM_TARGET_FILES + "${CMAKE_SOURCE_DIR}/src/platform/linux/kmsgrab.cpp") + list(APPEND SUNSHINE_DEFINITIONS EGL_NO_X11=1) +elseif(NOT LIBDRM_FOUND) + message(WARNING "Missing libdrm") +elseif(NOT LIBDRM_FOUND) + message(WARNING "Missing libcap") +endif() + +# evdev +pkg_check_modules(PC_EVDEV libevdev REQUIRED) +find_path(EVDEV_INCLUDE_DIR libevdev/libevdev.h + HINTS ${PC_EVDEV_INCLUDE_DIRS} ${PC_EVDEV_INCLUDEDIR}) +find_library(EVDEV_LIBRARY + NAMES evdev libevdev) +if(EVDEV_INCLUDE_DIR AND EVDEV_LIBRARY) + include_directories(SYSTEM ${EVDEV_INCLUDE_DIR}) + list(APPEND PLATFORM_LIBRARIES ${EVDEV_LIBRARY}) +endif() + +# vaapi +if(${SUNSHINE_ENABLE_VAAPI}) + find_package(Libva) +else() + set(LIBVA_FOUND OFF) +endif() +if(LIBVA_FOUND) + add_compile_definitions(SUNSHINE_BUILD_VAAPI) + include_directories(SYSTEM ${LIBVA_INCLUDE_DIR}) + list(APPEND PLATFORM_LIBRARIES ${LIBVA_LIBRARIES} ${LIBVA_DRM_LIBRARIES}) + list(APPEND PLATFORM_TARGET_FILES + "${CMAKE_SOURCE_DIR}/src/platform/linux/vaapi.h" + "${CMAKE_SOURCE_DIR}/src/platform/linux/vaapi.cpp") +endif() + +# wayland +if(${SUNSHINE_ENABLE_WAYLAND}) + find_package(Wayland) +else() + set(WAYLAND_FOUND OFF) +endif() +if(WAYLAND_FOUND) + add_compile_definitions(SUNSHINE_BUILD_WAYLAND) + + if(NOT SUNSHINE_SYSTEM_WAYLAND_PROTOCOLS) + set(WAYLAND_PROTOCOLS_DIR "${CMAKE_SOURCE_DIR}/third-party/wayland-protocols") + else() + pkg_get_variable(WAYLAND_PROTOCOLS_DIR wayland-protocols pkgdatadir) + pkg_check_modules(WAYLAND_PROTOCOLS wayland-protocols REQUIRED) + endif() + + GEN_WAYLAND("${WAYLAND_PROTOCOLS_DIR}" "unstable/xdg-output" xdg-output-unstable-v1) + GEN_WAYLAND("${CMAKE_SOURCE_DIR}/third-party/wlr-protocols" "unstable" wlr-export-dmabuf-unstable-v1) + + include_directories( + SYSTEM + ${WAYLAND_INCLUDE_DIRS} + ${CMAKE_BINARY_DIR}/generated-src + ) + + list(APPEND PLATFORM_LIBRARIES ${WAYLAND_LIBRARIES}) + list(APPEND PLATFORM_TARGET_FILES + "${CMAKE_SOURCE_DIR}/src/platform/linux/wlgrab.cpp" + "${CMAKE_SOURCE_DIR}/src/platform/linux/wayland.h" + "${CMAKE_SOURCE_DIR}/src/platform/linux/wayland.cpp") +endif() + +# x11 +if(${SUNSHINE_ENABLE_X11}) + find_package(X11) +else() + set(X11_FOUND OFF) +endif() +if(X11_FOUND) + add_compile_definitions(SUNSHINE_BUILD_X11) + include_directories(SYSTEM ${X11_INCLUDE_DIR}) + list(APPEND PLATFORM_LIBRARIES ${X11_LIBRARIES}) + list(APPEND PLATFORM_TARGET_FILES + "${CMAKE_SOURCE_DIR}/src/platform/linux/x11grab.h" + "${CMAKE_SOURCE_DIR}/src/platform/linux/x11grab.cpp") +endif() + +if(NOT ${CUDA_FOUND} + AND NOT ${WAYLAND_FOUND} + AND NOT ${X11_FOUND} + AND NOT (${LIBDRM_FOUND} AND ${LIBCAP_FOUND}) + AND NOT ${LIBVA_FOUND}) + message(FATAL_ERROR "Couldn't find either cuda, wayland, x11, (libdrm and libcap), or libva") +endif() + +# tray icon +if(${SUNSHINE_ENABLE_TRAY}) + pkg_check_modules(APPINDICATOR ayatana-appindicator3-0.1) + if(APPINDICATOR_FOUND) + list(APPEND SUNSHINE_DEFINITIONS TRAY_AYATANA_APPINDICATOR=1) + else() + pkg_check_modules(APPINDICATOR appindicator3-0.1) + if(APPINDICATOR_FOUND) + list(APPEND SUNSHINE_DEFINITIONS TRAY_LEGACY_APPINDICATOR=1) + endif () + endif() + pkg_check_modules(LIBNOTIFY libnotify) + if(NOT APPINDICATOR_FOUND OR NOT LIBNOTIFY_FOUND) + set(SUNSHINE_TRAY 0) + message(WARNING "Missing appindicator or libnotify, disabling tray icon") + message(STATUS "APPINDICATOR_FOUND: ${APPINDICATOR_FOUND}") + message(STATUS "LIBNOTIFY_FOUND: ${LIBNOTIFY_FOUND}") + else() + include_directories(SYSTEM ${APPINDICATOR_INCLUDE_DIRS} ${LIBNOTIFY_INCLUDE_DIRS}) + link_directories(${APPINDICATOR_LIBRARY_DIRS} ${LIBNOTIFY_LIBRARY_DIRS}) + + list(APPEND PLATFORM_TARGET_FILES "${CMAKE_SOURCE_DIR}/third-party/tray/src/tray_linux.c") + list(APPEND SUNSHINE_EXTERNAL_LIBRARIES ${APPINDICATOR_LIBRARIES} ${LIBNOTIFY_LIBRARIES}) + endif() +else() + set(SUNSHINE_TRAY 0) + message(STATUS "Tray icon disabled") +endif() + +if (${SUNSHINE_TRAY} EQUAL 0 AND SUNSHINE_REQUIRE_TRAY) + message(FATAL_ERROR "Tray icon is required") +endif() + +list(APPEND PLATFORM_TARGET_FILES + "${CMAKE_SOURCE_DIR}/src/platform/linux/publish.cpp" + "${CMAKE_SOURCE_DIR}/src/platform/linux/graphics.h" + "${CMAKE_SOURCE_DIR}/src/platform/linux/graphics.cpp" + "${CMAKE_SOURCE_DIR}/src/platform/linux/misc.h" + "${CMAKE_SOURCE_DIR}/src/platform/linux/misc.cpp" + "${CMAKE_SOURCE_DIR}/src/platform/linux/audio.cpp" + "${CMAKE_SOURCE_DIR}/src/platform/linux/input.cpp" + "${CMAKE_SOURCE_DIR}/third-party/glad/src/egl.c" + "${CMAKE_SOURCE_DIR}/third-party/glad/src/gl.c" + "${CMAKE_SOURCE_DIR}/third-party/glad/include/EGL/eglplatform.h" + "${CMAKE_SOURCE_DIR}/third-party/glad/include/KHR/khrplatform.h" + "${CMAKE_SOURCE_DIR}/third-party/glad/include/glad/gl.h" + "${CMAKE_SOURCE_DIR}/third-party/glad/include/glad/egl.h") + +list(APPEND PLATFORM_LIBRARIES + Boost::dynamic_linking + dl + pulse + pulse-simple) + +include_directories( + SYSTEM + "${CMAKE_SOURCE_DIR}/third-party/nv-codec-headers/include" + "${CMAKE_SOURCE_DIR}/third-party/glad/include") diff --git a/cmake/compile_definitions/macos.cmake b/cmake/compile_definitions/macos.cmake new file mode 100644 index 00000000000..ab30a641b3b --- /dev/null +++ b/cmake/compile_definitions/macos.cmake @@ -0,0 +1,58 @@ +# macos specific compile definitions + +add_compile_definitions(SUNSHINE_PLATFORM="macos") + +set(MACOS_LINK_DIRECTORIES + /opt/homebrew/lib + /opt/local/lib + /usr/local/lib) + +foreach(dir ${MACOS_LINK_DIRECTORIES}) + if(EXISTS ${dir}) + link_directories(${dir}) + endif() +endforeach() + +ADD_DEFINITIONS(-DBOOST_LOG_DYN_LINK) + +list(APPEND SUNSHINE_EXTERNAL_LIBRARIES + ${APP_KIT_LIBRARY} + ${APP_SERVICES_LIBRARY} + ${AV_FOUNDATION_LIBRARY} + ${CORE_MEDIA_LIBRARY} + ${CORE_VIDEO_LIBRARY} + ${FOUNDATION_LIBRARY} + ${VIDEO_TOOLBOX_LIBRARY}) + +set(PLATFORM_INCLUDE_DIRS + ${Boost_INCLUDE_DIR}) + +set(APPLE_PLIST_FILE "${SUNSHINE_SOURCE_ASSETS_DIR}/macos/assets/Info.plist") + +# todo - tray is not working on macos +set(SUNSHINE_TRAY 0) + +set(PLATFORM_TARGET_FILES + "${CMAKE_SOURCE_DIR}/src/platform/macos/av_audio.h" + "${CMAKE_SOURCE_DIR}/src/platform/macos/av_audio.m" + "${CMAKE_SOURCE_DIR}/src/platform/macos/av_img_t.h" + "${CMAKE_SOURCE_DIR}/src/platform/macos/av_video.h" + "${CMAKE_SOURCE_DIR}/src/platform/macos/av_video.m" + "${CMAKE_SOURCE_DIR}/src/platform/macos/display.mm" + "${CMAKE_SOURCE_DIR}/src/platform/macos/input.cpp" + "${CMAKE_SOURCE_DIR}/src/platform/macos/microphone.mm" + "${CMAKE_SOURCE_DIR}/src/platform/macos/misc.mm" + "${CMAKE_SOURCE_DIR}/src/platform/macos/misc.h" + "${CMAKE_SOURCE_DIR}/src/platform/macos/nv12_zero_device.cpp" + "${CMAKE_SOURCE_DIR}/src/platform/macos/nv12_zero_device.h" + "${CMAKE_SOURCE_DIR}/src/platform/macos/publish.cpp" + "${CMAKE_SOURCE_DIR}/third-party/TPCircularBuffer/TPCircularBuffer.c" + "${CMAKE_SOURCE_DIR}/third-party/TPCircularBuffer/TPCircularBuffer.h" + ${APPLE_PLIST_FILE}) + +if(SUNSHINE_ENABLE_TRAY) + list(APPEND SUNSHINE_EXTERNAL_LIBRARIES + ${COCOA}) + list(APPEND PLATFORM_TARGET_FILES + "${CMAKE_SOURCE_DIR}/third-party/tray/src/tray_darwin.m") +endif() diff --git a/cmake/compile_definitions/unix.cmake b/cmake/compile_definitions/unix.cmake new file mode 100644 index 00000000000..27df1249540 --- /dev/null +++ b/cmake/compile_definitions/unix.cmake @@ -0,0 +1,11 @@ +# unix specific compile definitions +# put anything here that applies to both linux and macos + +list(APPEND SUNSHINE_EXTERNAL_LIBRARIES + Boost::log + ${CURL_LIBRARIES}) + +# add install prefix to assets path if not already there +if(NOT SUNSHINE_ASSETS_DIR MATCHES "^${CMAKE_INSTALL_PREFIX}") + set(SUNSHINE_ASSETS_DIR "${CMAKE_INSTALL_PREFIX}/${SUNSHINE_ASSETS_DIR}") +endif() diff --git a/cmake/compile_definitions/windows.cmake b/cmake/compile_definitions/windows.cmake new file mode 100644 index 00000000000..fa5c0614d5a --- /dev/null +++ b/cmake/compile_definitions/windows.cmake @@ -0,0 +1,87 @@ +# windows specific compile definitions + +add_compile_definitions(SUNSHINE_PLATFORM="windows") + +enable_language(RC) +set(CMAKE_RC_COMPILER windres) +set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static") + +# gcc complains about misleading indentation in some mingw includes +list(APPEND SUNSHINE_COMPILE_OPTIONS -Wno-misleading-indentation) + +# see gcc bug 98723 +add_definitions(-DUSE_BOOST_REGEX) + +# curl +add_definitions(-DCURL_STATICLIB) +include_directories(SYSTEM ${CURL_STATIC_INCLUDE_DIRS}) +link_directories(${CURL_STATIC_LIBRARY_DIRS}) + +# miniupnpc +add_definitions(-DMINIUPNP_STATICLIB) + +# extra tools/binaries for audio/display devices +add_subdirectory(tools) # todo - this is temporary, only tools for Windows are needed, for now + +# nvidia +include_directories(SYSTEM "${CMAKE_SOURCE_DIR}/third-party/nvapi-open-source-sdk") +file(GLOB NVPREFS_FILES CONFIGURE_DEPENDS + "${CMAKE_SOURCE_DIR}/third-party/nvapi-open-source-sdk/*.h" + "${CMAKE_SOURCE_DIR}/src/platform/windows/nvprefs/*.cpp" + "${CMAKE_SOURCE_DIR}/src/platform/windows/nvprefs/*.h") + +# vigem +include_directories(SYSTEM "${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/include") + +# sunshine icon +if(NOT DEFINED SUNSHINE_ICON_PATH) + set(SUNSHINE_ICON_PATH "${CMAKE_SOURCE_DIR}/sunshine.ico") +endif() + +configure_file("${CMAKE_SOURCE_DIR}/src/platform/windows/windows.rs.in" windows.rc @ONLY) + +set(PLATFORM_TARGET_FILES + "${CMAKE_CURRENT_BINARY_DIR}/windows.rc" + "${CMAKE_SOURCE_DIR}/src/platform/windows/publish.cpp" + "${CMAKE_SOURCE_DIR}/src/platform/windows/misc.h" + "${CMAKE_SOURCE_DIR}/src/platform/windows/misc.cpp" + "${CMAKE_SOURCE_DIR}/src/platform/windows/input.cpp" + "${CMAKE_SOURCE_DIR}/src/platform/windows/display.h" + "${CMAKE_SOURCE_DIR}/src/platform/windows/display_base.cpp" + "${CMAKE_SOURCE_DIR}/src/platform/windows/display_vram.cpp" + "${CMAKE_SOURCE_DIR}/src/platform/windows/display_ram.cpp" + "${CMAKE_SOURCE_DIR}/src/platform/windows/display_wgc.cpp" + "${CMAKE_SOURCE_DIR}/src/platform/windows/audio.cpp" + "${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/src/ViGEmClient.cpp" + "${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/include/ViGEm/Client.h" + "${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/include/ViGEm/Common.h" + "${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/include/ViGEm/Util.h" + "${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/include/ViGEm/km/BusShared.h" + ${NVPREFS_FILES}) + +set(OPENSSL_LIBRARIES + libssl.a + libcrypto.a) + +list(PREPEND PLATFORM_LIBRARIES + libstdc++.a + libwinpthread.a + libssp.a + ksuser + wsock32 + ws2_32 + d3d11 dxgi D3DCompiler + setupapi + dwmapi + userenv + synchronization.lib + avrt + iphlpapi + shlwapi + PkgConfig::NLOHMANN_JSON + ${CURL_STATIC_LIBRARIES}) + +if(SUNSHINE_ENABLE_TRAY) + list(APPEND PLATFORM_TARGET_FILES + "${CMAKE_SOURCE_DIR}/third-party/tray/src/tray_windows.c") +endif() diff --git a/cmake/dependencies/common.cmake b/cmake/dependencies/common.cmake new file mode 100644 index 00000000000..d10a769ca9c --- /dev/null +++ b/cmake/dependencies/common.cmake @@ -0,0 +1,83 @@ +# load common dependencies +# this file will also load platform specific dependencies + +# submodules +# moonlight common library +set(ENET_NO_INSTALL ON CACHE BOOL "Don't install any libraries build for enet") +add_subdirectory("${CMAKE_SOURCE_DIR}/third-party/moonlight-common-c/enet") + +# web server +add_subdirectory("${CMAKE_SOURCE_DIR}/third-party/Simple-Web-Server") + +# common dependencies +find_package(OpenSSL REQUIRED) +find_package(PkgConfig REQUIRED) +find_package(Threads REQUIRED) +pkg_check_modules(CURL REQUIRED libcurl) + +# miniupnp +pkg_check_modules(MINIUPNP miniupnpc REQUIRED) +include_directories(SYSTEM ${MINIUPNP_INCLUDE_DIRS}) + +# ffmpeg pre-compiled binaries +if(NOT DEFINED FFMPEG_PREPARED_BINARIES) + if(WIN32) + set(FFMPEG_PLATFORM_LIBRARIES mfplat ole32 strmiids mfuuid vpl) + elseif(UNIX AND NOT APPLE) + set(FFMPEG_PLATFORM_LIBRARIES numa va va-drm va-x11 vdpau X11) + if(CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64") + list(APPEND FFMPEG_PLATFORM_LIBRARIES mfx) + set(CPACK_DEB_PLATFORM_PACKAGE_DEPENDS "libmfx1,") + set(CPACK_RPM_PLATFORM_PACKAGE_REQUIRES "intel-mediasdk >= 22.3.0,") + endif() + endif() + set(FFMPEG_PREPARED_BINARIES + "${CMAKE_SOURCE_DIR}/third-party/build-deps/ffmpeg/${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}") + + # check if the directory exists + if(NOT EXISTS "${FFMPEG_PREPARED_BINARIES}") + message(FATAL_ERROR + "FFmpeg pre-compiled binaries not found at ${FFMPEG_PREPARED_BINARIES}. \ + Please consider contributing to the LizardByte/build-deps repository. \ + Optionally, you can use the FFMPEG_PREPARED_BINARIES option to specify the path to the \ + system-installed FFmpeg libraries") + endif() + + if(EXISTS "${FFMPEG_PREPARED_BINARIES}/lib/libhdr10plus.a") + set(HDR10_PLUS_LIBRARY + "${FFMPEG_PREPARED_BINARIES}/lib/libhdr10plus.a") + endif() + set(FFMPEG_LIBRARIES + "${FFMPEG_PREPARED_BINARIES}/lib/libavcodec.a" + "${FFMPEG_PREPARED_BINARIES}/lib/libavutil.a" + "${FFMPEG_PREPARED_BINARIES}/lib/libcbs.a" + "${FFMPEG_PREPARED_BINARIES}/lib/libSvtAv1Enc.a" + "${FFMPEG_PREPARED_BINARIES}/lib/libswscale.a" + "${FFMPEG_PREPARED_BINARIES}/lib/libx264.a" + "${FFMPEG_PREPARED_BINARIES}/lib/libx265.a" + ${HDR10_PLUS_LIBRARY} + ${FFMPEG_PLATFORM_LIBRARIES}) +else() + set(FFMPEG_LIBRARIES + "${FFMPEG_PREPARED_BINARIES}/lib/libavcodec.a" + "${FFMPEG_PREPARED_BINARIES}/lib/libavutil.a" + "${FFMPEG_PREPARED_BINARIES}/lib/libcbs.a" + "${FFMPEG_PREPARED_BINARIES}/lib/libswscale.a" + ${FFMPEG_PLATFORM_LIBRARIES}) +endif() + +set(FFMPEG_INCLUDE_DIRS + "${FFMPEG_PREPARED_BINARIES}/include") + +# platform specific dependencies +if(WIN32) + include("${CMAKE_MODULE_PATH}/dependencies/windows.cmake") +elseif(UNIX) + include("${CMAKE_MODULE_PATH}/dependencies/unix.cmake") + + if(APPLE) + include("${CMAKE_MODULE_PATH}/dependencies/macos.cmake") + else() + include("${CMAKE_MODULE_PATH}/dependencies/linux.cmake") + endif() +endif() diff --git a/cmake/dependencies/linux.cmake b/cmake/dependencies/linux.cmake new file mode 100644 index 00000000000..8022b9dfea0 --- /dev/null +++ b/cmake/dependencies/linux.cmake @@ -0,0 +1 @@ +# linux specific dependencies diff --git a/cmake/dependencies/macos.cmake b/cmake/dependencies/macos.cmake new file mode 100644 index 00000000000..61efc6a902b --- /dev/null +++ b/cmake/dependencies/macos.cmake @@ -0,0 +1,13 @@ +# macos specific dependencies + +FIND_LIBRARY(APP_KIT_LIBRARY AppKit) +FIND_LIBRARY(APP_SERVICES_LIBRARY ApplicationServices) +FIND_LIBRARY(AV_FOUNDATION_LIBRARY AVFoundation) +FIND_LIBRARY(CORE_MEDIA_LIBRARY CoreMedia) +FIND_LIBRARY(CORE_VIDEO_LIBRARY CoreVideo) +FIND_LIBRARY(FOUNDATION_LIBRARY Foundation) +FIND_LIBRARY(VIDEO_TOOLBOX_LIBRARY VideoToolbox) + +if(SUNSHINE_ENABLE_TRAY) + FIND_LIBRARY(COCOA Cocoa REQUIRED) +endif() diff --git a/cmake/dependencies/unix.cmake b/cmake/dependencies/unix.cmake new file mode 100644 index 00000000000..5b13bf60403 --- /dev/null +++ b/cmake/dependencies/unix.cmake @@ -0,0 +1,4 @@ +# unix specific dependencies +# put anything here that applies to both linux and macos + +find_package(Boost COMPONENTS locale log filesystem program_options REQUIRED) diff --git a/cmake/dependencies/windows.cmake b/cmake/dependencies/windows.cmake new file mode 100644 index 00000000000..a7ecce3963d --- /dev/null +++ b/cmake/dependencies/windows.cmake @@ -0,0 +1,7 @@ +# windows specific dependencies + +set(Boost_USE_STATIC_LIBS ON) # cmake-lint: disable=C0103 +find_package(Boost 1.71.0 COMPONENTS locale log filesystem program_options REQUIRED) + +# nlohmann_json +pkg_check_modules(NLOHMANN_JSON nlohmann_json REQUIRED IMPORTED_TARGET) diff --git a/cmake/macros/common.cmake b/cmake/macros/common.cmake new file mode 100644 index 00000000000..24592355fbb --- /dev/null +++ b/cmake/macros/common.cmake @@ -0,0 +1,15 @@ +# common macros +# this file will also load platform specific macros + +# platform specific macros +if(WIN32) + include(${CMAKE_MODULE_PATH}/macros/windows.cmake) +elseif(UNIX) + include(${CMAKE_MODULE_PATH}/macros/unix.cmake) + + if(APPLE) + include(${CMAKE_MODULE_PATH}/macros/macos.cmake) + else() + include(${CMAKE_MODULE_PATH}/macros/linux.cmake) + endif() +endif() diff --git a/cmake/macros/linux.cmake b/cmake/macros/linux.cmake new file mode 100644 index 00000000000..0bb5e043bf2 --- /dev/null +++ b/cmake/macros/linux.cmake @@ -0,0 +1,31 @@ +# linux specific macros + +# GEN_WAYLAND: args = `filename` +macro(GEN_WAYLAND wayland_directory subdirectory filename) + file(MAKE_DIRECTORY ${CMAKE_BINARY_DIR}/generated-src) + + message("wayland-scanner private-code \ +${wayland_directory}/${subdirectory}/${filename}.xml \ +${CMAKE_BINARY_DIR}/generated-src/${filename}.c") + message("wayland-scanner client-header \ +${wayland_directory}/${subdirectory}/${filename}.xml \ +${CMAKE_BINARY_DIR}/generated-src/${filename}.h") + execute_process( + COMMAND wayland-scanner private-code + ${wayland_directory}/${subdirectory}/${filename}.xml + ${CMAKE_BINARY_DIR}/generated-src/${filename}.c + COMMAND wayland-scanner client-header + ${wayland_directory}/${subdirectory}/${filename}.xml + ${CMAKE_BINARY_DIR}/generated-src/${filename}.h + + RESULT_VARIABLE EXIT_INT + ) + + if(NOT ${EXIT_INT} EQUAL 0) + message(FATAL_ERROR "wayland-scanner failed") + endif() + + list(APPEND PLATFORM_TARGET_FILES + ${CMAKE_BINARY_DIR}/generated-src/${filename}.c + ${CMAKE_BINARY_DIR}/generated-src/${filename}.h) +endmacro() diff --git a/cmake/macros/macos.cmake b/cmake/macros/macos.cmake new file mode 100644 index 00000000000..81cb969497b --- /dev/null +++ b/cmake/macros/macos.cmake @@ -0,0 +1,16 @@ +# macos specific macros + +# ADD_FRAMEWORK: args = `fwname`, `appname` +macro(ADD_FRAMEWORK fwname appname) + find_library(FRAMEWORK_${fwname} + NAMES ${fwname} + PATHS ${CMAKE_OSX_SYSROOT}/System/Library + PATH_SUFFIXES Frameworks + NO_DEFAULT_PATH) + if( ${FRAMEWORK_${fwname}} STREQUAL FRAMEWORK_${fwname}-NOTFOUND) + MESSAGE(ERROR ": Framework ${fwname} not found") + else() + TARGET_LINK_LIBRARIES(${appname} "${FRAMEWORK_${fwname}}/${fwname}") + MESSAGE(STATUS "Framework ${fwname} found at ${FRAMEWORK_${fwname}}") + endif() +endmacro(ADD_FRAMEWORK) diff --git a/cmake/macros/unix.cmake b/cmake/macros/unix.cmake new file mode 100644 index 00000000000..3fb68ed4c26 --- /dev/null +++ b/cmake/macros/unix.cmake @@ -0,0 +1,2 @@ +# unix specific macros +# put anything here that applies to both linux and macos diff --git a/cmake/macros/windows.cmake b/cmake/macros/windows.cmake new file mode 100644 index 00000000000..9cc0e46f2e0 --- /dev/null +++ b/cmake/macros/windows.cmake @@ -0,0 +1 @@ +# windows specific macros diff --git a/cmake/packaging/common.cmake b/cmake/packaging/common.cmake new file mode 100644 index 00000000000..c7c5b3a5cc9 --- /dev/null +++ b/cmake/packaging/common.cmake @@ -0,0 +1,45 @@ +# common packaging + +# common cpack options +set(CPACK_PACKAGE_NAME ${CMAKE_PROJECT_NAME}) +set(CPACK_PACKAGE_VENDOR "LizardByte") +set(CPACK_PACKAGE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/cpack_artifacts) +set(CPACK_PACKAGE_CONTACT "https://app.lizardbyte.dev") +set(CPACK_PACKAGE_DESCRIPTION ${CMAKE_PROJECT_DESCRIPTION}) +set(CPACK_PACKAGE_HOMEPAGE_URL ${CMAKE_PROJECT_HOMEPAGE_URL}) +set(CPACK_RESOURCE_FILE_LICENSE ${PROJECT_SOURCE_DIR}/LICENSE) +set(CPACK_PACKAGE_ICON ${PROJECT_SOURCE_DIR}/sunshine.png) +set(CPACK_PACKAGE_FILE_NAME "${CMAKE_PROJECT_NAME}") +set(CPACK_STRIP_FILES YES) + +# install common assets +install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/common/assets/" + DESTINATION "${SUNSHINE_ASSETS_DIR}" + PATTERN "web" EXCLUDE) +# copy assets to build directory, for running without install +file(GLOB_RECURSE ALL_ASSETS + RELATIVE "${SUNSHINE_SOURCE_ASSETS_DIR}/common/assets/" "${SUNSHINE_SOURCE_ASSETS_DIR}/common/assets/*") +list(FILTER ALL_ASSETS EXCLUDE REGEX "^web/.*$") # Filter out the web directory +foreach(asset ${ALL_ASSETS}) # Copy assets to build directory, excluding the web directory + file(COPY "${SUNSHINE_SOURCE_ASSETS_DIR}/common/assets/${asset}" + DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/assets") +endforeach() + +# install built vite assets +install(DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/assets/web" + DESTINATION "${SUNSHINE_ASSETS_DIR}") + +# platform specific packaging +if(WIN32) + include(${CMAKE_MODULE_PATH}/packaging/windows.cmake) +elseif(UNIX) + include(${CMAKE_MODULE_PATH}/packaging/unix.cmake) + + if(APPLE) + include(${CMAKE_MODULE_PATH}/packaging/macos.cmake) + else() + include(${CMAKE_MODULE_PATH}/packaging/linux.cmake) + endif() +endif() + +include(CPack) diff --git a/cmake/packaging/linux.cmake b/cmake/packaging/linux.cmake new file mode 100644 index 00000000000..4d9cfbcec72 --- /dev/null +++ b/cmake/packaging/linux.cmake @@ -0,0 +1,113 @@ +# linux specific packaging + +install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/linux/assets/" + DESTINATION "${SUNSHINE_ASSETS_DIR}") +# copy assets to build directory, for running without install +file(COPY "${SUNSHINE_SOURCE_ASSETS_DIR}/linux/assets/" + DESTINATION "${CMAKE_BINARY_DIR}/assets") +if(${SUNSHINE_BUILD_APPIMAGE} OR ${SUNSHINE_BUILD_FLATPAK}) + install(FILES "${SUNSHINE_SOURCE_ASSETS_DIR}/linux/misc/60-sunshine.rules" + DESTINATION "${SUNSHINE_ASSETS_DIR}/udev/rules.d") + install(FILES "${CMAKE_CURRENT_BINARY_DIR}/sunshine.service" + DESTINATION "${SUNSHINE_ASSETS_DIR}/systemd/user") +else() + find_package(Systemd) + find_package(Udev) + + install(FILES "${SUNSHINE_SOURCE_ASSETS_DIR}/linux/misc/60-sunshine.rules" + DESTINATION "${UDEV_RULES_INSTALL_DIR}") + install(FILES "${CMAKE_CURRENT_BINARY_DIR}/sunshine.service" + DESTINATION "${SYSTEMD_USER_UNIT_INSTALL_DIR}") +endif() + +# Post install +set(CPACK_DEBIAN_PACKAGE_CONTROL_EXTRA "${SUNSHINE_SOURCE_ASSETS_DIR}/linux/misc/postinst") +set(CPACK_RPM_POST_INSTALL_SCRIPT_FILE "${SUNSHINE_SOURCE_ASSETS_DIR}/linux/misc/postinst") + +# Dependencies +set(CPACK_DEB_COMPONENT_INSTALL ON) +set(CPACK_DEBIAN_PACKAGE_DEPENDS "\ + ${CPACK_DEB_PLATFORM_PACKAGE_DEPENDS} \ + libboost-filesystem${Boost_VERSION}, \ + libboost-locale${Boost_VERSION}, \ + libboost-log${Boost_VERSION}, \ + libboost-program-options${Boost_VERSION}, \ + libcap2, \ + libcurl4, \ + libdrm2, \ + libevdev2, \ + libnuma1, \ + libopus0, \ + libpulse0, \ + libva2, \ + libva-drm2, \ + libvdpau1, \ + libwayland-client0, \ + libx11-6, \ + miniupnpc, \ + openssl | libssl3") +set(CPACK_RPM_PACKAGE_REQUIRES "\ + ${CPACK_RPM_PLATFORM_PACKAGE_REQUIRES} \ + boost-filesystem >= ${Boost_VERSION}, \ + boost-locale >= ${Boost_VERSION}, \ + boost-log >= ${Boost_VERSION}, \ + boost-program-options >= ${Boost_VERSION}, \ + libcap >= 2.22, \ + libcurl >= 7.0, \ + libdrm >= 2.4.97, \ + libevdev >= 1.5.6, \ + libopusenc >= 0.2.1, \ + libva >= 2.14.0, \ + libvdpau >= 1.5, \ + libwayland-client >= 1.20.0, \ + libX11 >= 1.7.3.1, \ + miniupnpc >= 2.2.4, \ + numactl-libs >= 2.0.14, \ + openssl >= 3.0.2, \ + pulseaudio-libs >= 10.0") + +# This should automatically figure out dependencies, doesn't work with the current config +set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS OFF) + +# application icon +install(FILES "${CMAKE_SOURCE_DIR}/sunshine.svg" + DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/scalable/apps") + +# tray icon +if(${SUNSHINE_TRAY} STREQUAL 1) + install(FILES "${CMAKE_SOURCE_DIR}/sunshine.svg" + DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/scalable/status" + RENAME "sunshine-tray.svg") + install(FILES "${SUNSHINE_SOURCE_ASSETS_DIR}/common/assets/web/public/images/sunshine-playing.svg" + DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/scalable/status") + install(FILES "${SUNSHINE_SOURCE_ASSETS_DIR}/common/assets/web/public/images/sunshine-pausing.svg" + DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/scalable/status") + install(FILES "${SUNSHINE_SOURCE_ASSETS_DIR}/common/assets/web/public/images/sunshine-locked.svg" + DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/scalable/status") + + set(CPACK_DEBIAN_PACKAGE_DEPENDS "\ + ${CPACK_DEBIAN_PACKAGE_DEPENDS}, \ + libayatana-appindicator3-1, \ + libnotify4") + set(CPACK_RPM_PACKAGE_REQUIRES "\ + ${CPACK_RPM_PACKAGE_REQUIRES}, \ + libappindicator-gtk3 >= 12.10.0") +endif() + +# desktop file +# todo - validate desktop files with `desktop-file-validate` +install(FILES "${CMAKE_CURRENT_BINARY_DIR}/sunshine.desktop" + DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/applications") +if(NOT ${SUNSHINE_BUILD_APPIMAGE}) + install(FILES "${CMAKE_CURRENT_BINARY_DIR}/sunshine_terminal.desktop" + DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/applications") +endif() +if(${SUNSHINE_BUILD_FLATPAK}) + install(FILES "${CMAKE_CURRENT_BINARY_DIR}/sunshine_kms.desktop" + DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/applications") +endif() + +# metadata file +# todo - validate file with `appstream-util validate-relax` +install(FILES "${CMAKE_CURRENT_BINARY_DIR}/sunshine.appdata.xml" + DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/metainfo") diff --git a/cmake/packaging/macos.cmake b/cmake/packaging/macos.cmake new file mode 100644 index 00000000000..a16fdb66a26 --- /dev/null +++ b/cmake/packaging/macos.cmake @@ -0,0 +1,25 @@ +# macos specific packaging + +# todo - bundle doesn't produce a valid .app use cpack -G DragNDrop +set(CPACK_BUNDLE_NAME "${CMAKE_PROJECT_NAME}") +set(CPACK_BUNDLE_PLIST "${APPLE_PLIST_FILE}") +set(CPACK_BUNDLE_ICON "${PROJECT_SOURCE_DIR}/sunshine.icns") +# set(CPACK_BUNDLE_STARTUP_COMMAND "${INSTALL_RUNTIME_DIR}/sunshine") + +if(SUNSHINE_PACKAGE_MACOS) # todo + set(MAC_PREFIX "${CMAKE_PROJECT_NAME}.app/Contents") + set(INSTALL_RUNTIME_DIR "${MAC_PREFIX}/MacOS") + + install(TARGETS sunshine + BUNDLE DESTINATION . COMPONENT Runtime + RUNTIME DESTINATION ${INSTALL_RUNTIME_DIR} COMPONENT Runtime) +else() + install(FILES "${SUNSHINE_SOURCE_ASSETS_DIR}/macos/misc/uninstall_pkg.sh" + DESTINATION "${SUNSHINE_ASSETS_DIR}") +endif() + +install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/macos/assets/" + DESTINATION "${SUNSHINE_ASSETS_DIR}") +# copy assets to build directory, for running without install +file(COPY "${SUNSHINE_SOURCE_ASSETS_DIR}/macos/assets/" + DESTINATION "${CMAKE_BINARY_DIR}/assets") diff --git a/cmake/packaging/unix.cmake b/cmake/packaging/unix.cmake new file mode 100644 index 00000000000..bacbfc910de --- /dev/null +++ b/cmake/packaging/unix.cmake @@ -0,0 +1,15 @@ +# unix specific packaging +# put anything here that applies to both linux and macos + +# return here if building a macos package +if(SUNSHINE_PACKAGE_MACOS) + return() +endif() + +# Installation destination dir +set(CPACK_SET_DESTDIR true) +if(NOT CMAKE_INSTALL_PREFIX) + set(CMAKE_INSTALL_PREFIX "/usr/share/sunshine") +endif() + +install(TARGETS sunshine RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}") diff --git a/cmake/packaging/windows.cmake b/cmake/packaging/windows.cmake new file mode 100644 index 00000000000..bbd497ee3a0 --- /dev/null +++ b/cmake/packaging/windows.cmake @@ -0,0 +1,155 @@ +# windows specific packaging + +# see options at: https://cmake.org/cmake/help/latest/cpack_gen/nsis.html +install(TARGETS sunshine RUNTIME DESTINATION "." COMPONENT application) + +# Hardening: include zlib1.dll (loaded via LoadLibrary() in openssl's libcrypto.a) +install(FILES "${ZLIB}" DESTINATION "." COMPONENT application) + +# Adding tools +install(TARGETS dxgi-info RUNTIME DESTINATION "tools" COMPONENT dxgi) +install(TARGETS audio-info RUNTIME DESTINATION "tools" COMPONENT audio) + +# Mandatory tools +install(TARGETS ddprobe RUNTIME DESTINATION "tools" COMPONENT application) +install(TARGETS sunshinesvc RUNTIME DESTINATION "tools" COMPONENT application) + +# Mandatory scripts +install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/windows/misc/service/" + DESTINATION "scripts" + COMPONENT assets) +install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/windows/misc/migration/" + DESTINATION "scripts" + COMPONENT assets) + +# Configurable options for the service +install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/windows/misc/autostart/" + DESTINATION "scripts" + COMPONENT autostart) + +# scripts +install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/windows/misc/firewall/" + DESTINATION "scripts" + COMPONENT firewall) +install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/windows/misc/gamepad/" + DESTINATION "scripts" + COMPONENT gamepad) + +# Sunshine assets +install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/windows/assets/" + DESTINATION "${SUNSHINE_ASSETS_DIR}" + COMPONENT assets) +# copy assets to build directory, for running without install +file(COPY "${SUNSHINE_SOURCE_ASSETS_DIR}/windows/assets/" + DESTINATION "${CMAKE_BINARY_DIR}/assets") + +# set(CPACK_NSIS_MUI_HEADERIMAGE "") # TODO: image should be 150x57 bmp +set(CPACK_PACKAGE_ICON "${CMAKE_SOURCE_DIR}\\\\sunshine.ico") +set(CPACK_NSIS_INSTALLED_ICON_NAME "${PROJECT__DIR}\\\\${PROJECT_EXE}") +# The name of the directory that will be created in C:/Program files/ +set(CPACK_PACKAGE_INSTALL_DIRECTORY "${CPACK_PACKAGE_NAME}") + +# Extra install commands +# Restores permissions on the install directory +# Migrates config files from the root into the new config folder +# Install service +SET(CPACK_NSIS_EXTRA_INSTALL_COMMANDS + "${CPACK_NSIS_EXTRA_INSTALL_COMMANDS} + IfSilent +2 0 + ExecShell 'open' 'https://sunshinestream.readthedocs.io/' + nsExec::ExecToLog 'icacls \\\"$INSTDIR\\\" /reset' + nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\migrate-config.bat\\\"' + nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\add-firewall-rule.bat\\\"' + nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\install-gamepad.bat\\\"' + nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\install-service.bat\\\"' + nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\autostart-service.bat\\\"' + NoController: + ") + +# Extra uninstall commands +# Uninstall service +set(CPACK_NSIS_EXTRA_UNINSTALL_COMMANDS + "${CPACK_NSIS_EXTRA_UNINSTALL_COMMANDS} + nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\delete-firewall-rule.bat\\\"' + nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\uninstall-service.bat\\\"' + nsExec::ExecToLog '\\\"$INSTDIR\\\\sunshine.exe\\\" --restore-nvprefs-undo' + MessageBox MB_YESNO|MB_ICONQUESTION \ + 'Do you want to remove Virtual Gamepad)?' \ + /SD IDNO IDNO NoGamepad + nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\uninstall-gamepad.bat\\\"'; skipped if no + NoGamepad: + MessageBox MB_YESNO|MB_ICONQUESTION \ + 'Do you want to remove $INSTDIR (this includes the configuration, cover images, and settings)?' \ + /SD IDNO IDNO NoDelete + RMDir /r \\\"$INSTDIR\\\"; skipped if no + NoDelete: + ") + +# Adding an option for the start menu +set(CPACK_NSIS_MODIFY_PATH "OFF") +set(CPACK_NSIS_EXECUTABLES_DIRECTORY ".") +# This will be shown on the installed apps Windows settings +set(CPACK_NSIS_INSTALLED_ICON_NAME "${CMAKE_PROJECT_NAME}.exe") +set(CPACK_NSIS_CREATE_ICONS_EXTRA + "${CPACK_NSIS_CREATE_ICONS_EXTRA} + CreateShortCut '\$SMPROGRAMS\\\\$STARTMENU_FOLDER\\\\${CMAKE_PROJECT_NAME}.lnk' \ + '\$INSTDIR\\\\${CMAKE_PROJECT_NAME}.exe' '--shortcut' + ") +set(CPACK_NSIS_DELETE_ICONS_EXTRA + "${CPACK_NSIS_DELETE_ICONS_EXTRA} + Delete '\$SMPROGRAMS\\\\$MUI_TEMP\\\\${CMAKE_PROJECT_NAME}.lnk' + ") + +# Checking for previous installed versions +set(CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL "ON") + +set(CPACK_NSIS_HELP_LINK "https://sunshinestream.readthedocs.io/en/latest/about/installation.html") +set(CPACK_NSIS_URL_INFO_ABOUT "${CMAKE_PROJECT_HOMEPAGE_URL}") +set(CPACK_NSIS_CONTACT "${CMAKE_PROJECT_HOMEPAGE_URL}/support") + +set(CPACK_NSIS_MENU_LINKS + "https://sunshinestream.readthedocs.io" "Sunshine documentation" + "https://app.lizardbyte.dev" "LizardByte Web Site" + "https://app.lizardbyte.dev/support" "LizardByte Support") +set(CPACK_NSIS_MANIFEST_DPI_AWARE true) + +# Setting components groups and dependencies +set(CPACK_COMPONENT_GROUP_CORE_EXPANDED true) + +# sunshine binary +set(CPACK_COMPONENT_APPLICATION_DISPLAY_NAME "${CMAKE_PROJECT_NAME}") +set(CPACK_COMPONENT_APPLICATION_DESCRIPTION "${CMAKE_PROJECT_NAME} main application and required components.") +set(CPACK_COMPONENT_APPLICATION_GROUP "Core") +set(CPACK_COMPONENT_APPLICATION_REQUIRED true) +set(CPACK_COMPONENT_APPLICATION_DEPENDS assets) + +# service auto-start script +set(CPACK_COMPONENT_AUTOSTART_DISPLAY_NAME "Launch on Startup") +set(CPACK_COMPONENT_AUTOSTART_DESCRIPTION "If enabled, launches Sunshine automatically on system startup.") +set(CPACK_COMPONENT_AUTOSTART_GROUP "Core") + +# assets +set(CPACK_COMPONENT_ASSETS_DISPLAY_NAME "Required Assets") +set(CPACK_COMPONENT_ASSETS_DESCRIPTION "Shaders, default box art, and web UI.") +set(CPACK_COMPONENT_ASSETS_GROUP "Core") +set(CPACK_COMPONENT_ASSETS_REQUIRED true) + +# audio tool +set(CPACK_COMPONENT_AUDIO_DISPLAY_NAME "audio-info") +set(CPACK_COMPONENT_AUDIO_DESCRIPTION "CLI tool providing information about sound devices.") +set(CPACK_COMPONENT_AUDIO_GROUP "Tools") + +# display tool +set(CPACK_COMPONENT_DXGI_DISPLAY_NAME "dxgi-info") +set(CPACK_COMPONENT_DXGI_DESCRIPTION "CLI tool providing information about graphics cards and displays.") +set(CPACK_COMPONENT_DXGI_GROUP "Tools") + +# firewall scripts +set(CPACK_COMPONENT_FIREWALL_DISPLAY_NAME "Add Firewall Exclusions") +set(CPACK_COMPONENT_FIREWALL_DESCRIPTION "Scripts to enable or disable firewall rules.") +set(CPACK_COMPONENT_FIREWALL_GROUP "Scripts") + +# gamepad scripts +set(CPACK_COMPONENT_GAMEPAD_DISPLAY_NAME "Virtual Gamepad") +set(CPACK_COMPONENT_GAMEPAD_DESCRIPTION "Scripts to install and uninstall Virtual Gamepad.") +set(CPACK_COMPONENT_GAMEPAD_GROUP "Scripts") diff --git a/cmake/prep/build_version.cmake b/cmake/prep/build_version.cmake new file mode 100644 index 00000000000..56a4eddafcf --- /dev/null +++ b/cmake/prep/build_version.cmake @@ -0,0 +1,55 @@ +# Check if env vars are defined before attempting to access them, variables will be defined even if blank +if((DEFINED ENV{BRANCH}) AND (DEFINED ENV{BUILD_VERSION}) AND (DEFINED ENV{COMMIT})) # cmake-lint: disable=W0106 + if(($ENV{BRANCH} STREQUAL "master") AND (NOT $ENV{BUILD_VERSION} STREQUAL "")) + # If BRANCH is "master" and BUILD_VERSION is not empty, then we are building a master branch + MESSAGE("Got from CI master branch and version $ENV{BUILD_VERSION}") + set(PROJECT_VERSION $ENV{BUILD_VERSION}) + elseif((DEFINED ENV{BRANCH}) AND (DEFINED ENV{COMMIT})) + # If BRANCH is set but not BUILD_VERSION we are building a PR, we gather only the commit hash + MESSAGE("Got from CI $ENV{BRANCH} branch and commit $ENV{COMMIT}") + set(PROJECT_VERSION ${PROJECT_VERSION}.$ENV{COMMIT}) + endif() + # Generate Sunshine Version based of the git tag + # https://github.com/nocnokneo/cmake-git-versioning-example/blob/master/LICENSE +else() + find_package(Git) + if(GIT_EXECUTABLE) + MESSAGE("${CMAKE_SOURCE_DIR}") + get_filename_component(SRC_DIR "${CMAKE_SOURCE_DIR}" DIRECTORY) + #Get current Branch + execute_process( + COMMAND ${GIT_EXECUTABLE} rev-parse --abbrev-ref HEAD + OUTPUT_VARIABLE GIT_DESCRIBE_BRANCH + RESULT_VARIABLE GIT_DESCRIBE_ERROR_CODE + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + # Gather current commit + execute_process( + COMMAND ${GIT_EXECUTABLE} rev-parse --short HEAD + OUTPUT_VARIABLE GIT_DESCRIBE_VERSION + RESULT_VARIABLE GIT_DESCRIBE_ERROR_CODE + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + # Check if Dirty + execute_process( + COMMAND ${GIT_EXECUTABLE} diff --quiet --exit-code + RESULT_VARIABLE GIT_IS_DIRTY + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + if(NOT GIT_DESCRIBE_ERROR_CODE) + MESSAGE("Sunshine Branch: ${GIT_DESCRIBE_BRANCH}") + if(NOT GIT_DESCRIBE_BRANCH STREQUAL "master") + set(PROJECT_VERSION ${PROJECT_VERSION}.${GIT_DESCRIBE_VERSION}) + MESSAGE("Sunshine Version: ${GIT_DESCRIBE_VERSION}") + endif() + if(GIT_IS_DIRTY) + set(PROJECT_VERSION ${PROJECT_VERSION}.dirty) + MESSAGE("Git tree is dirty!") + endif() + else() + MESSAGE(ERROR ": Got git error while fetching tags: ${GIT_DESCRIBE_ERROR_CODE}") + endif() + else() + MESSAGE(WARNING ": Git not found, cannot find git version") + endif() +endif() diff --git a/cmake/prep/constants.cmake b/cmake/prep/constants.cmake new file mode 100644 index 00000000000..b80be1e3480 --- /dev/null +++ b/cmake/prep/constants.cmake @@ -0,0 +1,5 @@ +# source assets will be installed from this directory +set(SUNSHINE_SOURCE_ASSETS_DIR "${CMAKE_SOURCE_DIR}/src_assets") + +# enable system tray, we will disable this later if we cannot find the required package config on linux +set(SUNSHINE_TRAY 1) diff --git a/cmake/prep/init.cmake b/cmake/prep/init.cmake new file mode 100644 index 00000000000..93e8b597721 --- /dev/null +++ b/cmake/prep/init.cmake @@ -0,0 +1,9 @@ +if (WIN32) +elseif (APPLE) +elseif (UNIX) + include(GNUInstallDirs) + + if(NOT DEFINED SUNSHINE_EXECUTABLE_PATH) + set(SUNSHINE_EXECUTABLE_PATH "sunshine") + endif() +endif () diff --git a/cmake/prep/options.cmake b/cmake/prep/options.cmake new file mode 100644 index 00000000000..1555036eeb0 --- /dev/null +++ b/cmake/prep/options.cmake @@ -0,0 +1,56 @@ +option(BUILD_TESTS "Build tests" ON) +option(TESTS_ENABLE_PYTHON_TESTS "Enable Python tests" ON) + +# DirectX11 is not available in GitHub runners, so even software encoding fails +set(TESTS_SOFTWARE_ENCODER_UNAVAILABLE "fail" + CACHE STRING "How to handle unavailable software encoders in tests. 'fail/skip'") + +option(BUILD_WERROR "Enable -Werror flag." OFF) + +# if this option is set, the build will exit after configuring special package configuration files +option(SUNSHINE_CONFIGURE_ONLY "Configure special files only, then exit." OFF) + +option(SUNSHINE_ENABLE_TRAY "Enable system tray icon. This option will be ignored on macOS." ON) +option(SUNSHINE_REQUIRE_TRAY "Require system tray icon. Fail the build if tray requirements are not met." ON) + +option(SUNSHINE_SYSTEM_WAYLAND_PROTOCOLS "Use system installation of wayland-protocols rather than the submodule." OFF) + +option(CUDA_INHERIT_COMPILE_OPTIONS + "When building CUDA code, inherit compile options from the the main project. You may want to disable this if + your IDE throws errors about unknown flags after running cmake." ON) + +if(UNIX) + # technically, the homebrew build could be on linux as well... no idea if it would actually work + option(SUNSHINE_BUILD_HOMEBREW + "Enable a Homebrew build." OFF) +endif () + +if(APPLE) + option(SUNSHINE_CONFIGURE_HOMEBREW + "Configure macOS Homebrew formula. Recommended to use with SUNSHINE_CONFIGURE_ONLY" OFF) + option(SUNSHINE_CONFIGURE_PORTFILE + "Configure macOS Portfile. Recommended to use with SUNSHINE_CONFIGURE_ONLY" OFF) + option(SUNSHINE_PACKAGE_MACOS + "Should only be used when creating a macOS package/dmg." OFF) +elseif(UNIX) # Linux + option(SUNSHINE_BUILD_APPIMAGE + "Enable an AppImage build." OFF) + option(SUNSHINE_BUILD_FLATPAK + "Enable a Flatpak build." OFF) + option(SUNSHINE_CONFIGURE_PKGBUILD + "Configure files required for AUR. Recommended to use with SUNSHINE_CONFIGURE_ONLY" OFF) + option(SUNSHINE_CONFIGURE_FLATPAK_MAN + "Configure manifest file required for Flatpak build. Recommended to use with SUNSHINE_CONFIGURE_ONLY" OFF) + + # Linux capture methods + option(SUNSHINE_ENABLE_CUDA + "Enable cuda specific code." ON) + option(SUNSHINE_ENABLE_DRM + "Enable KMS grab if available." ON) + option(SUNSHINE_ENABLE_VAAPI + "Enable building vaapi specific code." ON) + option(SUNSHINE_ENABLE_WAYLAND + "Enable building wayland specific code." ON) + option(SUNSHINE_ENABLE_X11 + "Enable X11 grab if available." ON) +endif() diff --git a/cmake/prep/special_package_configuration.cmake b/cmake/prep/special_package_configuration.cmake new file mode 100644 index 00000000000..17e724c90d0 --- /dev/null +++ b/cmake/prep/special_package_configuration.cmake @@ -0,0 +1,47 @@ +if (APPLE) + if(${SUNSHINE_CONFIGURE_PORTFILE}) + configure_file(packaging/macos/Portfile Portfile @ONLY) + endif() + if(${SUNSHINE_CONFIGURE_HOMEBREW}) + configure_file(packaging/macos/sunshine.rb sunshine.rb @ONLY) + endif() +elseif (UNIX) + # configure the .desktop file + if(${SUNSHINE_BUILD_APPIMAGE}) + configure_file(packaging/linux/AppImage/sunshine.desktop sunshine.desktop @ONLY) + elseif(${SUNSHINE_BUILD_FLATPAK}) + configure_file(packaging/linux/flatpak/sunshine.desktop sunshine.desktop @ONLY) + configure_file(packaging/linux/flatpak/sunshine_kms.desktop sunshine_kms.desktop @ONLY) + configure_file(packaging/linux/sunshine_terminal.desktop sunshine_terminal.desktop @ONLY) + else() + configure_file(packaging/linux/sunshine.desktop sunshine.desktop @ONLY) + configure_file(packaging/linux/sunshine_terminal.desktop sunshine_terminal.desktop @ONLY) + endif() + + # configure metadata file + configure_file(packaging/linux/sunshine.appdata.xml sunshine.appdata.xml @ONLY) + + # configure service + configure_file(packaging/linux/sunshine.service.in sunshine.service @ONLY) + + # configure the arch linux pkgbuild + if(${SUNSHINE_CONFIGURE_PKGBUILD}) + configure_file(packaging/linux/Arch/PKGBUILD PKGBUILD @ONLY) + configure_file(packaging/linux/Arch/sunshine.install sunshine.install @ONLY) + endif() + + # configure the flatpak manifest + if(${SUNSHINE_CONFIGURE_FLATPAK_MAN}) + configure_file(packaging/linux/flatpak/dev.lizardbyte.sunshine.yml dev.lizardbyte.sunshine.yml @ONLY) + file(COPY packaging/linux/flatpak/deps/ DESTINATION ${CMAKE_BINARY_DIR}) + endif() +endif() + +# return if configure only is set +if(${SUNSHINE_CONFIGURE_ONLY}) + # message + message(STATUS "SUNSHINE_CONFIGURE_ONLY: ON, exiting...") + set(END_BUILD ON) +else() + set(END_BUILD OFF) +endif() diff --git a/cmake/targets/common.cmake b/cmake/targets/common.cmake new file mode 100644 index 00000000000..941ef0b7330 --- /dev/null +++ b/cmake/targets/common.cmake @@ -0,0 +1,107 @@ +# common target definitions +# this file will also load platform specific macros + +add_executable(sunshine ${SUNSHINE_TARGET_FILES}) + +# platform specific target definitions +if(WIN32) + include(${CMAKE_MODULE_PATH}/targets/windows.cmake) +elseif(UNIX) + include(${CMAKE_MODULE_PATH}/targets/unix.cmake) + + if(APPLE) + include(${CMAKE_MODULE_PATH}/targets/macos.cmake) + else() + include(${CMAKE_MODULE_PATH}/targets/linux.cmake) + endif() +endif() + +# todo - is this necessary? ... for anything except linux? +if(NOT DEFINED CMAKE_CUDA_STANDARD) + set(CMAKE_CUDA_STANDARD 17) + set(CMAKE_CUDA_STANDARD_REQUIRED ON) +endif() + +target_link_libraries(sunshine ${SUNSHINE_EXTERNAL_LIBRARIES} ${EXTRA_LIBS}) +target_compile_definitions(sunshine PUBLIC ${SUNSHINE_DEFINITIONS}) +set_target_properties(sunshine PROPERTIES CXX_STANDARD 20 + VERSION ${PROJECT_VERSION} + SOVERSION ${PROJECT_VERSION_MAJOR}) + +# CLion complains about unknown flags after running cmake, and cannot add symbols to the index for cuda files +if(CUDA_INHERIT_COMPILE_OPTIONS) + foreach(flag IN LISTS SUNSHINE_COMPILE_OPTIONS) + list(APPEND SUNSHINE_COMPILE_OPTIONS_CUDA "$<$:--compiler-options=${flag}>") + endforeach() +endif() + +target_compile_options(sunshine PRIVATE $<$:${SUNSHINE_COMPILE_OPTIONS}>;$<$:${SUNSHINE_COMPILE_OPTIONS_CUDA};-std=c++17>) # cmake-lint: disable=C0301 + +# Homebrew build fails the vite build if we set these environment variables +if(${SUNSHINE_BUILD_HOMEBREW}) + set(NPM_SOURCE_ASSETS_DIR "") + set(NPM_ASSETS_DIR "") + set(NPM_BUILD_HOMEBREW "true") +else() + set(NPM_SOURCE_ASSETS_DIR ${SUNSHINE_SOURCE_ASSETS_DIR}) + set(NPM_ASSETS_DIR ${CMAKE_BINARY_DIR}) + set(NPM_BUILD_HOMEBREW "") +endif() + +#WebUI build +find_program(NPM npm REQUIRED) +add_custom_target(web-ui ALL + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" + COMMENT "Installing NPM Dependencies and Building the Web UI" + COMMAND "$<$:cmd;/C>" "${NPM}" install + COMMAND "${CMAKE_COMMAND}" -E env "SUNSHINE_BUILD_HOMEBREW=${NPM_BUILD_HOMEBREW}" "SUNSHINE_SOURCE_ASSETS_DIR=${NPM_SOURCE_ASSETS_DIR}" "SUNSHINE_ASSETS_DIR=${NPM_ASSETS_DIR}" "$<$:cmd;/C>" "${NPM}" run build # cmake-lint: disable=C0301 + COMMAND_EXPAND_LISTS + VERBATIM) + +# tests +if(BUILD_TESTS) + add_subdirectory(tests) +endif() + +# custom compile flags, must be after adding tests + +if (NOT BUILD_TESTS) + set(TEST_DIR "") +else() + set(TEST_DIR "${CMAKE_SOURCE_DIR}/tests") +endif() + +# src/upnp +set_source_files_properties("${CMAKE_SOURCE_DIR}/src/upnp.cpp" + DIRECTORY "${CMAKE_SOURCE_DIR}" "${TEST_DIR}" + PROPERTIES COMPILE_FLAGS -Wno-pedantic) + +# third-party/nanors +set_source_files_properties("${CMAKE_SOURCE_DIR}/third-party/nanors/rs.c" + DIRECTORY "${CMAKE_SOURCE_DIR}" "${TEST_DIR}" + PROPERTIES COMPILE_FLAGS "-include deps/obl/autoshim.h -ftree-vectorize") + +# third-party/ViGEmClient +set(VIGEM_COMPILE_FLAGS "") +string(APPEND VIGEM_COMPILE_FLAGS "-Wno-unknown-pragmas ") +string(APPEND VIGEM_COMPILE_FLAGS "-Wno-misleading-indentation ") +string(APPEND VIGEM_COMPILE_FLAGS "-Wno-class-memaccess ") +string(APPEND VIGEM_COMPILE_FLAGS "-Wno-unused-function ") +string(APPEND VIGEM_COMPILE_FLAGS "-Wno-unused-variable ") +set_source_files_properties("${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/src/ViGEmClient.cpp" + DIRECTORY "${CMAKE_SOURCE_DIR}" "${TEST_DIR}" + PROPERTIES + COMPILE_DEFINITIONS "UNICODE=1;ERROR_INVALID_DEVICE_OBJECT_PARAMETER=650" + COMPILE_FLAGS ${VIGEM_COMPILE_FLAGS}) + +# src/nvhttp +string(TOUPPER "x${CMAKE_BUILD_TYPE}" BUILD_TYPE) +if("${BUILD_TYPE}" STREQUAL "XDEBUG") + if(WIN32) + set_source_files_properties("${CMAKE_SOURCE_DIR}/src/nvhttp.cpp" + DIRECTORY "${CMAKE_SOURCE_DIR}" "${CMAKE_SOURCE_DIR}/tests" + PROPERTIES COMPILE_FLAGS -O2) + endif() +else() + add_definitions(-DNDEBUG) +endif() diff --git a/cmake/targets/linux.cmake b/cmake/targets/linux.cmake new file mode 100644 index 00000000000..fa1f33c0752 --- /dev/null +++ b/cmake/targets/linux.cmake @@ -0,0 +1 @@ +# linux specific target definitions diff --git a/cmake/targets/macos.cmake b/cmake/targets/macos.cmake new file mode 100644 index 00000000000..065b85c5d87 --- /dev/null +++ b/cmake/targets/macos.cmake @@ -0,0 +1,4 @@ +# macos specific target definitions +target_link_options(sunshine PRIVATE LINKER:-sectcreate,__TEXT,__info_plist,${APPLE_PLIST_FILE}) +# Tell linker to dynamically load these symbols at runtime, in case they're unavailable: +target_link_options(sunshine PRIVATE -Wl,-U,_CGPreflightScreenCaptureAccess -Wl,-U,_CGRequestScreenCaptureAccess) diff --git a/cmake/targets/unix.cmake b/cmake/targets/unix.cmake new file mode 100644 index 00000000000..047a0b3d381 --- /dev/null +++ b/cmake/targets/unix.cmake @@ -0,0 +1,2 @@ +# unix specific target definitions +# put anything here that applies to both linux and macos diff --git a/cmake/targets/windows.cmake b/cmake/targets/windows.cmake new file mode 100644 index 00000000000..b7f8fbcfe7a --- /dev/null +++ b/cmake/targets/windows.cmake @@ -0,0 +1,7 @@ +# windows specific target definitions +set_target_properties(sunshine PROPERTIES LINK_SEARCH_START_STATIC 1) +set(CMAKE_FIND_LIBRARY_SUFFIXES ".dll") +find_library(ZLIB ZLIB1) +list(APPEND SUNSHINE_EXTERNAL_LIBRARIES + Windowsapp.lib + Wtsapi32.lib) diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 00000000000..e9c9c87d87d --- /dev/null +++ b/codecov.yml @@ -0,0 +1,19 @@ +--- +codecov: + branch: master + +coverage: + status: + project: + default: + target: auto + threshold: 10% + +comment: + layout: "diff, flags, files" + behavior: default + require_changes: false # if true: only post the comment if coverage changes + +ignore: + - "tests" + - "third-party" diff --git a/crowdin.yml b/crowdin.yml index 0be504ba7d8..3dd19366ef0 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -1,7 +1,7 @@ --- "base_path": "." "base_url": "https://api.crowdin.com" # optional (for Crowdin Enterprise only) -"preserve_hierarchy": false # flatten tree on crowdin +"preserve_hierarchy": true # false will flatten tree on crowdin, but doesn't work with dest option "pull_request_labels": [ "crowdin", "l10n" @@ -10,6 +10,7 @@ "files": [ { "source": "/locale/*.po", + "dest": "/%original_file_name%", "translation": "/locale/%two_letters_code%/LC_MESSAGES/%original_file_name%", "languages_mapping": { "two_letters_code": { @@ -17,6 +18,13 @@ "en-GB": "en_GB", "en-US": "en_US" } - } + }, + "update_option": "update_as_unapproved" + }, + { + "source": "/src_assets/common/assets/web/public/assets/locale/en.json", + "dest": "/sunshine.json", + "translation": "/src_assets/common/assets/web/public/assets/locale/%two_letters_code%.%file_extension%", + "update_option": "update_as_unapproved" } ] diff --git a/docker/archlinux.dockerfile b/docker/archlinux.dockerfile index 5ff4a4ab0b8..26c1b186d7d 100644 --- a/docker/archlinux.dockerfile +++ b/docker/archlinux.dockerfile @@ -2,7 +2,7 @@ # artifacts: true # platforms: linux/amd64 # archlinux does not have an arm64 base image -# no-cache-filters: sunshine-base,artifacts,sunshine +# no-cache-filters: artifacts,sunshine ARG BASE=archlinux ARG TAG=base-devel FROM ${BASE}:${TAG} AS sunshine-base @@ -11,7 +11,7 @@ FROM ${BASE}:${TAG} AS sunshine-base RUN <<_DEPS #!/bin/bash set -e -pacman -Syu --disable-download-timeout --noconfirm \ +pacman -Syu --disable-download-timeout --needed --noconfirm \ archlinux-keyring _DEPS @@ -34,18 +34,17 @@ ENV COMMIT=${COMMIT} SHELL ["/bin/bash", "-o", "pipefail", "-c"] # install dependencies -# cuda, libcap, and libdrm are optional dependencies for PKGBUILD +# cuda is an optional build-time dependency for PKGBUILD RUN <<_DEPS #!/bin/bash set -e -pacman -Syu --disable-download-timeout --noconfirm \ +pacman -Syu --disable-download-timeout --needed --noconfirm \ base-devel \ cmake \ cuda \ git \ - libcap \ - libdrm \ - namcap + namcap \ + xorg-server-xvfb _DEPS # Setup builder user @@ -68,9 +67,11 @@ else sub_version="" fi cmake \ - -DSUNSHINE_CONFIGURE_AUR=ON \ + -DSUNSHINE_CONFIGURE_PKGBUILD=ON \ -DSUNSHINE_SUB_VERSION="${sub_version}" \ -DGITHUB_CLONE_URL="${CLONE_URL}" \ + -DGITHUB_BRANCH=${BRANCH} \ + -DGITHUB_BUILD_VERSION=${BUILD_VERSION} \ -DGITHUB_COMMIT="${COMMIT}" \ -DSUNSHINE_CONFIGURE_ONLY=ON \ /build/sunshine @@ -78,19 +79,22 @@ _MAKE WORKDIR /build/sunshine/pkg RUN mv /build/sunshine/build/PKGBUILD . +RUN mv /build/sunshine/build/sunshine.install . # namcap and build PKGBUILD file RUN <<_PKGBUILD #!/bin/bash set -e +export DISPLAY=:1 +Xvfb ${DISPLAY} -screen 0 1024x768x24 & namcap -i PKGBUILD makepkg -si --noconfirm +rm -f /build/sunshine/pkg/sunshine-debug*.pkg.tar.zst ls -a _PKGBUILD FROM scratch as artifacts -COPY --link --from=sunshine-build /build/sunshine/pkg/PKGBUILD /PKGBUILD COPY --link --from=sunshine-build /build/sunshine/pkg/sunshine*.pkg.tar.zst /sunshine.pkg.tar.zst FROM sunshine-base as sunshine @@ -102,7 +106,10 @@ COPY --link --from=artifacts /sunshine.pkg.tar.zst / RUN <<_INSTALL_SUNSHINE #!/bin/bash set -e -pacman -U --disable-download-timeout --noconfirm \ +# update keyring to prevent cached keyring errors +pacman -Syu --disable-download-timeout --needed --noconfirm \ + archlinux-keyring +pacman -U --disable-download-timeout --needed --noconfirm \ /sunshine.pkg.tar.zst _INSTALL_SUNSHINE diff --git a/docker/clion-toolchain.dockerfile b/docker/clion-toolchain.dockerfile new file mode 100644 index 00000000000..204450bf1c9 --- /dev/null +++ b/docker/clion-toolchain.dockerfile @@ -0,0 +1,132 @@ +# syntax=docker/dockerfile:1.4 +# artifacts: false +# platforms: linux/amd64 +# platforms_pr: linux/amd64 +# no-cache-filters: toolchain-base,toolchain +ARG BASE=ubuntu +ARG TAG=22.04 +FROM ${BASE}:${TAG} AS toolchain-base + +ENV DEBIAN_FRONTEND=noninteractive + +FROM toolchain-base as toolchain + +ARG TARGETPLATFORM +RUN echo "target_platform: ${TARGETPLATFORM}" + +ENV DISPLAY=:0 + +SHELL ["/bin/bash", "-o", "pipefail", "-c"] +# install dependencies +RUN <<_DEPS +#!/bin/bash +set -e +apt-get update -y +apt-get install -y --no-install-recommends \ + build-essential \ + cmake=3.22.* \ + ca-certificates \ + doxygen \ + gcc=4:11.2.* \ + g++=4:11.2.* \ + gdb \ + git \ + graphviz \ + libayatana-appindicator3-dev \ + libboost-filesystem-dev=1.74.* \ + libboost-locale-dev=1.74.* \ + libboost-log-dev=1.74.* \ + libboost-program-options-dev=1.74.* \ + libcap-dev \ + libcurl4-openssl-dev \ + libdrm-dev \ + libevdev-dev \ + libminiupnpc-dev \ + libnotify-dev \ + libnuma-dev \ + libopus-dev \ + libpulse-dev \ + libssl-dev \ + libva-dev \ + libvdpau-dev \ + libwayland-dev \ + libx11-dev \ + libxcb-shm0-dev \ + libxcb-xfixes0-dev \ + libxcb1-dev \ + libxfixes-dev \ + libxrandr-dev \ + libxtst-dev \ + python3.10 \ + python3.10-venv \ + udev \ + wget \ + x11-xserver-utils \ + xvfb +if [[ "${TARGETPLATFORM}" == 'linux/amd64' ]]; then + apt-get install -y --no-install-recommends \ + libmfx-dev +fi +apt-get clean +rm -rf /var/lib/apt/lists/* +_DEPS + +#Install Node +# hadolint ignore=SC1091 +RUN <<_INSTALL_NODE +#!/bin/bash +set -e +node_version="20.9.0" +wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash +source "$HOME/.nvm/nvm.sh" +nvm install "$node_version" +nvm use "$node_version" +nvm alias default "$node_version" +_INSTALL_NODE + +# install cuda +WORKDIR /build/cuda +# versions: https://developer.nvidia.com/cuda-toolkit-archive +ENV CUDA_VERSION="11.8.0" +ENV CUDA_BUILD="520.61.05" +# hadolint ignore=SC3010 +RUN <<_INSTALL_CUDA +#!/bin/bash +set -e +cuda_prefix="https://developer.download.nvidia.com/compute/cuda/" +cuda_suffix="" +if [[ "${TARGETPLATFORM}" == 'linux/arm64' ]]; then + cuda_suffix="_sbsa" +fi +url="${cuda_prefix}${CUDA_VERSION}/local_installers/cuda_${CUDA_VERSION}_${CUDA_BUILD}_linux${cuda_suffix}.run" +echo "cuda url: ${url}" +wget "$url" --progress=bar:force:noscroll -q --show-progress -O ./cuda.run +chmod a+x ./cuda.run +./cuda.run --silent --toolkit --toolkitpath=/usr/local --no-opengl-libs --no-man-page --no-drm +rm ./cuda.run +_INSTALL_CUDA + +WORKDIR / +# Write a shell script that starts Xvfb and then runs a shell +RUN <<_ENTRYPOINT +#!/bin/bash +set -e +cat < /entrypoint.sh +#!/bin/bash +Xvfb ${DISPLAY} -screen 0 1024x768x24 & +if [ "\$#" -eq 0 ]; then + exec "/bin/bash" +else + exec "\$@" +fi +EOF +_ENTRYPOINT + +# Make the script executable +RUN chmod +x /entrypoint.sh + +# Note about CLion +RUN echo "ATTENTION: CLion will override the entrypoint, you can disable this in the toolchain settings" + +# Use the shell script as the entrypoint +ENTRYPOINT ["/entrypoint.sh"] diff --git a/docker/debian-bookworm.dockerfile b/docker/debian-bookworm.dockerfile new file mode 100644 index 00000000000..34cf29bedc9 --- /dev/null +++ b/docker/debian-bookworm.dockerfile @@ -0,0 +1,191 @@ +# syntax=docker/dockerfile:1.4 +# artifacts: true +# platforms: linux/amd64,linux/arm64/v8 +# platforms_pr: linux/amd64 +# no-cache-filters: sunshine-base,artifacts,sunshine +ARG BASE=debian +ARG TAG=bookworm +FROM ${BASE}:${TAG} AS sunshine-base + +ENV DEBIAN_FRONTEND=noninteractive + +FROM sunshine-base as sunshine-build + +ARG TARGETPLATFORM +RUN echo "target_platform: ${TARGETPLATFORM}" + +ARG BRANCH +ARG BUILD_VERSION +ARG COMMIT +# note: BUILD_VERSION may be blank + +ENV BRANCH=${BRANCH} +ENV BUILD_VERSION=${BUILD_VERSION} +ENV COMMIT=${COMMIT} + +SHELL ["/bin/bash", "-o", "pipefail", "-c"] +# install dependencies +RUN <<_DEPS +#!/bin/bash +set -e +apt-get update -y +apt-get install -y --no-install-recommends \ + build-essential \ + cmake=3.25.* \ + doxygen \ + git \ + graphviz \ + libayatana-appindicator3-dev \ + libboost-filesystem-dev=1.74.* \ + libboost-locale-dev=1.74.* \ + libboost-log-dev=1.74.* \ + libboost-program-options-dev=1.74.* \ + libcap-dev \ + libcurl4-openssl-dev \ + libdrm-dev \ + libevdev-dev \ + libminiupnpc-dev \ + libnotify-dev \ + libnuma-dev \ + libopus-dev \ + libpulse-dev \ + libssl-dev \ + libva-dev \ + libvdpau-dev \ + libwayland-dev \ + libx11-dev \ + libxcb-shm0-dev \ + libxcb-xfixes0-dev \ + libxcb1-dev \ + libxfixes-dev \ + libxrandr-dev \ + libxtst-dev \ + nodejs \ + npm \ + python3.11 \ + python3.11-venv \ + udev \ + wget \ + x11-xserver-utils \ + xvfb +if [[ "${TARGETPLATFORM}" == 'linux/amd64' ]]; then + apt-get install -y --no-install-recommends \ + libmfx-dev +fi +apt-get clean +rm -rf /var/lib/apt/lists/* +_DEPS + +# install cuda +WORKDIR /build/cuda +# versions: https://developer.nvidia.com/cuda-toolkit-archive +ENV CUDA_VERSION="12.0.0" +ENV CUDA_BUILD="525.60.13" +# hadolint ignore=SC3010 +RUN <<_INSTALL_CUDA +#!/bin/bash +set -e +cuda_prefix="https://developer.download.nvidia.com/compute/cuda/" +cuda_suffix="" +if [[ "${TARGETPLATFORM}" == 'linux/arm64' ]]; then + cuda_suffix="_sbsa" +fi +url="${cuda_prefix}${CUDA_VERSION}/local_installers/cuda_${CUDA_VERSION}_${CUDA_BUILD}_linux${cuda_suffix}.run" +echo "cuda url: ${url}" +wget "$url" --progress=bar:force:noscroll -q --show-progress -O ./cuda.run +chmod a+x ./cuda.run +./cuda.run --silent --toolkit --toolkitpath=/build/cuda --no-opengl-libs --no-man-page --no-drm +rm ./cuda.run +_INSTALL_CUDA + +# copy repository +WORKDIR /build/sunshine/ +COPY --link .. . + +# setup build directory +WORKDIR /build/sunshine/build + +# cmake and cpack +RUN <<_MAKE +#!/bin/bash +set -e +cmake \ + -DBUILD_WERROR=ON \ + -DCMAKE_CUDA_COMPILER:PATH=/build/cuda/bin/nvcc \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX=/usr \ + -DSUNSHINE_ASSETS_DIR=share/sunshine \ + -DSUNSHINE_EXECUTABLE_PATH=/usr/bin/sunshine \ + -DSUNSHINE_ENABLE_WAYLAND=ON \ + -DSUNSHINE_ENABLE_X11=ON \ + -DSUNSHINE_ENABLE_DRM=ON \ + -DSUNSHINE_ENABLE_CUDA=ON \ + /build/sunshine +make -j "$(nproc)" +cpack -G DEB +_MAKE + +# run tests +WORKDIR /build/sunshine/build/tests +# hadolint ignore=SC1091 +RUN <<_TEST +#!/bin/bash +set -e +export DISPLAY=:1 +Xvfb ${DISPLAY} -screen 0 1024x768x24 & +./test_sunshine --gtest_color=yes +_TEST + +FROM scratch AS artifacts +ARG BASE +ARG TAG +ARG TARGETARCH +COPY --link --from=sunshine-build /build/sunshine/build/cpack_artifacts/Sunshine.deb /sunshine-${BASE}-${TAG}-${TARGETARCH}.deb + +FROM sunshine-base as sunshine + +# copy deb from builder +COPY --link --from=artifacts /sunshine*.deb /sunshine.deb + +# install sunshine +RUN <<_INSTALL_SUNSHINE +#!/bin/bash +set -e +apt-get update -y +apt-get install -y --no-install-recommends /sunshine.deb +apt-get clean +rm -rf /var/lib/apt/lists/* +_INSTALL_SUNSHINE + +# network setup +EXPOSE 47984-47990/tcp +EXPOSE 48010 +EXPOSE 47998-48000/udp + +# setup user +ARG PGID=1000 +ENV PGID=${PGID} +ARG PUID=1000 +ENV PUID=${PUID} +ENV TZ="UTC" +ARG UNAME=lizard +ENV UNAME=${UNAME} + +ENV HOME=/home/$UNAME + +# setup user +RUN <<_SETUP_USER +#!/bin/bash +set -e +groupadd -f -g "${PGID}" "${UNAME}" +useradd -lm -d ${HOME} -s /bin/bash -g "${PGID}" -u "${PUID}" "${UNAME}" +mkdir -p ${HOME}/.config/sunshine +ln -s ${HOME}/.config/sunshine /config +chown -R ${UNAME} ${HOME} +_SETUP_USER + +USER ${UNAME} +WORKDIR ${HOME} + +# entrypoint +ENTRYPOINT ["/usr/bin/sunshine"] diff --git a/docker/debian-bullseye.dockerfile b/docker/debian-bullseye.dockerfile index 7c4396184ae..2a491559083 100644 --- a/docker/debian-bullseye.dockerfile +++ b/docker/debian-bullseye.dockerfile @@ -31,18 +31,22 @@ set -e apt-get update -y apt-get install -y --no-install-recommends \ build-essential \ + ca-certificates \ cmake=3.18.* \ + doxygen \ git \ - libavdevice-dev \ + graphviz \ + libayatana-appindicator3-dev \ libboost-filesystem-dev=1.74.* \ libboost-locale-dev=1.74.* \ libboost-log-dev=1.74.* \ libboost-program-options-dev=1.74.* \ - libboost-thread-dev=1.74.* \ libcap-dev \ libcurl4-openssl-dev \ libdrm-dev \ libevdev-dev \ + libminiupnpc-dev \ + libnotify-dev \ libnuma-dev \ libopus-dev \ libpulse-dev \ @@ -57,9 +61,12 @@ apt-get install -y --no-install-recommends \ libxfixes-dev \ libxrandr-dev \ libxtst-dev \ - nodejs \ - npm \ - wget + python3.9 \ + python3.9-venv \ + udev \ + wget \ + x11-xserver-utils \ + xvfb if [[ "${TARGETPLATFORM}" == 'linux/amd64' ]]; then apt-get install -y --no-install-recommends \ libmfx-dev @@ -68,6 +75,17 @@ apt-get clean rm -rf /var/lib/apt/lists/* _DEPS +#Install Node +# hadolint ignore=SC1091 +RUN <<_INSTALL_NODE +#!/bin/bash +set -e +wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash +source "$HOME/.nvm/nvm.sh" +nvm install 20.9.0 +nvm use 20.9.0 +_INSTALL_NODE + # install cuda WORKDIR /build/cuda # versions: https://developer.nvidia.com/cuda-toolkit-archive @@ -94,17 +112,19 @@ _INSTALL_CUDA WORKDIR /build/sunshine/ COPY --link .. . -# setup npm dependencies -RUN npm install - # setup build directory WORKDIR /build/sunshine/build # cmake and cpack +# hadolint ignore=SC1091 RUN <<_MAKE #!/bin/bash set -e +#Set Node version +source "$HOME/.nvm/nvm.sh" +nvm use 20.9.0 cmake \ + -DBUILD_WERROR=ON \ -DCMAKE_CUDA_COMPILER:PATH=/build/cuda/bin/nvcc \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_INSTALL_PREFIX=/usr \ @@ -119,6 +139,17 @@ make -j "$(nproc)" cpack -G DEB _MAKE +# run tests +WORKDIR /build/sunshine/build/tests +# hadolint ignore=SC1091 +RUN <<_TEST +#!/bin/bash +set -e +export DISPLAY=:1 +Xvfb ${DISPLAY} -screen 0 1024x768x24 & +./test_sunshine --gtest_color=yes +_TEST + FROM scratch AS artifacts ARG BASE ARG TAG diff --git a/docker/fedora-37.dockerfile b/docker/fedora-39.dockerfile similarity index 78% rename from docker/fedora-37.dockerfile rename to docker/fedora-39.dockerfile index b05f8cb1c21..262b40fc7ac 100644 --- a/docker/fedora-37.dockerfile +++ b/docker/fedora-39.dockerfile @@ -1,10 +1,10 @@ # syntax=docker/dockerfile:1.4 # artifacts: true -# platforms: linux/amd64,linux/arm64/v8 +# platforms: linux/amd64 # platforms_pr: linux/amd64 # no-cache-filters: sunshine-base,artifacts,sunshine ARG BASE=fedora -ARG TAG=37 +ARG TAG=39 FROM ${BASE}:${TAG} AS sunshine-base FROM sunshine-base as sunshine-build @@ -30,16 +30,19 @@ set -e dnf -y update dnf -y group install "Development Tools" dnf -y install \ - boost-devel-1.78.* \ - cmake-3.26.* \ - gcc-12.2.* \ - gcc-c++-12.2.* \ + boost-devel-1.81.0* \ + cmake-3.27.* \ + doxygen \ + gcc-13.2.* \ + gcc-c++-13.2.* \ git \ + graphviz \ libappindicator-gtk3-devel \ libcap-devel \ libcurl-devel \ libdrm-devel \ libevdev-devel \ + libnotify-devel \ libva-devel \ libvdpau-devel \ libX11-devel \ @@ -51,14 +54,17 @@ dnf -y install \ libXrandr-devel \ libXtst-devel \ mesa-libGL-devel \ - nodejs-npm \ + miniupnpc-devel \ + nodejs \ numactl-devel \ openssl-devel \ opus-devel \ pulseaudio-libs-devel \ + python3.11 \ rpm-build \ wget \ - which + which \ + xorg-x11-server-Xvfb if [[ "${TARGETPLATFORM}" == 'linux/amd64' ]]; then dnf -y install intel-mediasdk-devel fi @@ -69,8 +75,8 @@ _DEPS # install cuda WORKDIR /build/cuda # versions: https://developer.nvidia.com/cuda-toolkit-archive -ENV CUDA_VERSION="12.0.0" -ENV CUDA_BUILD="525.60.13" +ENV CUDA_VERSION="12.4.0" +ENV CUDA_BUILD="550.54.14" # hadolint ignore=SC3010 RUN <<_INSTALL_CUDA #!/bin/bash @@ -79,6 +85,13 @@ cuda_prefix="https://developer.download.nvidia.com/compute/cuda/" cuda_suffix="" if [[ "${TARGETPLATFORM}" == 'linux/arm64' ]]; then cuda_suffix="_sbsa" + + # patch headers https://bugs.launchpad.net/ubuntu/+source/mumax3/+bug/2032624 + sed -i 's/__Float32x4_t/int/g' /usr/include/bits/math-vector.h + sed -i 's/__Float64x2_t/int/g' /usr/include/bits/math-vector.h + sed -i 's/__SVFloat32_t/float/g' /usr/include/bits/math-vector.h + sed -i 's/__SVFloat64_t/float/g' /usr/include/bits/math-vector.h + sed -i 's/__SVBool_t/int/g' /usr/include/bits/math-vector.h fi url="${cuda_prefix}${CUDA_VERSION}/local_installers/cuda_${CUDA_VERSION}_${CUDA_BUILD}_linux${cuda_suffix}.run" echo "cuda url: ${url}" @@ -92,9 +105,6 @@ _INSTALL_CUDA WORKDIR /build/sunshine/ COPY --link .. . -# setup npm dependencies -RUN npm install - # setup build directory WORKDIR /build/sunshine/build @@ -104,6 +114,7 @@ RUN <<_MAKE set -e cmake \ -DCMAKE_CUDA_COMPILER:PATH=/build/cuda/bin/nvcc \ + -DBUILD_WERROR=ON \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_INSTALL_PREFIX=/usr \ -DSUNSHINE_ASSETS_DIR=share/sunshine \ @@ -117,6 +128,17 @@ make -j "$(nproc)" cpack -G RPM _MAKE +# run tests +WORKDIR /build/sunshine/build/tests +# hadolint ignore=SC1091 +RUN <<_TEST +#!/bin/bash +set -e +export DISPLAY=:1 +Xvfb ${DISPLAY} -screen 0 1024x768x24 & +./test_sunshine --gtest_color=yes +_TEST + FROM scratch AS artifacts ARG BASE ARG TAG diff --git a/docker/fedora-38.dockerfile b/docker/fedora-40.dockerfile similarity index 75% rename from docker/fedora-38.dockerfile rename to docker/fedora-40.dockerfile index 9635f192cc7..94a8a9fa733 100644 --- a/docker/fedora-38.dockerfile +++ b/docker/fedora-40.dockerfile @@ -1,10 +1,10 @@ # syntax=docker/dockerfile:1.4 # artifacts: true -# platforms: linux/amd64,linux/arm64/v8 +# platforms: linux/amd64 # platforms_pr: linux/amd64 # no-cache-filters: sunshine-base,artifacts,sunshine ARG BASE=fedora -ARG TAG=38 +ARG TAG=40 FROM ${BASE}:${TAG} AS sunshine-base FROM sunshine-base as sunshine-build @@ -30,16 +30,19 @@ set -e dnf -y update dnf -y group install "Development Tools" dnf -y install \ - boost-devel-1.78.0* \ - cmake-3.26.* \ - gcc-13.0.* \ - gcc-c++-13.0.* \ + boost-devel-1.83.0* \ + cmake-3.28.* \ + doxygen \ + gcc-14.1.* \ + gcc-c++-14.1.* \ git \ + graphviz \ libappindicator-gtk3-devel \ libcap-devel \ libcurl-devel \ libdrm-devel \ libevdev-devel \ + libnotify-devel \ libva-devel \ libvdpau-devel \ libX11-devel \ @@ -51,14 +54,17 @@ dnf -y install \ libXrandr-devel \ libXtst-devel \ mesa-libGL-devel \ - nodejs-npm \ + miniupnpc-devel \ + nodejs \ numactl-devel \ openssl-devel \ opus-devel \ pulseaudio-libs-devel \ + python3.11 \ rpm-build \ wget \ - which + which \ + xorg-x11-server-Xvfb if [[ "${TARGETPLATFORM}" == 'linux/amd64' ]]; then dnf -y install intel-mediasdk-devel fi @@ -66,12 +72,12 @@ dnf clean all rm -rf /var/cache/yum _DEPS -# todo - enable cuda once it's supported for gcc 13 and fedora 38 +# TODO: re-enable cuda once cuda supports gcc-14 ## install cuda #WORKDIR /build/cuda ## versions: https://developer.nvidia.com/cuda-toolkit-archive -#ENV CUDA_VERSION="12.0.0" -#ENV CUDA_BUILD="525.60.13" +#ENV CUDA_VERSION="12.4.0" +#ENV CUDA_BUILD="550.54.14" ## hadolint ignore=SC3010 #RUN <<_INSTALL_CUDA ##!/bin/bash @@ -80,6 +86,13 @@ _DEPS #cuda_suffix="" #if [[ "${TARGETPLATFORM}" == 'linux/arm64' ]]; then # cuda_suffix="_sbsa" +# +# # patch headers https://bugs.launchpad.net/ubuntu/+source/mumax3/+bug/2032624 +# sed -i 's/__Float32x4_t/int/g' /usr/include/bits/math-vector.h +# sed -i 's/__Float64x2_t/int/g' /usr/include/bits/math-vector.h +# sed -i 's/__SVFloat32_t/float/g' /usr/include/bits/math-vector.h +# sed -i 's/__SVFloat64_t/float/g' /usr/include/bits/math-vector.h +# sed -i 's/__SVBool_t/int/g' /usr/include/bits/math-vector.h #fi #url="${cuda_prefix}${CUDA_VERSION}/local_installers/cuda_${CUDA_VERSION}_${CUDA_BUILD}_linux${cuda_suffix}.run" #echo "cuda url: ${url}" @@ -93,19 +106,17 @@ _DEPS WORKDIR /build/sunshine/ COPY --link .. . -# setup npm dependencies -RUN npm install - # setup build directory WORKDIR /build/sunshine/build +# TODO: re-add as first cmake argument: -DCMAKE_CUDA_COMPILER:PATH=/build/cuda/bin/nvcc \ +# TODO: enable cuda flag # cmake and cpack -# todo - add cmake argument back in for cuda support "-DCMAKE_CUDA_COMPILER:PATH=/build/cuda/bin/nvcc \" -# todo - re-enable "DSUNSHINE_ENABLE_CUDA" RUN <<_MAKE #!/bin/bash set -e cmake \ + -DBUILD_WERROR=ON \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_INSTALL_PREFIX=/usr \ -DSUNSHINE_ASSETS_DIR=share/sunshine \ @@ -119,6 +130,17 @@ make -j "$(nproc)" cpack -G RPM _MAKE +# run tests +WORKDIR /build/sunshine/build/tests +# hadolint ignore=SC1091 +RUN <<_TEST +#!/bin/bash +set -e +export DISPLAY=:1 +Xvfb ${DISPLAY} -screen 0 1024x768x24 & +./test_sunshine --gtest_color=yes +_TEST + FROM scratch AS artifacts ARG BASE ARG TAG diff --git a/docker/ubuntu-22.04.dockerfile b/docker/ubuntu-22.04.dockerfile index 143fc1e4b24..e02ca1eba91 100644 --- a/docker/ubuntu-22.04.dockerfile +++ b/docker/ubuntu-22.04.dockerfile @@ -32,18 +32,21 @@ apt-get update -y apt-get install -y --no-install-recommends \ build-essential \ cmake=3.22.* \ + ca-certificates \ + doxygen \ git \ - libappindicator3-dev \ - libavdevice-dev \ + graphviz \ + libayatana-appindicator3-dev \ libboost-filesystem-dev=1.74.* \ libboost-locale-dev=1.74.* \ libboost-log-dev=1.74.* \ libboost-program-options-dev=1.74.* \ - libboost-thread-dev=1.74.* \ libcap-dev \ libcurl4-openssl-dev \ libdrm-dev \ libevdev-dev \ + libminiupnpc-dev \ + libnotify-dev \ libnuma-dev \ libopus-dev \ libpulse-dev \ @@ -58,9 +61,12 @@ apt-get install -y --no-install-recommends \ libxfixes-dev \ libxrandr-dev \ libxtst-dev \ - nodejs \ - npm \ - wget + python3.10 \ + python3.10-venv \ + udev \ + wget \ + x11-xserver-utils \ + xvfb if [[ "${TARGETPLATFORM}" == 'linux/amd64' ]]; then apt-get install -y --no-install-recommends \ libmfx-dev @@ -69,6 +75,17 @@ apt-get clean rm -rf /var/lib/apt/lists/* _DEPS +#Install Node +# hadolint ignore=SC1091 +RUN <<_INSTALL_NODE +#!/bin/bash +set -e +wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash +source "$HOME/.nvm/nvm.sh" +nvm install 20.9.0 +nvm use 20.9.0 +_INSTALL_NODE + # install cuda WORKDIR /build/cuda # versions: https://developer.nvidia.com/cuda-toolkit-archive @@ -95,17 +112,20 @@ _INSTALL_CUDA WORKDIR /build/sunshine/ COPY --link .. . -# setup npm dependencies -RUN npm install - # setup build directory WORKDIR /build/sunshine/build # cmake and cpack +# hadolint ignore=SC1091 RUN <<_MAKE #!/bin/bash set -e +#Set Node version +source "$HOME/.nvm/nvm.sh" +nvm use 20.9.0 +#Actually build cmake \ + -DBUILD_WERROR=ON \ -DCMAKE_CUDA_COMPILER:PATH=/build/cuda/bin/nvcc \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_INSTALL_PREFIX=/usr \ @@ -120,6 +140,17 @@ make -j "$(nproc)" cpack -G DEB _MAKE +# run tests +WORKDIR /build/sunshine/build/tests +# hadolint ignore=SC1091 +RUN <<_TEST +#!/bin/bash +set -e +export DISPLAY=:1 +Xvfb ${DISPLAY} -screen 0 1024x768x24 & +./test_sunshine --gtest_color=yes +_TEST + FROM scratch AS artifacts ARG BASE ARG TAG diff --git a/docker/ubuntu-20.04.dockerfile b/docker/ubuntu-24.04.dockerfile similarity index 73% rename from docker/ubuntu-20.04.dockerfile rename to docker/ubuntu-24.04.dockerfile index 5689ba7134b..7ef83bfba39 100644 --- a/docker/ubuntu-20.04.dockerfile +++ b/docker/ubuntu-24.04.dockerfile @@ -1,10 +1,10 @@ # syntax=docker/dockerfile:1.4 # artifacts: true -# platforms: linux/amd64,linux/arm64/v8 +# platforms: linux/amd64 # platforms_pr: linux/amd64 # no-cache-filters: sunshine-base,artifacts,sunshine ARG BASE=ubuntu -ARG TAG=20.04 +ARG TAG=24.04 FROM ${BASE}:${TAG} AS sunshine-base ENV DEBIAN_FRONTEND=noninteractive @@ -31,20 +31,24 @@ set -e apt-get update -y apt-get install -y --no-install-recommends \ build-essential \ - gcc-10=10.3.* \ - g++-10=10.3.* \ + cmake=3.28.* \ + ca-certificates \ + doxygen \ + gcc-11 \ + g++-11 \ git \ - libappindicator3-dev \ - libavdevice-dev \ - libboost-filesystem-dev=1.71.* \ - libboost-locale-dev=1.71.* \ - libboost-log-dev=1.71.* \ - libboost-program-options-dev=1.71.* \ - libboost-thread-dev=1.71.* \ + graphviz \ + libayatana-appindicator3-dev \ + libboost-filesystem-dev=1.83.* \ + libboost-locale-dev=1.83.* \ + libboost-log-dev=1.83.* \ + libboost-program-options-dev=1.83.* \ libcap-dev \ libcurl4-openssl-dev \ libdrm-dev \ libevdev-dev \ + libminiupnpc-dev \ + libnotify-dev \ libnuma-dev \ libopus-dev \ libpulse-dev \ @@ -59,9 +63,12 @@ apt-get install -y --no-install-recommends \ libxfixes-dev \ libxrandr-dev \ libxtst-dev \ - nodejs \ - npm \ - wget + python3.12 \ + python3.12-venv \ + udev \ + wget \ + x11-xserver-utils \ + xvfb if [[ "${TARGETPLATFORM}" == 'linux/amd64' ]]; then apt-get install -y --no-install-recommends \ libmfx-dev @@ -70,41 +77,31 @@ apt-get clean rm -rf /var/lib/apt/lists/* _DEPS + +#Install Node +# hadolint ignore=SC1091 +RUN <<_INSTALL_NODE +#!/bin/bash +set -e +wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash +source "$HOME/.nvm/nvm.sh" +nvm install 20.9.0 +nvm use 20.9.0 +_INSTALL_NODE + # Update gcc alias # https://stackoverflow.com/a/70653945/11214013 RUN <<_GCC_ALIAS #!/bin/bash set -e update-alternatives --install \ - /usr/bin/gcc gcc /usr/bin/gcc-10 100 \ - --slave /usr/bin/g++ g++ /usr/bin/g++-10 \ - --slave /usr/bin/gcov gcov /usr/bin/gcov-10 \ - --slave /usr/bin/gcc-ar gcc-ar /usr/bin/gcc-ar-10 \ - --slave /usr/bin/gcc-ranlib gcc-ranlib /usr/bin/gcc-ranlib-10 + /usr/bin/gcc gcc /usr/bin/gcc-11 100 \ + --slave /usr/bin/g++ g++ /usr/bin/g++-11 \ + --slave /usr/bin/gcov gcov /usr/bin/gcov-11 \ + --slave /usr/bin/gcc-ar gcc-ar /usr/bin/gcc-ar-11 \ + --slave /usr/bin/gcc-ranlib gcc-ranlib /usr/bin/gcc-ranlib-11 _GCC_ALIAS -# install cmake -# sunshine requires cmake >= 3.18 -WORKDIR /build/cmake -# https://cmake.org/download/ -ENV CMAKE_VERSION="3.25.1" -# hadolint ignore=SC3010 -RUN <<_INSTALL_CMAKE -#!/bin/bash -set -e -cmake_prefix="https://github.com/Kitware/CMake/releases/download/v" -if [[ "${TARGETPLATFORM}" == 'linux/amd64' ]]; then - cmake_arch="x86_64" -elif [[ "${TARGETPLATFORM}" == 'linux/arm64' ]]; then - cmake_arch="aarch64" -fi -url="${cmake_prefix}${CMAKE_VERSION}/cmake-${CMAKE_VERSION}-linux-${cmake_arch}.sh" -echo "cmake url: ${url}" -wget "$url" --progress=bar:force:noscroll -q --show-progress -O ./cmake.sh -sh ./cmake.sh --prefix=/usr/local --skip-license -cmake --version -_INSTALL_CMAKE - # install cuda WORKDIR /build/cuda # versions: https://developer.nvidia.com/cuda-toolkit-archive @@ -131,17 +128,20 @@ _INSTALL_CUDA WORKDIR /build/sunshine/ COPY --link .. . -# setup npm dependencies -RUN npm install - # setup build directory WORKDIR /build/sunshine/build # cmake and cpack +# hadolint ignore=SC1091 RUN <<_MAKE #!/bin/bash set -e +#Set Node version +source "$HOME/.nvm/nvm.sh" +nvm use 20.9.0 +#Actually build cmake \ + -DBUILD_WERROR=ON \ -DCMAKE_CUDA_COMPILER:PATH=/build/cuda/bin/nvcc \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_INSTALL_PREFIX=/usr \ @@ -156,6 +156,17 @@ make -j "$(nproc)" cpack -G DEB _MAKE +# run tests +WORKDIR /build/sunshine/build/tests +# hadolint ignore=SC1091 +RUN <<_TEST +#!/bin/bash +set -e +export DISPLAY=:1 +Xvfb ${DISPLAY} -screen 0 1024x768x24 & +./test_sunshine --gtest_color=yes +_TEST + FROM scratch AS artifacts ARG BASE ARG TAG @@ -183,9 +194,9 @@ EXPOSE 48010 EXPOSE 47998-48000/udp # setup user -ARG PGID=1000 +ARG PGID=1001 ENV PGID=${PGID} -ARG PUID=1000 +ARG PUID=1001 ENV PUID=${PUID} ENV TZ="UTC" ARG UNAME=lizard diff --git a/docs/Doxyfile b/docs/Doxyfile index 5d909b5335e..d6aa47edd51 100644 --- a/docs/Doxyfile +++ b/docs/Doxyfile @@ -1,4 +1,4 @@ -# Doxyfile 1.9.6 +# Doxyfile 1.10.0 # This file describes the settings to be used by the documentation system # doxygen (www.doxygen.org) for a project. @@ -42,7 +42,7 @@ DOXYFILE_ENCODING = UTF-8 # title of most generated pages and in a few other places. # The default value is: My Project. -PROJECT_NAME = "Sunshine" +PROJECT_NAME = Sunshine # The PROJECT_NUMBER tag can be used to enter a project or revision number. This # could be handy for archiving the generated documentation or if some version @@ -63,6 +63,12 @@ PROJECT_BRIEF = "Sunshine is a Gamestream host for Moonlight." PROJECT_LOGO = ../sunshine.png +# With the PROJECT_ICON tag one can specify an icon that is included in the tabs +# when the HTML document is shown. Doxygen will copy the logo to the output +# directory. + +PROJECT_ICON = + # The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path # into which the generated documentation will be written. If a relative path is # entered, it will be relative to the location where doxygen was started. If @@ -365,6 +371,17 @@ MARKDOWN_SUPPORT = YES TOC_INCLUDE_HEADINGS = 5 +# The MARKDOWN_ID_STYLE tag can be used to specify the algorithm used to +# generate identifiers for the Markdown headings. Note: Every identifier is +# unique. +# Possible values are: DOXYGEN use a fixed 'autotoc_md' string followed by a +# sequence number starting at 0 and GITHUB use the lower case version of title +# with any whitespace replaced by '-' and punctuation characters removed. +# The default value is: DOXYGEN. +# This tag requires that the tag MARKDOWN_SUPPORT is set to YES. + +MARKDOWN_ID_STYLE = DOXYGEN + # When enabled doxygen tries to link words that correspond to documented # classes, or namespaces to their corresponding documentation. Such a link can # be prevented in individual cases by putting a % sign in front of the word or @@ -489,6 +506,14 @@ LOOKUP_CACHE_SIZE = 0 NUM_PROC_THREADS = 0 +# If the TIMESTAMP tag is set different from NO then each generated page will +# contain the date or date and time when the page was generated. Setting this to +# NO can help when comparing the output of multiple runs. +# Possible values are: YES, NO, DATETIME and DATE. +# The default value is: NO. + +TIMESTAMP = NO + #--------------------------------------------------------------------------- # Build related configuration options #--------------------------------------------------------------------------- @@ -874,7 +899,14 @@ WARN_IF_UNDOC_ENUM_VAL = NO # a warning is encountered. If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS # then doxygen will continue running as if WARN_AS_ERROR tag is set to NO, but # at the end of the doxygen process doxygen will return with a non-zero status. -# Possible values are: NO, YES and FAIL_ON_WARNINGS. +# If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS_PRINT then doxygen behaves +# like FAIL_ON_WARNINGS but in case no WARN_LOGFILE is defined doxygen will not +# write the warning messages in between other messages but write them at the end +# of a run, in case a WARN_LOGFILE is defined the warning messages will be +# besides being in the defined file also be shown at the end of a run, unless +# the WARN_LOGFILE is defined as - i.e. standard output (stdout) in that case +# the behavior will remain as with the setting FAIL_ON_WARNINGS. +# Possible values are: NO, YES, FAIL_ON_WARNINGS and FAIL_ON_WARNINGS_PRINT. # The default value is: NO. WARN_AS_ERROR = NO @@ -953,12 +985,12 @@ INPUT_FILE_ENCODING = # Note the list of default checked file patterns might differ from the list of # default file extension mappings. # -# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cpp, -# *.c++, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h, -# *.hh, *.hxx, *.hpp, *.h++, *.l, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, -# *.inc, *.m, *.markdown, *.md, *.mm, *.dox (to be provided as doxygen C -# comment), *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, *.f18, *.f, *.for, *.vhd, -# *.vhdl, *.ucf, *.qsf and *.ice. +# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cxxm, +# *.cpp, *.cppm, *.ccm, *.c++, *.c++m, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, +# *.idl, *.ddl, *.odl, *.h, *.hh, *.hxx, *.hpp, *.h++, *.ixx, *.l, *.cs, *.d, +# *.php, *.php4, *.php5, *.phtml, *.inc, *.m, *.markdown, *.md, *.mm, *.dox (to +# be provided as doxygen C comment), *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, +# *.f18, *.f, *.for, *.vhd, *.vhdl, *.ucf, *.qsf and *.ice. FILE_PATTERNS = *.c \ *.cc \ @@ -1043,9 +1075,6 @@ EXCLUDE_PATTERNS = # output. The symbol name can be a fully qualified name, a word, or if the # wildcard * is used, a substring. Examples: ANamespace, AClass, # ANamespace::AClass, ANamespace::*Test -# -# Note that the wildcards are matched against the file with absolute path, so to -# exclude all test directories use the pattern */test/* EXCLUDE_SYMBOLS = @@ -1159,7 +1188,8 @@ FORTRAN_COMMENT_AFTER = 72 SOURCE_BROWSER = NO # Setting the INLINE_SOURCES tag to YES will include the body of functions, -# classes and enums directly into the documentation. +# multi-line macros, enums or list initialized variables directly into the +# documentation. # The default value is: NO. INLINE_SOURCES = NO @@ -1428,15 +1458,6 @@ HTML_COLORSTYLE_SAT = 100 HTML_COLORSTYLE_GAMMA = 80 -# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML -# page will contain the date and time when the page was generated. Setting this -# to YES can help to show when doxygen was last run and thus if the -# documentation is up to date. -# The default value is: NO. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_TIMESTAMP = NO - # If the HTML_DYNAMIC_MENUS tag is set to YES then the generated HTML # documentation will contain a main index with vertical navigation menus that # are dynamically created via JavaScript. If disabled, the navigation index will @@ -1456,6 +1477,33 @@ HTML_DYNAMIC_MENUS = YES HTML_DYNAMIC_SECTIONS = NO +# If the HTML_CODE_FOLDING tag is set to YES then classes and functions can be +# dynamically folded and expanded in the generated HTML source code. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_CODE_FOLDING = YES + +# If the HTML_COPY_CLIPBOARD tag is set to YES then doxygen will show an icon in +# the top right corner of code and text fragments that allows the user to copy +# its content to the clipboard. Note this only works if supported by the browser +# and the web page is served via a secure context (see: +# https://www.w3.org/TR/secure-contexts/), i.e. using the https: or file: +# protocol. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COPY_CLIPBOARD = YES + +# Doxygen stores a couple of settings persistently in the browser (via e.g. +# cookies). By default these settings apply to all HTML pages generated by +# doxygen across all projects. The HTML_PROJECT_COOKIE tag can be used to store +# the settings under a project specific key, such that the user preferences will +# be stored separately. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_PROJECT_COOKIE = + # With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries # shown in the various tree structured indices initially; the user can expand # and collapse entries dynamically later on. Doxygen will expand the tree to @@ -1586,6 +1634,16 @@ BINARY_TOC = NO TOC_EXPAND = NO +# The SITEMAP_URL tag is used to specify the full URL of the place where the +# generated documentation will be placed on the server by the user during the +# deployment of the documentation. The generated sitemap is called sitemap.xml +# and placed on the directory specified by HTML_OUTPUT. In case no SITEMAP_URL +# is specified no sitemap is generated. For information about the sitemap +# protocol see https://www.sitemaps.org +# This tag requires that the tag GENERATE_HTML is set to YES. + +SITEMAP_URL = + # If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and # QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that # can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help @@ -2074,9 +2132,16 @@ PDF_HYPERLINKS = YES USE_PDFLATEX = YES -# If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \batchmode -# command to the generated LaTeX files. This will instruct LaTeX to keep running -# if errors occur, instead of asking the user for help. +# The LATEX_BATCHMODE tag signals the behavior of LaTeX in case of an error. +# Possible values are: NO same as ERROR_STOP, YES same as BATCH, BATCH In batch +# mode nothing is printed on the terminal, errors are scrolled as if is +# hit at every error; missing files that TeX tries to input or request from +# keyboard input (\read on a not open input stream) cause the job to abort, +# NON_STOP In nonstop mode the diagnostic message will appear on the terminal, +# but there is no possibility of user interaction just like in batch mode, +# SCROLL In scroll mode, TeX will stop only for missing files to input or if +# keyboard input is necessary and ERROR_STOP In errorstop mode, TeX will stop at +# each error, asking for user intervention. # The default value is: NO. # This tag requires that the tag GENERATE_LATEX is set to YES. @@ -2097,14 +2162,6 @@ LATEX_HIDE_INDICES = NO LATEX_BIB_STYLE = plain -# If the LATEX_TIMESTAMP tag is set to YES then the footer of each generated -# page will contain the date and time when the page was generated. Setting this -# to NO can help when comparing the output of multiple runs. -# The default value is: NO. -# This tag requires that the tag GENERATE_LATEX is set to YES. - -LATEX_TIMESTAMP = NO - # The LATEX_EMOJI_DIRECTORY tag is used to specify the (relative or absolute) # path from which the emoji images will be read. If a relative path is entered, # it will be relative to the LATEX_OUTPUT directory. If left blank the @@ -2270,13 +2327,39 @@ DOCBOOK_OUTPUT = doxydocbook #--------------------------------------------------------------------------- # If the GENERATE_AUTOGEN_DEF tag is set to YES, doxygen will generate an -# AutoGen Definitions (see http://autogen.sourceforge.net/) file that captures +# AutoGen Definitions (see https://autogen.sourceforge.net/) file that captures # the structure of the code including all documentation. Note that this feature # is still experimental and incomplete at the moment. # The default value is: NO. GENERATE_AUTOGEN_DEF = NO +#--------------------------------------------------------------------------- +# Configuration options related to Sqlite3 output +#--------------------------------------------------------------------------- + +# If the GENERATE_SQLITE3 tag is set to YES doxygen will generate a Sqlite3 +# database with symbols found by doxygen stored in tables. +# The default value is: NO. + +GENERATE_SQLITE3 = NO + +# The SQLITE3_OUTPUT tag is used to specify where the Sqlite3 database will be +# put. If a relative path is entered the value of OUTPUT_DIRECTORY will be put +# in front of it. +# The default directory is: sqlite3. +# This tag requires that the tag GENERATE_SQLITE3 is set to YES. + +SQLITE3_OUTPUT = sqlite3 + +# The SQLITE3_RECREATE_DB tag is set to YES, the existing doxygen_sqlite3.db +# database file will be recreated with each doxygen run. If set to NO, doxygen +# will warn if a database file is already found and not modify it. +# The default value is: YES. +# This tag requires that the tag GENERATE_SQLITE3 is set to YES. + +SQLITE3_RECREATE_DB = YES + #--------------------------------------------------------------------------- # Configuration options related to the Perl module output #--------------------------------------------------------------------------- @@ -2355,7 +2438,7 @@ SEARCH_INCLUDES = YES # RECURSIVE has no effect here. # This tag requires that the tag SEARCH_INCLUDES is set to YES. -INCLUDE_PATH = ../third-party/ffmpeg-linux-x86_64/include/ +INCLUDE_PATH = ../third-party/build-deps/ffmpeg/linux-x86_64/include/ # You can use the INCLUDE_FILE_PATTERNS tag to specify one or more wildcard # patterns (like *.h and *.hpp) to filter out the header-files in the @@ -2419,15 +2502,15 @@ TAGFILES = GENERATE_TAGFILE = -# If the ALLEXTERNALS tag is set to YES, all external class will be listed in -# the class index. If set to NO, only the inherited external classes will be -# listed. +# If the ALLEXTERNALS tag is set to YES, all external classes and namespaces +# will be listed in the class and namespace index. If set to NO, only the +# inherited external classes will be listed. # The default value is: NO. ALLEXTERNALS = NO # If the EXTERNAL_GROUPS tag is set to YES, all external groups will be listed -# in the modules index. If set to NO, only the current project's groups will be +# in the topic index. If set to NO, only the current project's groups will be # listed. # The default value is: YES. @@ -2441,16 +2524,9 @@ EXTERNAL_GROUPS = YES EXTERNAL_PAGES = YES #--------------------------------------------------------------------------- -# Configuration options related to the dot tool +# Configuration options related to diagram generator tools #--------------------------------------------------------------------------- -# You can include diagrams made with dia in doxygen documentation. Doxygen will -# then run dia to produce the diagram and insert it in the documentation. The -# DIA_PATH tag allows you to specify the directory where the dia binary resides. -# If left empty dia is assumed to be found in the default search path. - -DIA_PATH = - # If set to YES the inheritance and collaboration graphs will hide inheritance # and usage relations if the target is undocumented or is not a class. # The default value is: YES. @@ -2459,7 +2535,7 @@ HIDE_UNDOC_RELATIONS = YES # If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is # available from the path. This tool is part of Graphviz (see: -# http://www.graphviz.org/), a graph visualization toolkit from AT&T and Lucent +# https://www.graphviz.org/), a graph visualization toolkit from AT&T and Lucent # Bell Labs. The other options in this section have no effect if this option is # set to NO # The default value is: NO. @@ -2512,13 +2588,19 @@ DOT_NODE_ATTR = "shape=box,height=0.2,width=0.4" DOT_FONTPATH = -# If the CLASS_GRAPH tag is set to YES (or GRAPH) then doxygen will generate a -# graph for each documented class showing the direct and indirect inheritance -# relations. In case HAVE_DOT is set as well dot will be used to draw the graph, -# otherwise the built-in generator will be used. If the CLASS_GRAPH tag is set -# to TEXT the direct and indirect inheritance relations will be shown as texts / -# links. -# Possible values are: NO, YES, TEXT and GRAPH. +# If the CLASS_GRAPH tag is set to YES or GRAPH or BUILTIN then doxygen will +# generate a graph for each documented class showing the direct and indirect +# inheritance relations. In case the CLASS_GRAPH tag is set to YES or GRAPH and +# HAVE_DOT is enabled as well, then dot will be used to draw the graph. In case +# the CLASS_GRAPH tag is set to YES and HAVE_DOT is disabled or if the +# CLASS_GRAPH tag is set to BUILTIN, then the built-in generator will be used. +# If the CLASS_GRAPH tag is set to TEXT the direct and indirect inheritance +# relations will be shown as texts / links. Explicit enabling an inheritance +# graph or choosing a different representation for an inheritance graph of a +# specific class, can be accomplished by means of the command \inheritancegraph. +# Disabling an inheritance graph can be accomplished by means of the command +# \hideinheritancegraph. +# Possible values are: NO, YES, TEXT, GRAPH and BUILTIN. # The default value is: YES. CLASS_GRAPH = YES @@ -2526,15 +2608,21 @@ CLASS_GRAPH = YES # If the COLLABORATION_GRAPH tag is set to YES then doxygen will generate a # graph for each documented class showing the direct and indirect implementation # dependencies (inheritance, containment, and class references variables) of the -# class with other documented classes. +# class with other documented classes. Explicit enabling a collaboration graph, +# when COLLABORATION_GRAPH is set to NO, can be accomplished by means of the +# command \collaborationgraph. Disabling a collaboration graph can be +# accomplished by means of the command \hidecollaborationgraph. # The default value is: YES. # This tag requires that the tag HAVE_DOT is set to YES. COLLABORATION_GRAPH = YES # If the GROUP_GRAPHS tag is set to YES then doxygen will generate a graph for -# groups, showing the direct groups dependencies. See also the chapter Grouping -# in the manual. +# groups, showing the direct groups dependencies. Explicit enabling a group +# dependency graph, when GROUP_GRAPHS is set to NO, can be accomplished by means +# of the command \groupgraph. Disabling a directory graph can be accomplished by +# means of the command \hidegroupgraph. See also the chapter Grouping in the +# manual. # The default value is: YES. # This tag requires that the tag HAVE_DOT is set to YES. @@ -2576,8 +2664,8 @@ DOT_UML_DETAILS = NO # The DOT_WRAP_THRESHOLD tag can be used to set the maximum number of characters # to display on a single line. If the actual line length exceeds this threshold -# significantly it will wrapped across multiple lines. Some heuristics are apply -# to avoid ugly line breaks. +# significantly it will be wrapped across multiple lines. Some heuristics are +# applied to avoid ugly line breaks. # Minimum value: 0, maximum value: 1000, default value: 17. # This tag requires that the tag HAVE_DOT is set to YES. @@ -2594,7 +2682,9 @@ TEMPLATE_RELATIONS = NO # If the INCLUDE_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are set to # YES then doxygen will generate a graph for each documented file showing the # direct and indirect include dependencies of the file with other documented -# files. +# files. Explicit enabling an include graph, when INCLUDE_GRAPH is is set to NO, +# can be accomplished by means of the command \includegraph. Disabling an +# include graph can be accomplished by means of the command \hideincludegraph. # The default value is: YES. # This tag requires that the tag HAVE_DOT is set to YES. @@ -2603,7 +2693,10 @@ INCLUDE_GRAPH = YES # If the INCLUDED_BY_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are # set to YES then doxygen will generate a graph for each documented file showing # the direct and indirect include dependencies of the file with other documented -# files. +# files. Explicit enabling an included by graph, when INCLUDED_BY_GRAPH is set +# to NO, can be accomplished by means of the command \includedbygraph. Disabling +# an included by graph can be accomplished by means of the command +# \hideincludedbygraph. # The default value is: YES. # This tag requires that the tag HAVE_DOT is set to YES. @@ -2643,7 +2736,10 @@ GRAPHICAL_HIERARCHY = YES # If the DIRECTORY_GRAPH tag is set to YES then doxygen will show the # dependencies a directory has on other directories in a graphical way. The # dependency relations are determined by the #include relations between the -# files in the directories. +# files in the directories. Explicit enabling a directory graph, when +# DIRECTORY_GRAPH is set to NO, can be accomplished by means of the command +# \directorygraph. Disabling a directory graph can be accomplished by means of +# the command \hidedirectorygraph. # The default value is: YES. # This tag requires that the tag HAVE_DOT is set to YES. @@ -2659,7 +2755,7 @@ DIR_GRAPH_MAX_DEPTH = 1 # The DOT_IMAGE_FORMAT tag can be used to set the image format of the images # generated by dot. For an explanation of the image formats see the section # output formats in the documentation of the dot tool (Graphviz (see: -# http://www.graphviz.org/)). +# https://www.graphviz.org/)). # Note: If you choose svg you need to set HTML_FILE_EXTENSION to xhtml in order # to make the SVG files visible in IE 9+ (other browsers do not have this # requirement). @@ -2696,11 +2792,12 @@ DOT_PATH = DOTFILE_DIRS = -# The MSCFILE_DIRS tag can be used to specify one or more directories that -# contain msc files that are included in the documentation (see the \mscfile -# command). +# You can include diagrams made with dia in doxygen documentation. Doxygen will +# then run dia to produce the diagram and insert it in the documentation. The +# DIA_PATH tag allows you to specify the directory where the dia binary resides. +# If left empty dia is assumed to be found in the default search path. -MSCFILE_DIRS = +DIA_PATH = # The DIAFILE_DIRS tag can be used to specify one or more directories that # contain dia files that are included in the documentation (see the \diafile @@ -2777,3 +2874,19 @@ GENERATE_LEGEND = YES # The default value is: YES. DOT_CLEANUP = YES + +# You can define message sequence charts within doxygen comments using the \msc +# command. If the MSCGEN_TOOL tag is left empty (the default), then doxygen will +# use a built-in version of mscgen tool to produce the charts. Alternatively, +# the MSCGEN_TOOL tag can also specify the name an external tool. For instance, +# specifying prog as the value, doxygen will call the tool as prog -T +# -o . The external tool should support +# output file formats "png", "eps", "svg", and "ismap". + +MSCGEN_TOOL = + +# The MSCFILE_DIRS tag can be used to specify one or more directories that +# contain msc files that are included in the documentation (see the \mscfile +# command). + +MSCFILE_DIRS = diff --git a/docs/Makefile b/docs/Makefile index d0c3cbf1020..8b6275ab8cc 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -3,7 +3,7 @@ # You can set these variables from the command line, and also # from the environment for the first two. -SPHINXOPTS ?= +SPHINXOPTS ?= -W --keep-going SPHINXBUILD ?= sphinx-build SOURCEDIR = source BUILDDIR = build diff --git a/docs/make.bat b/docs/make.bat index dc1312ab09c..08ca2232081 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -9,6 +9,7 @@ if "%SPHINXBUILD%" == "" ( ) set SOURCEDIR=source set BUILDDIR=build +set "SPHINXOPTS=-W --keep-going" %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( @@ -25,11 +26,11 @@ if errorlevel 9009 ( if "%1" == "" goto help -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% || exit /b %ERRORLEVEL% goto end :help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% || exit /b %ERRORLEVEL% :end popd diff --git a/docs/requirements.txt b/docs/requirements.txt index 7ce0be80b83..a9964557c88 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,5 +1,9 @@ breathe==4.35.0 -furo==2023.5.20 +furo==2024.8.6 m2r2==0.3.3.post2 -Sphinx==7.0.1 +rstcheck[sphinx]==6.2.1 +rstfmt==0.0.14 +setuptools # required by m2r2, Ubuntu 24.04 doesn't include this +Sphinx==7.2.6 sphinx-copybutton==0.5.2 +sphinx_inline_tabs==2023.4.21 diff --git a/docs/source/about/advanced_usage.rst b/docs/source/about/advanced_usage.rst index 088d9045c29..1e606fa2187 100644 --- a/docs/source/about/advanced_usage.rst +++ b/docs/source/about/advanced_usage.rst @@ -5,14 +5,14 @@ Sunshine will work with the default settings for most users. In some cases you m Performance Tips ---------------- -AMD -^^^ -In Windows, enabling `Enhanced Sync` in AMD's settings may help reduce the latency by an additional frame. This -applies to `amfenc` and `libx264`. +.. tab:: AMD -Nvidia -^^^^^^ -Enabling `Fast Sync` in Nvidia settings may help reduce latency. + In Windows, enabling `Enhanced Sync` in AMD's settings may help reduce the latency by an additional frame. This + applies to `amfenc` and `libx264`. + +.. tab:: NVIDIA + + Enabling `Fast Sync` in Nvidia settings may help reduce latency. Configuration ------------- @@ -41,15 +41,50 @@ location by modifying the configuration file. sunshine ~/sunshine_config.conf -To manually configure sunshine you may edit the `conf` file in a text editor. Use the examples as reference. +Although it is recommended to use the configuration UI, it is possible manually configure sunshine by +editing the `conf` file in a text editor. Use the examples as reference. + +`General `__ +----------------------------------------------------- + +`locale `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -.. Hint:: Some settings are not available within the web ui. +**Description** + The locale used for Sunshine's user interface. -General -------- +**Choices** -sunshine_name -^^^^^^^^^^^^^ +.. table:: + :widths: auto + + ======= =========== + Value Description + ======= =========== + de German + en English + en_GB English (UK) + en_US English (United States) + es Spanish + fr French + it Italian + ja Japanese + pt Portuguese + ru Russian + sv Swedish + zh Chinese (Simplified) + ======= =========== + +**Default** + ``en`` + +**Example** + .. code-block:: text + + locale = en + +`sunshine_name `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Description** The name displayed by Moonlight @@ -62,8 +97,8 @@ sunshine_name sunshine_name = Sunshine -min_log_level -^^^^^^^^^^^^^ +`min_log_level `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Description** The minimum log level printed to standard out. @@ -93,22 +128,26 @@ min_log_level min_log_level = info -log_path -^^^^^^^^ +`channels `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Description** - The path where the sunshine log is stored. + Sunshine can support multiple clients streaming simultaneously, at the cost of higher CPU and GPU usage. + + .. note:: All connected clients share control of the same streaming session. + + .. warning:: Some hardware encoders may have limitations that reduce performance with multiple streams. **Default** - ``sunshine.log`` + ``1`` **Example** .. code-block:: text - log_path = sunshine.log + channels = 1 -global_prep_cmd -^^^^^^^^^^^^^^^ +`global_prep_cmd `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Description** A list of commands to be run before/after all applications. If any of the prep-commands fail, starting the application is aborted. @@ -121,16 +160,41 @@ global_prep_cmd global_prep_cmd = [{"do":"nircmd.exe setdisplay 1280 720 32 144","undo":"nircmd.exe setdisplay 2560 1440 32 144"}] -Controls --------- +`notify_pre_releases `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -gamepad -^^^^^^^ +**Description** + Whether to be notified of new pre-release versions of Sunshine. + +**Default** + ``disabled`` + +**Example** + .. code-block:: text + + notify_pre_releases = disabled + +`Input `__ +------------------------------------------------- + +`controller `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Description** + Whether to allow controller input from the client. + +**Example** + .. code-block:: text + + controller = enabled + +`gamepad `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Description** The type of gamepad to emulate on the host. - .. Caution:: Applies to Windows only. + .. caution:: Applies to Windows only. **Choices** @@ -140,25 +204,79 @@ gamepad ===== =========== Value Description ===== =========== - x360 xbox 360 controller - ds4 dualshock controller (PS4) + auto Selected based on information from client + x360 Xbox 360 controller + ds4 DualShock 4 controller (PS4) ===== =========== **Default** - ``x360`` + ``auto`` + +**Example** + .. code-block:: text + + gamepad = auto + +`ds4_back_as_touchpad_click `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Description** + .. hint:: Only applies when gamepad is set to ds4 manually. Unused in other gamepad modes. + + Allow Select/Back inputs to also trigger DS4 touchpad click. Useful for clients looking to emulate touchpad click + on Xinput devices. + +**Default** + ``enabled`` + +**Example** + .. code-block:: text + + ds4_back_as_touchpad_click = enabled + +`motion_as_ds4 `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Description** + .. hint:: Only applies when gamepad is set to auto. + + If a client reports that a connected gamepad has motion sensor support, emulate it on the host as a DS4 controller. + + When disabled, motion sensors will not be taken into account during gamepad type selection. + +**Default** + ``enabled`` **Example** .. code-block:: text - gamepad = x360 + motion_as_ds4 = enabled -back_button_timeout -^^^^^^^^^^^^^^^^^^^ +`touchpad_as_ds4 `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Description** + .. hint:: Only applies when gamepad is set to auto. + + If a client reports that a connected gamepad has a touchpad, emulate it on the host as a DS4 controller. + + When disabled, touchpad presence will not be taken into account during gamepad type selection. + +**Default** + ``enabled`` + +**Example** + .. code-block:: text + + touchpad_as_ds4 = enabled + +`back_button_timeout `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Description** If the Back/Select button is held down for the specified number of milliseconds, a Home/Guide button press is emulated. - .. Tip:: If back_button_timeout < 0, then the Home/Guide button will not be emulated. + .. tip:: If back_button_timeout < 0, then the Home/Guide button will not be emulated. **Default** ``-1`` @@ -168,8 +286,19 @@ back_button_timeout back_button_timeout = 2000 -key_repeat_delay -^^^^^^^^^^^^^^^^ +`keyboard `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Description** + Whether to allow keyboard input from the client. + +**Example** + .. code-block:: text + + keyboard = enabled + +`key_repeat_delay `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Description** The initial delay, in milliseconds, before repeating keys. Controls how fast keys will repeat themselves. @@ -182,13 +311,13 @@ key_repeat_delay key_repeat_delay = 500 -key_repeat_frequency -^^^^^^^^^^^^^^^^^^^^ +`key_repeat_frequency `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Description** How often keys repeat every second. - .. Tip:: This configurable option supports decimals. + .. tip:: This configurable option supports decimals. **Default** ``24.9`` @@ -198,8 +327,8 @@ key_repeat_frequency key_repeat_frequency = 24.9 -always_send_scancodes -^^^^^^^^^^^^^^^^^^^^^ +`always_send_scancodes `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Description** Sending scancodes enhances compatibility with games and apps but may result in incorrect keyboard input @@ -209,7 +338,7 @@ always_send_scancodes Disable if keys on the client are generating the wrong input on the host. - .. Caution:: Applies to Windows only. + .. caution:: Applies to Windows only. **Default** ``enabled`` @@ -219,22 +348,82 @@ always_send_scancodes always_send_scancodes = enabled +`key_rightalt_to_key_win `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Description** + It may be possible that you cannot send the Windows Key from Moonlight directly. In those cases it may be useful to + make Sunshine think the Right Alt key is the Windows key. + +**Default** + ``disabled`` + +**Example** + .. code-block:: text + + key_rightalt_to_key_win = enabled + +`mouse `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Description** + Whether to allow mouse input from the client. + +**Example** + .. code-block:: text + + mouse = enabled + +`high_resolution_scrolling `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Description** + When enabled, Sunshine will pass through high resolution scroll events from Moonlight clients. + + This can be useful to disable for older applications that scroll too fast with high resolution scroll events. + +**Default** + ``enabled`` + +**Example** + .. code-block:: text + + high_resolution_scrolling = enabled + +`native_pen_touch `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Description** + When enabled, Sunshine will pass through native pen/touch events from Moonlight clients. + + This can be useful to disable for older applications without native pen/touch support. + +**Default** + ``enabled`` + +**Example** + .. code-block:: text + + native_pen_touch = enabled + keybindings ^^^^^^^^^^^ **Description** Sometimes it may be useful to map keybindings. Wayland won't allow clients to capture the Win Key for example. - .. Tip:: See `virtual key codes `_ + .. tip:: See `virtual key codes `__ - .. Hint:: keybindings needs to have a multiple of two elements. + .. hint:: keybindings needs to have a multiple of two elements. **Default** .. code-block:: text - 0x10, 0xA0, - 0x11, 0xA2, - 0x12, 0xA4 + [ + 0x10, 0xA0, + 0x11, 0xA2, + 0x12, 0xA4 + ] **Example** .. code-block:: text @@ -246,31 +435,110 @@ keybindings 0x4A, 0x4B ] -key_rightalt_to_key_win -^^^^^^^^^^^^^^^^^^^^^^^ +.. note:: This option is not available in the UI. A PR would be welcome. + +`Audio/Video `__ +------------------------------------------------------------- + +`audio_sink `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Description** - It may be possible that you cannot send the Windows Key from Moonlight directly. In those cases it may be useful to - make Sunshine think the Right Alt key is the Windows key. + The name of the audio sink used for audio loopback. + + .. tip:: To find the name of the audio sink follow these instructions. + + **Linux + pulseaudio** + .. code-block:: bash + + pacmd list-sinks | grep "name:" + + **Linux + pipewire** + .. code-block:: bash + + pactl info | grep Source + # in some causes you'd need to use the `Sink` device, if `Source` doesn't work, so try: + pactl info | grep Sink + + **macOS** + Sunshine can only access microphones on macOS due to system limitations. To stream system audio use + `Soundflower `__ or + `BlackHole `__. + + **Windows** + .. code-block:: batch + + tools\audio-info.exe + + .. tip:: If you have multiple audio devices with identical names, use the Device ID instead. + + .. tip:: If you want to mute the host speakers, use `virtual_sink`_ instead. **Default** - ``disabled`` + Sunshine will select the default audio device. + +**Examples** + **Linux** + .. code-block:: text + + audio_sink = alsa_output.pci-0000_09_00.3.analog-stereo + + **macOS** + .. code-block:: text + + audio_sink = BlackHole 2ch + + **Windows** + .. code-block:: text + + audio_sink = Speakers (High Definition Audio Device) + +`virtual_sink `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Description** + The audio device that's virtual, like Steam Streaming Speakers. This allows Sunshine to stream audio, while muting + the speakers. + + .. tip:: See `audio_sink`_! + + .. tip:: These are some options for virtual sound devices. + + - Stream Streaming Speakers (Linux, macOS, Windows) + + - Steam must be installed. + - Enable `install_steam_audio_drivers`_ or use Steam Remote Play at least once to install the drivers. + + - `Virtual Audio Cable `__ (macOS, Windows) **Example** .. code-block:: text - key_rightalt_to_key_win = enabled + virtual_sink = Steam Streaming Speakers -Display -------- +`install_steam_audio_drivers `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -adapter_name -^^^^^^^^^^^^ +**Description** + Installs the Steam Streaming Speakers driver (if Steam is installed) to support surround sound and muting host audio. + + .. tip:: This option is only supported on Windows. + +**Default** + ``enabled`` + +**Example** + .. code-block:: text + + install_steam_audio_drivers = enabled + +`adapter_name `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Description** Select the video card you want to stream. - .. Tip:: To find the name of the appropriate values follow these instructions. + .. tip:: To find the name of the appropriate values follow these instructions. **Linux + VA-API** Unlike with `amdvce` and `nvenc`, it doesn't matter if video encoding is done on a different GPU. @@ -286,13 +554,17 @@ adapter_name To be supported by Sunshine, it needs to have at the very minimum: ``VAProfileH264High : VAEntrypointEncSlice`` - .. Todo:: macOS + .. todo:: macOS **Windows** .. code-block:: batch tools\dxgi-info.exe + .. note:: For hybrid graphics systems, DXGI reports the outputs are connected to whichever graphics adapter + that the application is configured to use, so it's not a reliable indicator of how the display is + physically connected. + **Default** Sunshine will select the default video card. @@ -302,36 +574,45 @@ adapter_name adapter_name = /dev/dri/renderD128 - .. Todo:: macOS + .. todo:: macOS **Windows** .. code-block:: text adapter_name = Radeon RX 580 Series -output_name -^^^^^^^^^^^ +`output_name `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Description** Select the display number you want to stream. - .. Tip:: To find the name of the appropriate values follow these instructions. + .. tip:: To find the name of the appropriate values follow these instructions. **Linux** - During Sunshine startup, you should see the list of detected monitors: + During Sunshine startup, you should see the list of detected displays: .. code-block:: text - Info: Detecting connected monitors - Info: Detected monitor 0: DVI-D-0, connected: false - Info: Detected monitor 1: HDMI-0, connected: true - Info: Detected monitor 2: DP-0, connected: true - Info: Detected monitor 3: DP-1, connected: false - Info: Detected monitor 4: DVI-D-1, connected: false + Info: Detecting displays + Info: Detected display: DVI-D-0 (id: 0) connected: false + Info: Detected display: HDMI-0 (id: 1) connected: true + Info: Detected display: DP-0 (id: 2) connected: true + Info: Detected display: DP-1 (id: 3) connected: false + Info: Detected display: DVI-D-1 (id: 4) connected: false + + You need to use the id value inside the parenthesis, e.g. ``1``. + + **macOS** + During Sunshine startup, you should see the list of detected displays: + + .. code-block:: text - You need to use the value before the colon in the output, e.g. ``1``. + Info: Detecting displays + Info: Detected display: Monitor-0 (id: 3) connected: true + Info: Detected display: Monitor-1 (id: 2) connected: true - .. Todo:: macOS + You need to use the id value inside the parenthesis, e.g. ``3``. **Windows** .. code-block:: batch @@ -347,37 +628,23 @@ output_name output_name = 0 - .. Todo:: macOS + **macOS** + .. code-block:: text + + output_name = 3 **Windows** .. code-block:: text output_name = \\.\DISPLAY1 -fps -^^^ - -**Description** - The fps modes advertised by Sunshine. - - .. Note:: Some versions of Moonlight, such as Moonlight-nx (Switch), rely on this list to ensure that the requested - fps is supported. - -**Default** - ``[10, 30, 60, 90, 120]`` - -**Example** - .. code-block:: text - - fps = [10, 30, 60, 90, 120] - -resolutions -^^^^^^^^^^^ +`resolutions `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Description** The resolutions advertised by Sunshine. - .. Note:: Some versions of Moonlight, such as Moonlight-nx (Switch), rely on this list to ensure that the requested + .. note:: Some versions of Moonlight, such as Moonlight-nx (Switch), rely on this list to ensure that the requested resolution is supported. **Default** @@ -412,141 +679,78 @@ resolutions 3840x1600, ] -dwmflush -^^^^^^^^ +`fps `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Description** - Invoke DwmFlush() to sync screen capture to the Windows presentation interval. - - .. Caution:: Applies to Windows only. Alleviates visual stuttering during mouse movement. - If enabled, this feature will automatically deactivate if the client framerate exceeds - the host monitor's current refresh rate. + The fps modes advertised by Sunshine. - .. Note:: If you disable this option, you may see video stuttering during mouse movement in certain scenarios. - It is recommended to leave enabled when possible. + .. note:: Some versions of Moonlight, such as Moonlight-nx (Switch), rely on this list to ensure that the requested + fps is supported. **Default** - ``enabled`` + ``[10, 30, 60, 90, 120]`` **Example** .. code-block:: text - dwmflush = enabled + fps = [10, 30, 60, 90, 120] -Audio ------ +`Network `__ +----------------------------------------------------- -audio_sink -^^^^^^^^^^ +`upnp `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Description** - The name of the audio sink used for audio loopback. - - .. Tip:: To find the name of the audio sink follow these instructions. - - **Linux + pulseaudio** - .. code-block:: bash - - pacmd list-sinks | grep "name:" - - **Linux + pipewire** - .. code-block:: bash - - pactl info | grep Source - # in some causes you'd need to use the `Sink` device, if `Source` doesn't work, so try: - pactl info | grep Sink - - **macOS** - Sunshine can only access microphones on macOS due to system limitations. To stream system audio use - `Soundflower `_ or - `BlackHole `_. - - **Windows** - .. code-block:: batch + Sunshine will attempt to open ports for streaming over the internet. - tools\audio-info.exe +**Choices** - .. Tip:: If you have multiple audio devices with identical names, use the Device ID instead. +.. table:: + :widths: auto - .. Tip:: If you want to mute the host speakers, use `virtual_sink`_ instead. + ===== =========== + Value Description + ===== =========== + on enable UPnP + off disable UPnP + ===== =========== **Default** - Sunshine will select the default audio device. - -**Examples** - **Linux** - .. code-block:: text - - audio_sink = alsa_output.pci-0000_09_00.3.analog-stereo - - **macOS** - .. code-block:: text - - audio_sink = BlackHole 2ch - - **Windows** - .. code-block:: text - - audio_sink = Speakers (High Definition Audio Device) - -virtual_sink -^^^^^^^^^^^^ - -**Description** - The audio device that's virtual, like Steam Streaming Speakers. This allows Sunshine to stream audio, while muting - the speakers. - - .. Tip:: See `audio_sink`_! - - .. Tip:: These are some options for virtual sound devices. - - - Stream Streaming Speakers (Linux, macOS, Windows) - - - Steam must be installed. - - Enable `install_steam_audio_drivers`_ or use Steam Remote Play at least once to install the drivers. - - - `Virtual Audio Cable `_ (macOS, Windows) + ``disabled`` **Example** .. code-block:: text - virtual_sink = Steam Streaming Speakers + upnp = on -install_steam_audio_drivers -^^^^^^^^^^^^^^^^^^^^^^^^^^^ +`address_family `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Description** - Installs the Steam Streaming Speakers driver (if Steam is installed) to support surround sound and muting host audio. - - .. Tip:: This option is only supported on Windows. - -**Default** - ``enabled`` - -**Example** - .. code-block:: text - - install_steam_audio_drivers = enabled - -Network -------- + Set the address family that Sunshine will use. -external_ip -^^^^^^^^^^^ +.. table:: + :widths: auto -**Description** - If no external IP address is given, Sunshine will attempt to automatically detect external ip-address. + ===== =========== + Value Description + ===== =========== + ipv4 IPv4 only + both IPv4+IPv6 + ===== =========== **Default** - Automatic + ``ipv4`` **Example** .. code-block:: text - external_ip = 123.456.789.12 + address_family = both -port -^^^^ +`port `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Description** Set the family of ports used by Sunshine. Changing this value will offset other ports per the table below. @@ -567,49 +771,67 @@ port Mic (unused) 48002 UDP +13 ================ ============ =========================== -.. Attention:: Custom ports may not be supported by all Moonlight clients. +.. attention:: Custom ports may not be supported by all Moonlight clients. **Default** ``47989`` +**Range** + ``1029-65514`` + **Example** .. code-block:: text port = 47989 -pkey -^^^^ +`origin_web_ui_allowed `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Description** - The private key. This must be 2048 bits. + The origin of the remote endpoint address that is not denied for HTTPS Web UI. + +**Choices** + +.. table:: + :widths: auto + + ===== =========== + Value Description + ===== =========== + pc Only localhost may access the web ui + lan Only LAN devices may access the web ui + wan Anyone may access the web ui + ===== =========== **Default** - ``credentials/cakey.pem`` + ``lan`` **Example** .. code-block:: text - pkey = /dir/pkey.pem + origin_web_ui_allowed = lan -cert -^^^^ +`external_ip `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Description** - The certificate. Must be signed with a 2048 bit key. + If no external IP address is given, Sunshine will attempt to automatically detect external ip-address. **Default** - ``credentials/cacert.pem`` + Automatic **Example** .. code-block:: text - cert = /dir/cert.pem + external_ip = 123.456.789.12 -origin_pin_allowed -^^^^^^^^^^^^^^^^^^ +`lan_encryption_mode `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Description** - The origin of the remote endpoint address that is not denied for HTTP method /pin. + This determines when encryption will be used when streaming over your local network. + + .. warning:: Encryption can reduce streaming performance, particularly on less powerful hosts and clients. **Choices** @@ -619,24 +841,26 @@ origin_pin_allowed ===== =========== Value Description ===== =========== - pc Only localhost may access /pin - lan Only LAN devices may access /pin - wan Anyone may access /pin + 0 encryption will not be used + 1 encryption will be used if the client supports it + 2 encryption is mandatory and unencrypted connections are rejected ===== =========== **Default** - ``pc`` + ``0`` **Example** .. code-block:: text - origin_pin_allowed = pc + lan_encryption_mode = 0 -origin_web_ui_allowed -^^^^^^^^^^^^^^^^^^^^^ +`wan_encryption_mode `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Description** - The origin of the remote endpoint address that is not denied for HTTPS Web UI. + This determines when encryption will be used when streaming over the Internet. + + .. warning:: Encryption can reduce streaming performance, particularly on less powerful hosts and clients. **Choices** @@ -646,92 +870,135 @@ origin_web_ui_allowed ===== =========== Value Description ===== =========== - pc Only localhost may access the web ui - lan Only LAN devices may access the web ui - wan Anyone may access the web ui + 0 encryption will not be used + 1 encryption will be used if the client supports it + 2 encryption is mandatory and unencrypted connections are rejected ===== =========== **Default** - ``lan`` + ``1`` **Example** .. code-block:: text - origin_web_ui_allowed = lan + wan_encryption_mode = 1 -upnp -^^^^ +`ping_timeout `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Description** - Sunshine will attempt to open ports for streaming over the internet. + How long to wait, in milliseconds, for data from Moonlight before shutting down the stream. -**Choices** +**Default** + ``10000`` -.. table:: - :widths: auto +**Example** + .. code-block:: text - ===== =========== - Value Description - ===== =========== - on enable UPnP - off disable UPnP - ===== =========== + ping_timeout = 10000 + +`Config Files `__ +-------------------------------------------------------- + +`file_apps `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Description** + The application configuration file path. The file contains a json formatted list of applications that can be started + by Moonlight. **Default** - ``disabled`` + OS and package dependent **Example** .. code-block:: text - upnp = on + file_apps = apps.json -ping_timeout -^^^^^^^^^^^^ +`credentials_file `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Description** - How long to wait, in milliseconds, for data from Moonlight before shutting down the stream. + The file where user credentials for the UI are stored. **Default** - ``10000`` + ``sunshine_state.json`` **Example** .. code-block:: text - ping_timeout = 10000 + credentials_file = sunshine_state.json + +`log_path `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Description** + The path where the sunshine log is stored. + +**Default** + ``sunshine.log`` -Encoding --------- +**Example** + .. code-block:: text + + log_path = sunshine.log -channels -^^^^^^^^ +`pkey `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Description** - This will generate distinct video streams, unlike simply broadcasting to multiple Clients. + The private key used for the web UI and Moonlight client pairing. For best compatibility, this should be an RSA-2048 private key. - When multicasting, it could be useful to have different configurations for each connected Client. + .. warning:: Not all Moonlight clients support ECDSA keys or RSA key lengths other than 2048 bits. - For instance: +**Default** + ``credentials/cakey.pem`` - - Clients connected through WAN and LAN have different bitrate constraints. - - Decoders may require different settings for color. +**Example** + .. code-block:: text - .. Warning:: CPU usage increases for each distinct video stream generated. + pkey = /dir/pkey.pem + +`cert `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Description** + The certificate used for the web UI and Moonlight client pairing. For best compatibility, this should have an RSA-2048 public key. + + .. warning:: Not all Moonlight clients support ECDSA keys or RSA key lengths other than 2048 bits. **Default** - ``1`` + ``credentials/cacert.pem`` **Example** .. code-block:: text - channels = 1 + cert = /dir/cert.pem -fec_percentage -^^^^^^^^^^^^^^ +`file_state `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Description** + The file where current state of Sunshine is stored. + +**Default** + ``sunshine_state.json`` + +**Example** + .. code-block:: text + + file_state = sunshine_state.json + +`Advanced `__ +------------------------------------------------------- + +`fec_percentage `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Description** Percentage of error correcting packets per data packet in each video frame. - .. Warning:: Higher values can correct for more network packet loss, but at the cost of increasing bandwidth usage. + .. warning:: Higher values can correct for more network packet loss, but at the cost of increasing bandwidth usage. **Default** ``20`` @@ -744,13 +1011,13 @@ fec_percentage fec_percentage = 20 -qp -^^ +`qp `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Description** Quantization Parameter. Some devices don't support Constant Bit Rate. For those devices, QP is used instead. - .. Warning:: Higher value means more compression, but less quality. + .. warning:: Higher value means more compression, but less quality. **Default** ``28`` @@ -760,31 +1027,31 @@ qp qp = 28 -min_threads -^^^^^^^^^^^ +`min_threads `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Description** - Minimum number of threads used for software encoding. + Minimum number of CPU threads used for encoding. - .. Note:: Increasing the value slightly reduces encoding efficiency, but the tradeoff is usually worth it to gain + .. note:: Increasing the value slightly reduces encoding efficiency, but the tradeoff is usually worth it to gain the use of more CPU cores for encoding. The ideal value is the lowest value that can reliably encode at your desired streaming settings on your hardware. **Default** - ``1`` + ``2`` **Example** .. code-block:: text - min_threads = 1 + min_threads = 2 -hevc_mode -^^^^^^^^^ +`hevc_mode `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Description** Allows the client to request HEVC Main or HEVC Main10 video streams. - .. Warning:: HEVC is more CPU-intensive to encode, so enabling this may reduce performance when using software + .. warning:: HEVC is more CPU-intensive to encode, so enabling this may reduce performance when using software encoding. **Choices** @@ -795,7 +1062,7 @@ hevc_mode ===== =========== Value Description ===== =========== - 0 advertise support for HEVC based on encoder + 0 advertise support for HEVC based on encoder capabilities (recommended) 1 do not advertise support for HEVC 2 advertise support for HEVC Main profile 3 advertise support for HEVC Main and Main10 (HDR) profiles @@ -809,32 +1076,63 @@ hevc_mode hevc_mode = 2 -capture -^^^^^^^ +`av1_mode `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Description** - Force specific screen capture method. + Allows the client to request AV1 Main 8-bit or 10-bit video streams. - .. Caution:: Applies to Linux only. + .. warning:: AV1 is more CPU-intensive to encode, so enabling this may reduce performance when using software + encoding. **Choices** .. table:: :widths: auto - ========= =========== - Value Description - ========= =========== - nvfbc Use NVIDIA Frame Buffer Capture to capture direct to GPU memory. This is usually the fastest method for - NVIDIA cards. For GeForce cards it will only work with drivers patched with - `nvidia-patch `_ - or `nvlax `_. - wlr Capture for wlroots based Wayland compositors via DMA-BUF. - kms DRM/KMS screen capture from the kernel. This requires that sunshine has cap_sys_admin capability. - See :ref:`Linux Setup `. - x11 Uses XCB. This is the slowest and most CPU intensive so should be avoided if possible. - ========= =========== - + ===== =========== + Value Description + ===== =========== + 0 advertise support for AV1 based on encoder capabilities (recommended) + 1 do not advertise support for AV1 + 2 advertise support for AV1 Main 8-bit profile + 3 advertise support for AV1 Main 8-bit and 10-bit (HDR) profiles + ===== =========== + +**Default** + ``0`` + +**Example** + .. code-block:: text + + av1_mode = 2 + +`capture `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Description** + Force specific screen capture method. + +**Choices** + +.. table:: + :widths: auto + + ========= ======== =========== + Value Platform Description + ========= ======== =========== + nvfbc Linux Use NVIDIA Frame Buffer Capture to capture direct to GPU memory. This is usually the fastest method for + NVIDIA cards. For GeForce cards it will only work with drivers patched with + `nvidia-patch `__ + or `nvlax `__. + wlr Linux Capture for wlroots based Wayland compositors via DMA-BUF. + kms Linux DRM/KMS screen capture from the kernel. This requires that sunshine has cap_sys_admin capability. + See :ref:`Linux Setup `. + x11 Linux Uses XCB. This is the slowest and most CPU intensive so should be avoided if possible. + ddx Windows Use DirectX Desktop Duplication API to capture the display. This is well-supported on Windows machines. + wgc Windows (beta feature) Use Windows.Graphics.Capture to capture the display. + ========= ======== =========== + **Default** Automatic. Sunshine will use the first capture method available in the order of the table above. @@ -842,9 +1140,9 @@ capture .. code-block:: text capture = kms - -encoder -^^^^^^^ + +`encoder `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Description** Force a specific encoder. @@ -871,61 +1169,53 @@ encoder encoder = nvenc -sw_preset -^^^^^^^^^ - -**Description** - The encoder preset to use. - - .. Note:: This option only applies when using software `encoder`_. +`NVIDIA NVENC Encoder `__ +------------------------------------------------------------------------------- - .. Note:: From `FFmpeg `_. +`nvenc_preset `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - A preset is a collection of options that will provide a certain encoding speed to compression ratio. A slower - preset will provide better compression (compression is quality per filesize). This means that, for example, if - you target a certain file size or constant bit rate, you will achieve better quality with a slower preset. - Similarly, for constant quality encoding, you will simply save bitrate by choosing a slower preset. +**Description** + NVENC encoder performance preset. + Higher numbers improve compression (quality at given bitrate) at the cost of increased encoding latency. + Recommended to change only when limited by network or decoder, otherwise similar effect can be accomplished by increasing bitrate. - Use the slowest preset that you have patience for. + .. note:: This option only applies when using NVENC `encoder`_. **Choices** .. table:: :widths: auto - ========= =========== - Value Description - ========= =========== - ultrafast fastest - superfast - veryfast - faster - fast - medium - slow - slower - veryslow slowest - ========= =========== + ========== =========== + Value Description + ========== =========== + 1 P1 (fastest) + 2 P2 + 3 P3 + 4 P4 + 5 P5 + 6 P6 + 7 P7 (slowest) + ========== =========== **Default** - ``superfast`` + ``1`` **Example** .. code-block:: text - sw_preset = superfast + nvenc_preset = 1 -sw_tune -^^^^^^^ +`nvenc_twopass `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Description** - The tuning preset to use. - - .. Note:: This option only applies when using software `encoder`_. - - .. Note:: From `FFmpeg `_. + Enable two-pass mode in NVENC encoder. + This allows to detect more motion vectors, better distribute bitrate across the frame and more strictly adhere to bitrate limits. + Disabling it is not recommended since this can lead to occasional bitrate overshoot and subsequent packet loss. - You can optionally use -tune to change settings based upon the specifics of your input. + .. note:: This option only applies when using NVENC `encoder`_. **Choices** @@ -935,30 +1225,27 @@ sw_tune =========== =========== Value Description =========== =========== - film use for high quality movie content; lowers deblocking - animation good for cartoons; uses higher deblocking and more reference frames - grain preserves the grain structure in old, grainy film material - stillimage good for slideshow-like content - fastdecode allows faster decoding by disabling certain filters - zerolatency good for fast encoding and low-latency streaming + disabled One pass (fastest) + quarter_res Two passes, first pass at quarter resolution (faster) + full_res Two passes, first pass at full resolution (slower) =========== =========== **Default** - ``zerolatency`` + ``quarter_res`` **Example** .. code-block:: text - sw_tune = zerolatency + nvenc_twopass = quarter_res -nv_preset -^^^^^^^^^ +`nvenc_spatial_aq `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Description** - The encoder preset to use. + Assign higher QP values to flat regions of the video. + Recommended to enable when streaming at lower bitrates. - .. Note:: This option only applies when using nvenc `encoder`_. For more information on the presets, see - `nvenc preset migration guide `_. + .. Note:: This option only applies when using NVENC `encoder`_. **Choices** @@ -968,30 +1255,53 @@ nv_preset ========== =========== Value Description ========== =========== - p1 fastest (lowest quality) - p2 faster (lower quality) - p3 fast (low quality) - p4 medium (default) - p5 slow (good quality) - p6 slower (better quality) - p7 slowest (best quality) + disabled Don't enable Spatial AQ (faster) + enabled Enable Spatial AQ (slower) ========== =========== **Default** - ``p4`` + ``disabled`` + +**Example** + .. code-block:: text + + nvenc_spatial_aq = disabled + +`nvenc_vbv_increase `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Description** + Single-frame VBV/HRD percentage increase. + By default sunshine uses single-frame VBV/HRD, which means any encoded video frame size is not expected to exceed requested bitrate divided by requested frame rate. + Relaxing this restriction can be beneficial and act as low-latency variable bitrate, but may also lead to packet loss if the network doesn't have buffer headroom to handle bitrate spikes. + Maximum accepted value is 400, which corresponds to 5x increased encoded video frame upper size limit. + + .. Note:: This option only applies when using NVENC `encoder`_. + + .. Warning:: Can lead to network packet loss. + +**Default** + ``0`` + +**Range** + ``0-400`` **Example** .. code-block:: text - nv_preset = p4 + nvenc_vbv_increase = 0 -nv_tune -^^^^^^^ +`nvenc_realtime_hags `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Description** - The encoder tuning profile. + Use realtime gpu scheduling priority in NVENC when hardware accelerated gpu scheduling (HAGS) is enabled in Windows. + Currently NVIDIA drivers may freeze in encoder when HAGS is enabled, realtime priority is used and VRAM utilization is close to maximum. + Disabling this option lowers the priority to high, sidestepping the freeze at the cost of reduced capture performance when the GPU is heavily loaded. - .. Note:: This option only applies when using nvenc `encoder`_. + .. note:: This option only applies when using NVENC `encoder`_. + + .. caution:: Applies to Windows only. **Choices** @@ -1001,27 +1311,29 @@ nv_tune ========== =========== Value Description ========== =========== - hq high quality - ll low latency - ull ultra low latency - lossless lossless + disabled Use high priority + enabled Use realtime priority ========== =========== **Default** - ``ull`` + ``enabled`` **Example** .. code-block:: text - nv_tune = ull + nvenc_realtime_hags = enabled -nv_rc -^^^^^ +`nvenc_latency_over_power `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Description** - The encoder rate control. + Adaptive P-State algorithm which NVIDIA drivers employ doesn't work well with low latency streaming, so sunshine requests high power mode explicitly. + + .. Note:: This option only applies when using NVENC `encoder`_. - .. Note:: This option only applies when using nvenc `encoder`_. + .. Warning:: Disabling it is not recommended since this can lead to significantly increased encoding latency. + + .. Caution:: Applies to Windows only. **Choices** @@ -1031,26 +1343,28 @@ nv_rc ========== =========== Value Description ========== =========== - constqp constant QP mode - vbr variable bitrate - cbr constant bitrate + disabled Sunshine doesn't change GPU power preferences (not recommended) + enabled Sunshine requests high power mode explicitly ========== =========== **Default** - ``cbr`` + ``enabled`` **Example** .. code-block:: text - nv_rc = cbr + nvenc_latency_over_power = enabled -nv_coder -^^^^^^^^ +`nvenc_opengl_vulkan_on_dxgi `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Description** - The entropy encoding to use. + Sunshine can't capture fullscreen OpenGL and Vulkan programs at full frame rate unless they present on top of DXGI. + This is system-wide setting that is reverted on sunshine program exit. - .. Note:: This option only applies when using H264 with nvenc `encoder`_. + .. Note:: This option only applies when using NVENC `encoder`_. + + .. Caution:: Applies to Windows only. **Choices** @@ -1060,26 +1374,57 @@ nv_coder ========== =========== Value Description ========== =========== - auto let ffmpeg decide - cabac context adaptive binary arithmetic coding - higher quality - cavlc context adaptive variable-length coding - faster decode + disabled Sunshine leaves global Vulkan/OpenGL present method unchanged + enabled Sunshine changes global Vulkan/OpenGL present method to "Prefer layered on DXGI Swapchain" ========== =========== **Default** - ``auto`` + ``enabled`` + +**Example** + .. code-block:: text + + nvenc_opengl_vulkan_on_dxgi = enabled + +`nvenc_h264_cavlc `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Description** + Prefer CAVLC entropy coding over CABAC in H.264 when using NVENC. + CAVLC is outdated and needs around 10% more bitrate for same quality, but provides slightly faster decoding when using software decoder. + + .. note:: This option only applies when using H.264 format with NVENC `encoder`_. + +**Choices** + +.. table:: + :widths: auto + + ========== =========== + Value Description + ========== =========== + disabled Prefer CABAC + enabled Prefer CAVLC + ========== =========== + +**Default** + ``disabled`` **Example** .. code-block:: text - nv_coder = auto + nvenc_h264_cavlc = disabled -qsv_preset -^^^^^^^^^^ +`Intel QuickSync Encoder `__ +------------------------------------------------------------------------------------- + +`qsv_preset `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Description** The encoder preset to use. - .. Note:: This option only applies when using quicksync `encoder`_. + .. note:: This option only applies when using quicksync `encoder`_. **Choices** @@ -1106,13 +1451,13 @@ qsv_preset qsv_preset = medium -qsv_coder -^^^^^^^^^ +`qsv_coder `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Description** The entropy encoding to use. - .. Note:: This option only applies when using H264 with quicksync `encoder`_. + .. note:: This option only applies when using H264 with quicksync `encoder`_. **Choices** @@ -1135,42 +1480,74 @@ qsv_coder qsv_coder = auto -amd_quality -^^^^^^^^^^^ +`qsv_slow_hevc `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Description** - The encoder preset to use. + This options enables use of HEVC on older Intel GPUs that only support low power encoding for H.264. + + .. Caution:: Streaming performance may be significantly reduced when this option is enabled. + +**Default** + ``disabled`` + +**Example** + .. code-block:: text + + qsv_slow_hevc = disabled + +`AMD AMF Encoder `__ +--------------------------------------------------------------------- + +`amd_usage `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Description** + The encoder usage profile is used to set the base set of encoding + parameters. + + .. note:: This option only applies when using amdvce `encoder`_. - .. Note:: This option only applies when using amdvce `encoder`_. + .. note:: The other AMF options that follow will override a subset + of the settings applied by your usage profile, but there are + hidden parameters set in usage profiles that cannot be + overridden elsewhere. **Choices** .. table:: :widths: auto - ========== =========== - Value Description - ========== =========== - speed prefer speed - balanced balanced - quality prefer quality - ========== =========== + ======================= =========== + Value Description + ======================= =========== + transcoding transcoding (slowest) + webcam webcam (slow) + lowlatency_high_quality low latency, high quality (fast) + lowlatency low latency (faster) + ultralowlatency ultra low latency (fastest) + ======================= =========== **Default** - ``balanced`` + ``ultralowlatency`` **Example** .. code-block:: text - amd_quality = balanced + amd_usage = ultralowlatency -amd_rc -^^^^^^ +`amd_rc `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Description** The encoder rate control. - .. Note:: This option only applies when using amdvce `encoder`_. + .. note:: This option only applies when using amdvce `encoder`_. + + .. warning:: The 'vbr_latency' option generally works best, but + some bitrate overshoots may still occur. Enabling HRD allows + all bitrate based rate controls to better constrain peak bitrate, + but may result in encoding artifacts depending on your card. **Choices** @@ -1194,43 +1571,75 @@ amd_rc amd_rc = vbr_latency -amd_usage -^^^^^^^^^ +`amd_enforce_hrd `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Description** - The encoder usage profile, used to balance latency with encoding quality. + Enable Hypothetical Reference Decoder (HRD) enforcement to help constrain the target bitrate. - .. Note:: This option only applies when using amdvce `encoder`_. + .. note:: This option only applies when using amdvce `encoder`_. + + .. warning:: HRD is known to cause encoding artifacts or negatively affect + encoding quality on certain cards. **Choices** .. table:: :widths: auto - =============== =========== - Value Description - =============== =========== - transcoding transcoding (slowest) - webcam webcam (slow) - lowlatency low latency (fast) - ultralowlatency ultra low latency (fastest) - =============== =========== + ======== =========== + Value Description + ======== =========== + enabled enable HRD + disabled disable HRD + ======== =========== **Default** - ``ultralowlatency`` + ``disabled`` **Example** .. code-block:: text - amd_usage = ultralowlatency + amd_enforce_hrd = disabled -amd_preanalysis -^^^^^^^^^^^^^^^ +`amd_quality `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Description** + The quality profile controls the tradeoff between + speed and quality of encoding. + + .. note:: This option only applies when using amdvce `encoder`_. + +**Choices** + +.. table:: + :widths: auto + + ========== =========== + Value Description + ========== =========== + speed prefer speed + balanced balanced + quality prefer quality + ========== =========== + +**Default** + ``balanced`` + +**Example** + .. code-block:: text + + amd_quality = balanced + + +`amd_preanalysis `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Description** Preanalysis can increase encoding quality at the cost of latency. - .. Note:: This option only applies when using amdvce `encoder`_. + .. note:: This option only applies when using amdvce `encoder`_. **Default** ``disabled`` @@ -1240,13 +1649,15 @@ amd_preanalysis amd_preanalysis = disabled -amd_vbaq -^^^^^^^^ +`amd_vbaq `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Description** - Variance Based Adaptive Quantization (VBAQ) can increase subjective visual quality. + Variance Based Adaptive Quantization (VBAQ) can increase subjective + visual quality by prioritizing allocation of more bits to smooth + areas compared to more textured areas. - .. Note:: This option only applies when using amdvce `encoder`_. + .. note:: This option only applies when using amdvce `encoder`_. **Default** ``enabled`` @@ -1256,13 +1667,13 @@ amd_vbaq amd_vbaq = enabled -amd_coder -^^^^^^^^^ +`amd_coder `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Description** The entropy encoding to use. - .. Note:: This option only applies when using H264 with amdvce `encoder`_. + .. note:: This option only applies when using H264 with amdvce `encoder`_. **Choices** @@ -1285,13 +1696,45 @@ amd_coder amd_coder = auto -vt_software -^^^^^^^^^^^ +`VideoToolbox Encoder `__ +------------------------------------------------------------------------------- + +`vt_coder `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Description** + The entropy encoding to use. + + .. note:: This option only applies when using macOS. + +**Choices** + +.. table:: + :widths: auto + + ========== =========== + Value Description + ========== =========== + auto let ffmpeg decide + cabac + cavlc + ========== =========== + +**Default** + ``auto`` + +**Example** + .. code-block:: text + + vt_coder = auto + +`vt_software `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Description** Force Video Toolbox to use software encoding. - .. Note:: This option only applies when using macOS. + .. note:: This option only applies when using macOS. **Choices** @@ -1315,15 +1758,15 @@ vt_software vt_software = auto -vt_realtime -^^^^^^^^^^^ +`vt_realtime `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Description** Realtime encoding. - .. Note:: This option only applies when using macOS. + .. note:: This option only applies when using macOS. - .. Warning:: Disabling realtime encoding might result in a delayed frame encoding or frame drop. + .. warning:: Disabling realtime encoding might result in a delayed frame encoding or frame drop. **Default** ``enabled`` @@ -1333,77 +1776,85 @@ vt_realtime vt_realtime = enabled -vt_coder -^^^^^^^^ +`Software Encoder `__ +----------------------------------------------------------------------- + +`sw_preset `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Description** - The entropy encoding to use. + The encoder preset to use. + + .. note:: This option only applies when using software `encoder`_. + + .. note:: From `FFmpeg `__. + + A preset is a collection of options that will provide a certain encoding speed to compression ratio. A slower + preset will provide better compression (compression is quality per filesize). This means that, for example, if + you target a certain file size or constant bit rate, you will achieve better quality with a slower preset. + Similarly, for constant quality encoding, you will simply save bitrate by choosing a slower preset. - .. Note:: This option only applies when using macOS. + Use the slowest preset that you have patience for. **Choices** .. table:: :widths: auto - ========== =========== - Value Description - ========== =========== - auto let ffmpeg decide - cabac - cavlc - ========== =========== + ========= =========== + Value Description + ========= =========== + ultrafast fastest + superfast + veryfast + faster + fast + medium + slow + slower + veryslow slowest + ========= =========== **Default** - ``auto`` + ``superfast`` **Example** .. code-block:: text - vt_coder = auto - -Advanced --------- + sw_preset = superfast -file_apps -^^^^^^^^^ +`sw_tune `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Description** - The application configuration file path. The file contains a json formatted list of applications that can be started - by Moonlight. - -**Default** - OS and package dependent - -**Example** - .. code-block:: text - - file_apps = apps.json - -file_state -^^^^^^^^^^ + The tuning preset to use. -**Description** - The file where current state of Sunshine is stored. + .. note:: This option only applies when using software `encoder`_. -**Default** - ``sunshine_state.json`` + .. note:: From `FFmpeg `__. -**Example** - .. code-block:: text + You can optionally use -tune to change settings based upon the specifics of your input. - file_state = sunshine_state.json +**Choices** -credentials_file -^^^^^^^^^^^^^^^^ +.. table:: + :widths: auto -**Description** - The file where user credentials for the UI are stored. + =========== =========== + Value Description + =========== =========== + film use for high quality movie content; lowers deblocking + animation good for cartoons; uses higher deblocking and more reference frames + grain preserves the grain structure in old, grainy film material + stillimage good for slideshow-like content + fastdecode allows faster decoding by disabling certain filters + zerolatency good for fast encoding and low-latency streaming + =========== =========== **Default** - ``sunshine_state.json`` + ``zerolatency`` **Example** .. code-block:: text - credentials_file = sunshine_state.json + sw_tune = zerolatency diff --git a/docs/source/about/app_examples.rst b/docs/source/about/app_examples.rst deleted file mode 100644 index 0e39029797b..00000000000 --- a/docs/source/about/app_examples.rst +++ /dev/null @@ -1,218 +0,0 @@ -App Examples -============ -Since not all applications behave the same, we decided to create some examples to help you get started adding games -and applications to Sunshine. - -.. Attention:: Throughout these examples, any fields not shown are left blank. You can enhance your experience by - adding an image or a log file (via the ``Output`` field). - -Common Examples ---------------- - -Desktop -^^^^^^^ - -+----------------------+-----------------+ -| **Field** | **Value** | -+----------------------+-----------------+ -| Application Name | ``Desktop`` | -+----------------------+-----------------+ -| Image | ``desktop.png`` | -+----------------------+-----------------+ - -Steam Big Picture -^^^^^^^^^^^^^^^^^ - -.. Note:: Steam is launched as a detached command because Steam starts with a process that self updates itself and the original - process is killed. Since the original process ends it will not work as a regular command. - -+----------------------+------------------------------------------+----------------------------------+-----------------------------------+ -| **Field** | **Linux** | **macOS** | **Windows** | -+----------------------+------------------------------------------+----------------------------------+-----------------------------------+ -| Application Name | ``Steam Big Picture`` | -+----------------------+------------------------------------------+----------------------------------+-----------------------------------+ -| Detached Commands | ``setsid steam steam://open/bigpicture`` | ``open steam://open/bigpicture`` | ``steam steam://open/bigpicture`` | -+----------------------+------------------------------------------+----------------------------------+-----------------------------------+ -| Image | ``steam.png`` | -+----------------------+------------------------------------------+----------------------------------+-----------------------------------+ - -Epic Game Store game -^^^^^^^^^^^^^^^^^^^^ - -.. Note:: Using URI method will be the most consistent between various games, but does not allow a game to be launched - using the "Command" and therefore the stream will not end when the game ends. - -URI (Epic) -"""""""""" - -+----------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------+ -| **Field** | **Windows** | -+----------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------+ -| Application Name | ``Surviving Mars`` | -+----------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------+ -| Detached Commands | ``cmd /C "start com.epicgames.launcher://apps/d759128018124dcabb1fbee9bb28e178%3A20729b9176c241f0b617c5723e70ec2d%3AOvenbird?action=launch&silent=true"`` | -+----------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------+ - -Binary (Epic w/ working directory) -"""""""""""""""""""""""""""""""""" - -+----------------------+-----------------------------------------------+ -| **Field** | **Windows** | -+----------------------+-----------------------------------------------+ -| Application Name | ``Surviving Mars`` | -+----------------------+-----------------------------------------------+ -| Command | ``cmd /c "MarsEpic.exe"`` | -+----------------------+-----------------------------------------------+ -| Working Directory | ``C:\Program Files\Epic Games\SurvivingMars`` | -+----------------------+-----------------------------------------------+ - -Binary (Epic w/o working directory) -""""""""""""""""""""""""""""""""""" - -+----------------------+--------------------------------------------------------------+ -| **Field** | **Windows** | -+----------------------+--------------------------------------------------------------+ -| Application Name | ``Surviving Mars`` | -+----------------------+--------------------------------------------------------------+ -| Command | ``"C:\Program Files\Epic Games\SurvivingMars\MarsEpic.exe"`` | -+----------------------+--------------------------------------------------------------+ - - -Steam game -^^^^^^^^^^ - -.. Note:: Using URI method will be the most consistent between various games, but does not allow a game to be launched - using the "Command" and therefore the stream will not end when the game ends. - -URI (Steam) -""""""""""" - -+----------------------+-------------------------------------------+-----------------------------------+---------------------------------------------+ -| **Field** | **Linux** | **macOS** | **Windows** | -+----------------------+-------------------------------------------+-----------------------------------+---------------------------------------------+ -| Application Name | ``Surviving Mars`` | -+----------------------+-------------------------------------------+-----------------------------------+---------------------------------------------+ -| Detached Commands | ``setsid steam steam://rungameid/464920`` | ``open steam://rungameid/464920`` | ``cmd /C "start steam://rungameid/464920"`` | -+----------------------+-------------------------------------------+-----------------------------------+---------------------------------------------+ - -Binary (Steam w/ working directory) -""""""""""""""""""""""""""""""""""" - -+----------------------+-------------------------+-------------------------+------------------------------------------------------------------+ -| **Field** | **Linux** | **macOS** | **Windows** | -+----------------------+-------------------------+-------------------------+------------------------------------------------------------------+ -| Application Name | ``Surviving Mars`` | -+----------------------+-------------------------+-------------------------+------------------------------------------------------------------+ -| Command | ``MarsSteam`` | ``cmd /c "MarsSteam.exe"`` | -+----------------------+-------------------------+-------------------------+------------------------------------------------------------------+ -| Working Directory | ``~/.steam/steam/SteamApps/common/Survivng Mars`` | ``C:\Program Files (x86)\Steam\steamapps\common\Surviving Mars`` | -+----------------------+-------------------------+-------------------------+------------------------------------------------------------------+ - -Binary (Steam w/o working directory) -"""""""""""""""""""""""""""""""""""" - -+----------------------+------------------------------+------------------------------+----------------------------------------------------------------------------------+ -| **Field** | **Linux** | **macOS** | **Windows** | -+----------------------+------------------------------+------------------------------+----------------------------------------------------------------------------------+ -| Application Name | ``Surviving Mars`` | -+----------------------+------------------------------+------------------------------+----------------------------------------------------------------------------------+ -| Command | ``~/.steam/steam/SteamApps/common/Survivng Mars/MarsSteam`` | ``"C:\Program Files (x86)\Steam\steamapps\common\Surviving Mars\MarsSteam.exe"`` | -+----------------------+------------------------------+------------------------------+----------------------------------------------------------------------------------+ - -Linux ------ - -Changing Resolution and Refresh Rate (Linux - X11) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -+----------------------+--------------------------------------------------------------+ -| **Field** | **Value** | -+----------------------+--------------------------------------------------------------+ -| Command Preparations | Do: ``xrandr --output HDMI-1 --mode 1920x1080 --rate 60`` | -| +--------------------------------------------------------------+ -| | Undo: ``xrandr --output HDMI-1 --mode 3840×2160 --rate 120`` | -+----------------------+--------------------------------------------------------------+ - -Changing Resolution and Refresh Rate (Linux - Wayland) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -+----------------------+-------------------------------------------------------------+ -| **Field** | **Value** | -+----------------------+-------------------------------------------------------------+ -| Command Preparations | Do: ``wlr-xrandr --output HDMI-1 --mode 1920x1080@60Hz`` | -| +-------------------------------------------------------------+ -| | Undo: ``wlr-xrandr --output HDMI-1 --mode 3840×2160@120Hz`` | -+----------------------+-------------------------------------------------------------+ - -Flatpak -^^^^^^^ - -.. Attention:: Because Flatpak packages run in a sandboxed environment and do not normally have access to the host, - the Flatpak of Sunshine requires commands to be prefixed with ``flatpak-spawn --host``. - -macOS ------ - -Changing Resolution and Refresh Rate (macOS) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. Note:: This example uses the `displayplacer` tool to change the resolution. - This tool can be installed following instructions in their - `GitHub repository `_. - -+----------------------+-----------------------------------------------------------------------------------------------+ -| **Field** | **Value** | -+----------------------+-----------------------------------------------------------------------------------------------+ -| Command Preparations | Do: ``displayplacer "id: res:1920x1080 hz:60 scaling:on origin:(0,0) degree:0"`` | -| +-----------------------------------------------------------------------------------------------+ -| | Undo: ``displayplacer "id: res:3840x2160 hz:120 scaling:on origin:(0,0) degree:0"`` | -+----------------------+-----------------------------------------------------------------------------------------------+ - -Windows -------- - -Changing Resolution and Refresh Rate (Windows) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. Note:: This example uses the `QRes` tool to change the resolution and refresh rate. - This tool can be downloaded from their `SourceForge repository `_. - -+----------------------+----------------------------------------------------+ -| **Field** | **Value** | -+----------------------+----------------------------------------------------+ -| Command Preparations | Do: ``FullPath\qres.exe /x:1920 /y:1080 /r:60`` | -| +----------------------------------------------------+ -| | Undo: ``FullPath\qres.exe /x:3840 /y:2160 /r:120`` | -+----------------------+----------------------------------------------------+ - -.. Tip:: You can change your host resolution to match the client resolution automatically using the - `Nonary/ResolutionAutomation `_ project. - - -Elevating Commands (Windows) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -If you've installed Sunshine as a service (default), you can now specify if a command should be elevated with adminsitrative privileges. -Simply enable the elevated option in the WEB UI, or add it to the JSON configuration. -This is an option for both prep-cmd and regular commands and will launch the process with the current user without a UAC prompt. - -.. Note:: It's important to write the values "true" and "false" as string values, not as the typical true/false values in most JSON. - -**Example** - .. code-block:: json - - { - "name": "Game With AntiCheat that Requires Admin", - "output": "", - "cmd": "ping 127.0.0.1", - "exclude-global-prep-cmd": "false", - "elevated": "true", - "prep-cmd": [ - { - "do": "powershell.exe -command \"Start-Streaming\"", - "undo": "powershell.exe -command \"Stop-Streaming\"", - "elevated": "false" - } - ], - "image-path": "" - } diff --git a/docs/source/about/changelog.rst b/docs/source/about/changelog.rst deleted file mode 100644 index 7aadfa7ebbd..00000000000 --- a/docs/source/about/changelog.rst +++ /dev/null @@ -1 +0,0 @@ -.. mdinclude:: ../../../CHANGELOG.md diff --git a/docs/source/about/guides/app_examples.rst b/docs/source/about/guides/app_examples.rst new file mode 100644 index 00000000000..8bca2bd89f0 --- /dev/null +++ b/docs/source/about/guides/app_examples.rst @@ -0,0 +1,352 @@ +App Examples +============ +Since not all applications behave the same, we decided to create some examples to help you get started adding games +and applications to Sunshine. + +.. attention:: Throughout these examples, any fields not shown are left blank. You can enhance your experience by + adding an image or a log file (via the ``Output`` field). + +.. note:: When a working directory is not specified, it defaults to the folder where the target application resides. + +Common Examples +--------------- + +Desktop +^^^^^^^ + ++----------------------+-----------------+ +| **Field** | **Value** | ++----------------------+-----------------+ +| Application Name | ``Desktop`` | ++----------------------+-----------------+ +| Image | ``desktop.png`` | ++----------------------+-----------------+ + +Steam Big Picture +^^^^^^^^^^^^^^^^^ + +.. note:: Steam is launched as a detached command because Steam starts with a process that self updates itself and the original + process is killed. + +.. tab:: Linux + + +----------------------+------------------------------------------+ + | Application Name | ``Steam Big Picture`` | + +----------------------+------------------------------------------+ + | Detached Commands | ``setsid steam steam://open/bigpicture`` | + +----------------------+------------------------------------------+ + | Image | ``steam.png`` | + +----------------------+------------------------------------------+ + +.. tab:: macOS + + +----------------------+----------------------------------+ + | Application Name | ``Steam Big Picture`` | + +----------------------+----------------------------------+ + | Detached Commands | ``open steam://open/bigpicture`` | + +----------------------+----------------------------------+ + | Image | ``steam.png`` | + +----------------------+----------------------------------+ + +.. tab:: Windows + + +----------------------+-----------------------------+ + | Application Name | ``Steam Big Picture`` | + +----------------------+-----------------------------+ + | Command | ``steam://open/bigpicture`` | + +----------------------+-----------------------------+ + | Image | ``steam.png`` | + +----------------------+-----------------------------+ + +Epic Game Store game +^^^^^^^^^^^^^^^^^^^^ + +.. note:: Using URI method will be the most consistent between various games. + +URI (Epic) +"""""""""" + +.. tab:: Windows + + +----------------------+--------------------------------------------------------------------------------------------------------------------------------------------+ + | Application Name | ``Surviving Mars`` | + +----------------------+--------------------------------------------------------------------------------------------------------------------------------------------+ + | Command | ``com.epicgames.launcher://apps/d759128018124dcabb1fbee9bb28e178%3A20729b9176c241f0b617c5723e70ec2d%3AOvenbird?action=launch&silent=true`` | + +----------------------+--------------------------------------------------------------------------------------------------------------------------------------------+ + +Binary (Epic w/ working directory) +"""""""""""""""""""""""""""""""""" + +.. tab:: Windows + + +----------------------+-----------------------------------------------+ + | Application Name | ``Surviving Mars`` | + +----------------------+-----------------------------------------------+ + | Command | ``MarsEpic.exe`` | + +----------------------+-----------------------------------------------+ + | Working Directory | ``C:\Program Files\Epic Games\SurvivingMars`` | + +----------------------+-----------------------------------------------+ + +Binary (Epic w/o working directory) +""""""""""""""""""""""""""""""""""" + +.. tab:: Windows + + +----------------------+--------------------------------------------------------------+ + | Application Name | ``Surviving Mars`` | + +----------------------+--------------------------------------------------------------+ + | Command | ``"C:\Program Files\Epic Games\SurvivingMars\MarsEpic.exe"`` | + +----------------------+--------------------------------------------------------------+ + +Steam game +^^^^^^^^^^ + +.. note:: Using URI method will be the most consistent between various games. + +URI (Steam) +""""""""""" + +.. tab:: Linux + + +----------------------+-------------------------------------------+ + | Application Name | ``Surviving Mars`` | + +----------------------+-------------------------------------------+ + | Detached Commands | ``setsid steam steam://rungameid/464920`` | + +----------------------+-------------------------------------------+ + +.. tab:: macOS + + +----------------------+-----------------------------------+ + | Application Name | ``Surviving Mars`` | + +----------------------+-----------------------------------+ + | Detached Commands | ``open steam://rungameid/464920`` | + +----------------------+-----------------------------------+ + +.. tab:: Windows + + +----------------------+------------------------------+ + | Application Name | ``Surviving Mars`` | + +----------------------+------------------------------+ + | Command | ``steam://rungameid/464920`` | + +----------------------+------------------------------+ + +Binary (Steam w/ working directory) +""""""""""""""""""""""""""""""""""" + +.. tab:: Linux + + +----------------------+---------------------------------------------------+ + | Application Name | ``Surviving Mars`` | + +----------------------+---------------------------------------------------+ + | Command | ``MarsSteam`` | + +----------------------+---------------------------------------------------+ + | Working Directory | ``~/.steam/steam/SteamApps/common/Survivng Mars`` | + +----------------------+---------------------------------------------------+ + +.. tab:: macOS + + +----------------------+---------------------------------------------------+ + | Application Name | ``Surviving Mars`` | + +----------------------+---------------------------------------------------+ + | Command | ``MarsSteam`` | + +----------------------+---------------------------------------------------+ + | Working Directory | ``~/.steam/steam/SteamApps/common/Survivng Mars`` | + +----------------------+---------------------------------------------------+ + +.. tab:: Windows + + +----------------------+------------------------------------------------------------------+ + | Application Name | ``Surviving Mars`` | + +----------------------+------------------------------------------------------------------+ + | Command | ``MarsSteam.exe`` | + +----------------------+------------------------------------------------------------------+ + | Working Directory | ``C:\Program Files (x86)\Steam\steamapps\common\Surviving Mars`` | + +----------------------+------------------------------------------------------------------+ + +Binary (Steam w/o working directory) +"""""""""""""""""""""""""""""""""""" + +.. tab:: Linux + + +----------------------+-------------------------------------------------------------+ + | Application Name | ``Surviving Mars`` | + +----------------------+-------------------------------------------------------------+ + | Command | ``~/.steam/steam/SteamApps/common/Survivng Mars/MarsSteam`` | + +----------------------+-------------------------------------------------------------+ + +.. tab:: macOS + + +----------------------+-------------------------------------------------------------+ + | Application Name | ``Surviving Mars`` | + +----------------------+-------------------------------------------------------------+ + | Command | ``~/.steam/steam/SteamApps/common/Survivng Mars/MarsSteam`` | + +----------------------+-------------------------------------------------------------+ + +.. tab:: Windows + + +----------------------+----------------------------------------------------------------------------------+ + | Application Name | ``Surviving Mars`` | + +----------------------+----------------------------------------------------------------------------------+ + | Command | ``"C:\Program Files (x86)\Steam\steamapps\common\Surviving Mars\MarsSteam.exe"`` | + +----------------------+----------------------------------------------------------------------------------+ + +Prep Commands +------------- + +Changing Resolution and Refresh Rate +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. tab:: Linux + + .. tab:: X11 + + +----------------------+------------------------------------------------------------------------------------------------------------------------------------+ + | Command Preparations | Do: ``sh -c "xrandr --output HDMI-1 --mode \"${SUNSHINE_CLIENT_WIDTH}x${SUNSHINE_CLIENT_HEIGHT}\" --rate ${SUNSHINE_CLIENT_FPS}"`` | + | +------------------------------------------------------------------------------------------------------------------------------------+ + | | Undo: ``xrandr --output HDMI-1 --mode 3840x2160 --rate 120`` | + +----------------------+------------------------------------------------------------------------------------------------------------------------------------+ + + .. hint:: + The above only works if the xrandr mode already exists. You will need to create new modes to stream to macOS and iOS devices, since they use non standard resolutions. + + You can update the ``Do`` command to this: + .. code-block:: bash + + bash -c "${HOME}/scripts/set-custom-res.sh \"${SUNSHINE_CLIENT_WIDTH}\" \"${SUNSHINE_CLIENT_HEIGHT}\" \"${SUNSHINE_CLIENT_FPS}\"" + + The ``set-custom-res.sh`` will have this content: + .. code-block:: bash + + #!/bin/bash + + # Get params and set any defaults + width=${1:-1920} + height=${2:-1080} + refresh_rate=${3:-60} + + # You may need to adjust the scaling differently so the UI/text isn't too small / big + scale=${4:-0.55} + + # Get the name of the active display + display_output=$(xrandr | grep " connected" | awk '{ print $1 }') + + # Get the modeline info from the 2nd row in the cvt output + modeline=$(cvt ${width} ${height} ${refresh_rate} | awk 'FNR == 2') + xrandr_mode_str=${modeline//Modeline \"*\" /} + mode_alias="${width}x${height}" + + echo "xrandr setting new mode ${mode_alias} ${xrandr_mode_str}" + xrandr --newmode ${mode_alias} ${xrandr_mode_str} + xrandr --addmode ${display_output} ${mode_alias} + + # Reset scaling + xrandr --output ${display_output} --scale 1 + + # Apply new xrandr mode + xrandr --output ${display_output} --primary --mode ${mode_alias} --pos 0x0 --rotate normal --scale ${scale} + + # Optional reset your wallpaper to fit to new resolution + # xwallpaper --zoom /path/to/wallpaper.png + + .. tab:: Wayland + + +----------------------+-----------------------------------------------------------------------------------------------------------------------------------+ + | Command Preparations | Do: ``sh -c "wlr-xrandr --output HDMI-1 --mode \"${SUNSHINE_CLIENT_WIDTH}x${SUNSHINE_CLIENT_HEIGHT}@${SUNSHINE_CLIENT_FPS}Hz\""`` | + | +-----------------------------------------------------------------------------------------------------------------------------------+ + | | Undo: ``wlr-xrandr --output HDMI-1 --mode 3840x2160@120Hz`` | + +----------------------+-----------------------------------------------------------------------------------------------------------------------------------+ + + .. tab:: KDE Plasma (Wayland, X11) + + +----------------------+-------------------------------------------------------------------------------------------------------------------------------+ + | Command Preparations | Do: ``sh -c "kscreen-doctor output.HDMI-A-1.mode.${SUNSHINE_CLIENT_WIDTH}x${SUNSHINE_CLIENT_HEIGHT}@${SUNSHINE_CLIENT_FPS}"`` | + | +-------------------------------------------------------------------------------------------------------------------------------+ + | | Undo: ``kscreen-doctor output.HDMI-A-1.mode.3840x2160@120`` | + +----------------------+-------------------------------------------------------------------------------------------------------------------------------+ + + .. tab:: NVIDIA + + +----------------------+------------------------------------------------------------------------------------------------------+ + | Command Preparations | Do: ``sh -c "${HOME}/scripts/set-custom-res.sh ${SUNSHINE_CLIENT_WIDTH} ${SUNSHINE_CLIENT_HEIGHT}"`` | + | +------------------------------------------------------------------------------------------------------+ + | | Undo: ``sh -c "${HOME}/scripts/set-custom-res.sh 3840 2160"`` | + +----------------------+------------------------------------------------------------------------------------------------------+ + + The ``set-custom-res.sh`` will have this content: + .. code-block:: bash + + #!/bin/bash + + # Get params and set any defaults + width=${1:-1920} + height=${2:-1080} + output=${3:-HDMI-1} + nvidia-settings -a CurrentMetaMode="${output}: nvidia-auto-select { ViewPortIn=${width}x${height}, ViewPortOut=${width}x${height}+0+0 }" + +.. tab:: macOS + + .. tab:: displayplacer + + .. note:: This example uses the `displayplacer` tool to change the resolution. + This tool can be installed following instructions in their + `GitHub repository `__. + + +----------------------+-----------------------------------------------------------------------------------------------+ + | Command Preparations | Do: ``displayplacer "id: res:1920x1080 hz:60 scaling:on origin:(0,0) degree:0"`` | + | +-----------------------------------------------------------------------------------------------+ + | | Undo: ``displayplacer "id: res:3840x2160 hz:120 scaling:on origin:(0,0) degree:0"`` | + +----------------------+-----------------------------------------------------------------------------------------------+ + +.. tab:: Windows + + .. tab:: QRes + + .. note:: This example uses the `QRes` tool to change the resolution and refresh rate. + This tool can be downloaded from their `SourceForge repository `__. + + +----------------------+------------------------------------------------------------------------------------------------------------------+ + | Command Preparations | Do: ``cmd /C FullPath\qres.exe /x:%SUNSHINE_CLIENT_WIDTH% /y:%SUNSHINE_CLIENT_HEIGHT% /r:%SUNSHINE_CLIENT_FPS%`` | + | +------------------------------------------------------------------------------------------------------------------+ + | | Undo: ``cmd /C FullPath\qres.exe /x:3840 /y:2160 /r:120`` | + +----------------------+------------------------------------------------------------------------------------------------------------------+ + +Additional Considerations +------------------------- + +.. tab:: Linux + + .. tab:: Flatpak + + .. attention:: Because Flatpak packages run in a sandboxed environment and do not normally have access to the + host, the Flatpak of Sunshine requires commands to be prefixed with ``flatpak-spawn --host``. + +.. tab:: Windows + + **Elevating Commands (Windows)** + + If you've installed Sunshine as a service (default), you can specify if a command should be elevated with + administrative privileges. Simply enable the elevated option in the WEB UI, or add it to the JSON configuration. + This is an option for both prep-cmd and regular commands and will launch the process with the current user without a + UAC prompt. + + .. note:: It is important to write the values "true" and "false" as string values, not as the typical true/false + values in most JSON. + + **Example** + .. code-block:: json + + { + "name": "Game With AntiCheat that Requires Admin", + "output": "", + "cmd": "ping 127.0.0.1", + "exclude-global-prep-cmd": "false", + "elevated": "true", + "prep-cmd": [ + { + "do": "powershell.exe -command \"Start-Streaming\"", + "undo": "powershell.exe -command \"Stop-Streaming\"", + "elevated": "false" + } + ], + "image-path": "" + } diff --git a/docs/source/about/guides/guides.rst b/docs/source/about/guides/guides.rst new file mode 100644 index 00000000000..e11418367b3 --- /dev/null +++ b/docs/source/about/guides/guides.rst @@ -0,0 +1,11 @@ +Guides +====== + +Collection of guides written by the community! + +.. toctree:: + :maxdepth: 2 + + app_examples + linux + windows diff --git a/docs/source/about/guides/linux.rst b/docs/source/about/guides/linux.rst new file mode 100644 index 00000000000..a7c456d201e --- /dev/null +++ b/docs/source/about/guides/linux.rst @@ -0,0 +1,10 @@ +Linux +====== + +Collection of Sunshine Linux host guides. + +.. toctree:: + :maxdepth: 1 + :glob: + + linux/* diff --git a/docs/source/about/guides/linux/discord_calls.rst b/docs/source/about/guides/linux/discord_calls.rst new file mode 100644 index 00000000000..34b5920a3cb --- /dev/null +++ b/docs/source/about/guides/linux/discord_calls.rst @@ -0,0 +1,30 @@ +How to not stream Discord call audio +==================================== + +#. Set your normal `Sound Output` volume to 100% + + .. image:: ../../../images/discord_calls_01.png + +#. Start Sunshine + +#. Set `Sound Output` to `sink-sunshine-stereo` (if it isn't automatic) + + .. image:: ../../../images/discord_calls_02.png + +#. In Discord - `Right Click` - `Deafen` - Select your normal `Output Device` + + This is also where you will need to adjust output volume for Discord calls + + .. image:: ../../../images/discord_calls_03.png + +#. Open `qpwgraph` + + .. image:: ../../../images/discord_calls_04.png + +#. Connect `sunshine [sunshine-record]` to your normal `Output Device` + + * Drag `monitor_FL` to `playback_FL` + + * Drag `monitor_FR` to `playback_FR` + + .. image:: ../../../images/discord_calls_05.png diff --git a/docs/source/about/guides/linux/headless_ssh.rst b/docs/source/about/guides/linux/headless_ssh.rst new file mode 100644 index 00000000000..b86bc8a7caa --- /dev/null +++ b/docs/source/about/guides/linux/headless_ssh.rst @@ -0,0 +1,526 @@ +Remote SSH Headless Setup +========================= + +.. csv-table:: Remote SSH Headless Setup + :header-rows: 0 + :stub-columns: 1 + + Author, `Eric Dong `__ + Difficulty, Intermediate + +This is a guide to setup remote SSH into host to startup X server and sunshine without physical login and dummy plug. +The virtual display is accelerated by the NVidia GPU using the TwinView configuration. + +.. attention:: + This guide is specific for Xorg and NVidia GPUs. I start the X server using the ``startx`` command. + I also only tested this on an Artix runit init system on LAN. + I didn't have to do anything special with pulseaudio (pipewire untested). + + Keep your monitors plugged in until the `Checkpoint`_ step + +.. tip:: + Prior to editing any system configurations, you should make a copy of the original file. + This will allow you to use it for reference or revert your changes easily. + +The Big Picture +--------------- +Once you are done, you will need to perform these 3 steps: + +#. Turn on the host machine +#. Start sunshine on remote host with a script that: + + - Edits permissions of ``/dev/uinput`` (added sudo config to execute script with no password prompt) + - Starts X server with ``startx`` on virtual display + - Starts ``Sunshine`` + +#. Startup Moonlight on the client of interest and connect to host + +.. hint:: + + As an alternative to SSH... + + **Step 2** can be replaced with autologin and starting sunshine as a service or putting + ``sunshine &`` in your ``.xinitrc`` file if you start your X server with ``startx``. + In this case, the workaround for ``/dev/uinput`` permissions is not needed because the udev rule would be triggered + for "physical" login. See :ref:`Linux Setup `. I personally think autologin compromises the + security of the PC, so I went with the remote SSH route. I use the PC more than for gaming, so I don't need a + virtual display everytime I turn on the PC (E.g running updates, config changes, file/media server). + +First we will setup the host and then the SSH Client (Which may not be the same as the machine running the +moonlight client) + +Host Setup +---------- + +We will be setting up: + +#. `Static IP Setup`_ +#. `SSH Server Setup`_ +#. `Virtual Display Setup`_ +#. `Uinput Permissions Workaround`_ +#. `Stream Launcher Script`_ + + +Static IP Setup +^^^^^^^^^^^^^^^ +Setup static IP Address for host. For LAN connections you can use DHCP reservation within your assigned range. +e.g. 192.168.x.x. This will allow you to ssh to the host consistently, so the assigned IP address does +not change. It is preferred to set this through your router config. + + +SSH Server Setup +^^^^^^^^^^^^^^^^ + +.. note:: + Most distros have OpenSSH already installed. If it is not present, install OpenSSH using your package manager. + +.. tab:: Debian/Ubuntu + + .. code-block:: bash + + sudo apt update + sudo apt install openssh-server + +.. tab:: Arch/Artix + + .. code-block:: bash + + sudo pacman -S openssh + # Install openssh- if you are not using SystemD + # e.g. sudo pacman -S openssh-runit + +.. tab:: Alpine + + .. code-block:: bash + + sudo apk update + sudo apk add openssh + +.. tab:: CentOS/RHEL/Fedora + + **CentOS/RHEL 7** + .. code-block:: bash + + sudo yum install openssh-server + + **CentOS/Fedora/RHEL 8** + .. code-block:: bash + + sudo dnf install openssh-server + +Next make sure the OpenSSH daemon is enabled to run when the system starts. + +.. tab:: SystemD + + .. code-block:: bash + + sudo systemctl enable sshd.service + sudo systemctl start sshd.service # Starts the service now + sudo systemctl status sshd.service # See if the service is running + +.. tab:: Runit + + .. code-block:: bash + + sudo ln -s /etc/runit/sv/sshd /run/runit/service # Enables the OpenSSH daemon to run when system starts + sudo sv start sshd # Starts the service now + sudo sv status sshd # See if the service is running + +.. tab:: OpenRC + + .. code-block:: bash + + rc-update add sshd # Enables service + rc-status # List services to verify sshd is enabled + rc-service sshd start # Starts the service now + +**Disabling PAM in sshd** + +I noticed when the ssh session is disconnected for any reason, ``pulseaudio`` would disconnect. +This is due to PAM handling sessions. When running ``dmesg``, I noticed ``elogind`` would say removed user session. +In this `Gentoo Forums post `__, +someone had a similar issue. Starting the X server in the background and exiting out of the console would cause your +session to be removed. + +.. caution:: + According to this `article `__ + disabling PAM increases security, but reduces certain functionality in terms of session handling. + *Do so at your own risk!* + +Edit the ``sshd_config`` file with the following to disable PAM. + +.. code-block:: text + + usePAM no + +After making changes to the ``sshd_config``, restart the sshd service for changes to take effect. + +.. tip:: + Run the command to check the ssh configuration prior to restarting the sshd service. + + .. code-block:: bash + + sudo sshd -t -f /etc/ssh/sshd_config + + An incorrect configuration will prevent the sshd service from starting, which might mean + losing SSH access to the server. + +.. tab:: SystemD + + .. code-block:: bash + + sudo systemctl restart sshd.service + +.. tab:: Runit + + .. code-block:: bash + + sudo sv restart sshd + +.. tab:: OpenRC + + .. code-block:: bash + + sudo rc-service sshd restart + + +Virtual Display Setup +^^^^^^^^^^^^^^^^^^^^^ + +As an alternative to a dummy dongle, you can use this config to create a virtual display. + +.. important:: + This is only available for NVidia GPUs using Xorg. + +.. hint:: + Use ``xrandr`` to see name of your active display output. Usually it starts with ``DP`` or ``HDMI``. For me, it is ``DP-0``. + Put this name for the ``ConnectedMonitor`` option under the ``Device`` section. + + .. code-block:: bash + + xrandr | grep " connected" | awk '{ print $1 }' + + +.. code-block:: xorg.conf + + Section "ServerLayout" + Identifier "TwinLayout" + Screen 0 "metaScreen" 0 0 + EndSection + + Section "Monitor" + Identifier "Monitor0" + Option "Enable" "true" + EndSection + + Section "Device" + Identifier "Card0" + Driver "nvidia" + VendorName "NVIDIA Corporation" + Option "MetaModes" "1920x1080" + Option "ConnectedMonitor" "DP-0" + Option "ModeValidation" "NoDFPNativeResolutionCheck,NoVirtualSizeCheck,NoMaxPClkCheck,NoHorizSyncCheck,NoVertRefreshCheck,NoWidthAlignmentCheck" + EndSection + + Section "Screen" + Identifier "metaScreen" + Device "Card0" + Monitor "Monitor0" + DefaultDepth 24 + Option "TwinView" "True" + SubSection "Display" + Modes "1920x1080" + EndSubSection + EndSection + +.. note:: + The ``ConnectedMonitor`` tricks the GPU into thinking a monitor is connected, + even if there is none actually connected! This allows a virtual display to be created that is accelerated with + your GPU! The ``ModeValidation`` option disables valid resolution checks, so you can choose any + resolution on the host! + + **References** + + - `issue comment on virtual-display-linux + `__ + - `Nvidia Documentation on Configuring TwinView + `__ + - `Arch Wiki Nvidia#TwinView `__ + - `Unix Stack Exchange - How to add virtual display monitor with Nvidia proprietary driver + `__ + + +Uinput Permissions Workaround +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Steps** + +We can use ``chown`` to change the permissions from a script. Since this requires ``sudo``, +we will need to update the sudo configuration to execute this without being prompted for a password. + +#. Create a ``sunshine-setup.sh`` script to update permissions on ``/dev/uinput``. Since we aren't logged into the host, + the udev rule doesn't apply. +#. Update user sudo configuration ``/etc/sudoers.d/`` to allow the ``sunshine-setup.sh`` + script to be executed with ``sudo``. + +.. note:: + After I setup the :ref:`udev rule ` to get access to ``/dev/uinput``, + I noticed when I sshed into the host without physical login, the ACL permissions on ``/dev/uinput`` were not changed. + So I asked `reddit + `__. + I discovered that SSH sessions are not the same as a physical login. + I suppose it's not possible for SSH to trigger a udev rule or create a physical login session. + +**Setup Script** + +This script will take care of any preconditions prior to starting up sunshine. + +Run the following to create a script named something like ``sunshine-setup.sh``: + .. code-block:: bash + + echo "chown $(id -un):$(id -gn) /dev/uinput" > sunshine-setup.sh &&\ + chmod +x sunshine-setup.sh + +(**Optional**) To Ensure ethernet is being used for streaming, +you can block WiFi with ``rfkill``. + +Run this command to append the rfkill block command to the script: + .. code-block:: bash + + echo "rfkill block $(rfkill list | grep "Wireless LAN" \ + | sed 's/^\([[:digit:]]\).*/\1/')" >> sunshine-setup.sh + +**Sudo Configuration** + +We will manually change the permissions of ``/dev/uinput`` using ``chown``. +You need to use ``sudo`` to make this change, so add/update the entry in ``/etc/sudoers.d/${USER}`` + +.. danger:: + Do so at your own risk! It is more secure to give sudo and no password prompt to a single script, + than a generic executable like chown. + +.. warning:: + Be very careful of messing this config up. If you make a typo, *YOU LOSE THE ABILITY TO USE SUDO*. + Fortunately, your system is not borked, you will need to login as root to fix the config. + You may want to setup a backup user / SSH into the host as root to fix the config if this happens. + Otherwise you will need to plug your machine back into a monitor and login as root to fix this. + To enable root login over SSH edit your SSHD config, and add ``PermitRootLogin yes``, and restart the SSH server. + +#. First make a backup of your ``/etc/sudoers.d/${USER}`` file. + + .. code-block:: bash + + sudo cp /etc/sudoers.d/${USER} /etc/sudoers.d/${USER}.backup + +#. ``cd`` to the parent dir of the ``sunshine-setup.sh`` script. +#. Execute the following to update your sudoer config file. + + .. code-block:: bash + + echo "${USER} ALL=(ALL:ALL) ALL, NOPASSWD: $(pwd)/sunshine-setup.sh" \ + | sudo tee /etc/sudoers.d/${USER} + +These changes allow the script to use sudo without being prompted with a password. + +e.g. ``sudo $(pwd)/sunshine-setup.sh`` + + +Stream Launcher Script +^^^^^^^^^^^^^^^^^^^^^^ + +This is the main entrypoint script that will run the ``sunshine-setup.sh`` script, start up X server, and Sunshine. +The client will call this script that runs on the host via ssh. + + +**Sunshine Startup Script** + +This guide will refer to this script as ``~/scripts/sunshine.sh``. +The setup script will be referred as ``~/scripts/sunshine-setup.sh`` + +.. code-block:: bash + + #!/bin/bash + + export DISPLAY=:0 + + # Check existing X server + ps -e | grep X >/dev/null + [[ ${?} -ne 0 ]] && { + echo "Starting X server" + startx &>/dev/null & + [[ ${?} -eq 0 ]] && { + echo "X server started successfully" + } || echo "X server failed to start" + } || echo "X server already running" + + # Check if sunshine is already running + ps -e | grep -e .*sunshine$ >/dev/null + [[ ${?} -ne 0 ]] && { + sudo ~/scripts/sunshine-setup.sh + echo "Starting Sunshine!" + sunshine > /dev/null & + [[ ${?} -eq 0 ]] && { + echo "Sunshine started successfully" + } || echo "Sunshine failed to start" + } || echo "Sunshine is already running" + + # Add any other Programs that you want to startup automatically + # e.g. + # steam &> /dev/null & + # firefox &> /dev/null & + # kdeconnect-app &> /dev/null & + +---- + +SSH Client Setup +---------------- + +We will be setting up: + +#. `SSH Key Authentication Setup`_ +#. `SSH Client Script (Optional)`_ + +SSH Key Authentication Setup +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +#. Setup your SSH keys with ``ssh-keygen`` and use ``ssh-copy-id`` to authorize remote login to your host. + Run ``ssh @`` to login to your host. + SSH keys automate login so you don't need to input your password! +#. Optionally setup a ``~/.ssh/config`` file to simplify the ``ssh`` command + + .. code-block:: text + + Host + Hostname + User + IdentityFile ~/.ssh/ + + Now you can use ``ssh ``. + ``ssh `` will execute the command or script on the remote host. + +Checkpoint +^^^^^^^^^^ + +As a sanity check, let's make sure your setup is working so far! + +**Test Steps** + +With your monitor still plugged into your Sunshine host PC: + +#. ``ssh `` +#. ``~/scripts/sunshine.sh`` +#. ``nvidia-smi`` + + You should see the sunshine and Xorg processing running: + + .. code-block:: bash + + nvidia-smi + + *Output:* + + .. code-block:: console + + +---------------------------------------------------------------------------------------+ + | NVIDIA-SMI 535.104.05 Driver Version: 535.104.05 CUDA Version: 12.2 | + |-----------------------------------------+----------------------+----------------------+ + | GPU Name Persistence-M | Bus-Id Disp.A | Volatile Uncorr. ECC | + | Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. | + | | | MIG M. | + |=========================================+======================+======================| + | 0 NVIDIA GeForce RTX 3070 Off | 00000000:01:00.0 On | N/A | + | 30% 46C P2 45W / 220W | 549MiB / 8192MiB | 2% Default | + | | | N/A | + +-----------------------------------------+----------------------+----------------------+ + + +---------------------------------------------------------------------------------------+ + | Processes: | + | GPU GI CI PID Type Process name GPU Memory | + | ID ID Usage | + |=======================================================================================| + | 0 N/A N/A 1393 G /usr/lib/Xorg 86MiB | + | 0 N/A N/A 1440 C+G sunshine 293MiB | + +---------------------------------------------------------------------------------------+ + +#. Check ``/dev/uinput`` permissions + + .. code-block:: bash + + ls -l /dev/uinput + + *Output:* + + .. code-block:: console + + crw------- 1 10, 223 Aug 29 17:31 /dev/uinput + +#. Connect to Sunshine host from a moonlight client + +Now kill X and sunshine by running ``pkill X`` on the host, +unplug your monitors from your GPU, and repeat steps 1 - 5. +You should get the same result. +With this setup you don't need to modify the Xorg config regardless if monitors are plugged in or not. + +.. code-block:: bash + + pkill X + + +SSH Client Script (Optional) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +At this point you have a working setup! For convenience I created this bash script to automate the +startup of the X server and Sunshine on the host. +This can be run on Unix systems, or on Windows using the ``git-bash`` or any bash shell. + +For Android/iOS you can install Linux emulators, e.g. ``Userland`` for Android and ``ISH`` for iOS. +The neat part is that you can execute one script to launch Sunshine from your phone or tablet! + +.. code-block:: bash + + #!/bin/bash + + ssh_args="@192.168.X.X" # Or use alias set in ~/.ssh/config + + check_ssh(){ + result=1 + # Note this checks infinitely, you could update this to have a max # of retries + while [[ $result -ne 0 ]] + do + echo "checking host..." + ssh $ssh_args "exit 0" 2>/dev/null + result=$? + [[ $result -ne 0 ]] && { + echo "Failed to ssh to $ssh_args, with exit code $result" + } + sleep 3 + done + echo "Host is ready for streaming!" + } + + start_stream(){ + echo "Starting sunshine server on host..." + echo "Start moonlight on your client of choice" + # -f runs ssh in the background + ssh -f $ssh_args "~/scripts/sunshine.sh &" + } + + check_ssh + start_stream + exit_code=${?} + + sleep 3 + exit ${exit_code} + +Next Steps +---------- + +Congrats you can now stream your desktop headless! When trying this the first time, +keep your monitors close by incase something isn't working right. + +If you have any feedback and any suggestions, feel free to make a post on Discord! + +.. seealso:: + Now that you have a virtual display, you may want to automate changing the resolution + and refresh rate prior to connecting to an app. See :ref:`Changing Resolution and Refresh Rate + ` for more information. diff --git a/docs/source/about/guides/windows.rst b/docs/source/about/guides/windows.rst new file mode 100644 index 00000000000..71d8b940473 --- /dev/null +++ b/docs/source/about/guides/windows.rst @@ -0,0 +1,10 @@ +Windows +======= + +Collection of Sunshine Windows host guides. + +.. toctree:: + :maxdepth: 1 + :glob: + + windows/* diff --git a/docs/source/about/guides/windows/discord_voicemeeter.rst b/docs/source/about/guides/windows/discord_voicemeeter.rst new file mode 100644 index 00000000000..41db4a7030b --- /dev/null +++ b/docs/source/about/guides/windows/discord_voicemeeter.rst @@ -0,0 +1,37 @@ +Discord call audio cancellation with Voicemeeter (Standard) +=========================================================== + +Voicemeeter +^^^^^^^^^^^ + +#. Click Hardware Out +#. Set the physical device you recieve audio to as your Hardware Out with MME +#. Turn on BUS A for the Virtual Input + +Windows +^^^^^^^ + +#. Open the sound settings +#. Set your default Playback as Voicemeeter Input + +.. note:: Run audio in the background to find the device that your Virtual Input is using + (Voicemeeter In #), you will see the bar to the right of the device have green bars + going up and down. This device will be referred to as Voicemeeter Input. + +Discord +^^^^^^^ + +#. Open the settings +#. Go to Voice & Video +#. Set your Output Device as the physical you receive audio to + +.. note:: It is usually the same device you set for Hardware Out in Voicemeeter. + +Sunshine +^^^^^^^^ + +#. Go to Configuration +#. Go to the Audio/Video tab +#. Set Virtual Sink as Voicemeeter Input + +.. note:: This should be the device you set as default previously in Playback. diff --git a/docs/source/about/installation.rst b/docs/source/about/installation.rst deleted file mode 100644 index 323d78f4610..00000000000 --- a/docs/source/about/installation.rst +++ /dev/null @@ -1,257 +0,0 @@ -Installation -============ -The recommended method for running Sunshine is to use the `binaries`_ bundled with the `latest release`_. - -.. Attention:: Additional setup is required after installation. See - :ref:`Setup `. - -Binaries --------- -Binaries of Sunshine are created for each release. They are available for Linux, macOS, and Windows. -Binaries can be found in the `latest release`_. - -.. Tip:: Some third party packages also exist. See - :ref:`Third Party Packages `. - -Docker ------- -Docker images are available on `Dockerhub.io`_ and `ghcr.io`_. - -See :ref:`Docker ` for additional information. - -Linux ------ -Follow the instructions for your preferred package type below. - -**CUDA Compatibility** - -CUDA is used for NVFBC capture. - -.. Tip:: See `CUDA GPUS `_ to cross reference Compute Capability to your GPU. - -.. table:: - :widths: auto - - =========================================== ============== ============== ================================ - Package CUDA Version Min Driver CUDA Compute Capabilities - =========================================== ============== ============== ================================ - PKGBUILD User dependent User dependent User dependent - sunshine.AppImage 11.8.0 450.80.02 50;52;60;61;62;70;75;80;86;90;35 - sunshine.pkg.tar.zst 11.8.0 450.80.02 50;52;60;61;62;70;75;80;86;90;35 - sunshine_{arch}.flatpak 12.0.0 525.60.13 50;52;60;61;62;70;75;80;86;90 - sunshine-debian-bullseye-{arch}.deb 11.8.0 450.80.02 50;52;60;61;62;70;75;80;86;90;35 - sunshine-fedora-37-{arch}.rpm 12.0.0 525.60.13 50;52;60;61;62;70;75;80;86;90 - sunshine-fedora-38-{arch}.rpm unavailable unavailable none - sunshine-ubuntu-20.04-{arch}.deb 11.8.0 450.80.02 50;52;60;61;62;70;75;80;86;90;35 - sunshine-ubuntu-22.04-{arch}.deb 11.8.0 450.80.02 50;52;60;61;62;70;75;80;86;90;35 - =========================================== ============== ============== ================================ - -AppImage -^^^^^^^^ -According to AppImageLint the supported distro matrix of the AppImage is below. - -- [✖] Debian oldstable (buster) -- [✔] Debian stable (bullseye) -- [✔] Debian testing (bookworm) -- [✔] Debian unstable (sid) -- [✔] Ubuntu kinetic -- [✔] Ubuntu jammy -- [✔] Ubuntu focal -- [✖] Ubuntu bionic -- [✖] Ubuntu xenial -- [✖] Ubuntu trusty -- [✖] CentOS 7 - -#. Download ``sunshine.AppImage`` to your home directory. -#. Open terminal and run the following code. - - .. code-block:: bash - - ./sunshine.AppImage --install - -Start: - .. code-block:: bash - - ./sunshine.AppImage --install && ./sunshine.AppImage - -Uninstall: - .. code-block:: bash - - ./sunshine.AppImage --remove - -Archlinux PKGBUILD -^^^^^^^^^^^^^^^^^^ -#. Open terminal and run the following code. - - .. code-block:: bash - - wget https://github.com/LizardByte/Sunshine/releases/latest/download/PKGBUILD - makepkg -fi - -Uninstall: - .. code-block:: bash - - pacman -R sunshine - -Archlinux pkg -^^^^^^^^^^^^^ -#. Open terminal and run the following code. - - .. code-block:: bash - - wget https://github.com/LizardByte/Sunshine/releases/latest/download/sunshine.pkg.tar.zst - pacman -U --noconfirm sunshine.pkg.tar.zst - -Uninstall: - .. code-block:: bash - - pacman -R sunshine - -Debian Package -^^^^^^^^^^^^^^ -#. Download ``sunshine-{ubuntu-version}.deb`` and run the following code. - - .. code-block:: bash - - sudo apt install -f ./sunshine-{ubuntu-version}.deb - -.. Note:: The ``{ubuntu-version}`` is the version of ubuntu we built the package on. If you are not using Ubuntu and - have an issue with one package, you can try another. - -.. Tip:: You can double click the deb file to see details about the package and begin installation. - -Uninstall: - .. code-block:: bash - - sudo apt remove sunshine - -Flatpak Package -^^^^^^^^^^^^^^^ -#. Install `Flatpak `_ as required. -#. Download ``sunshine_{arch}.flatpak`` and run the following code. - - .. Note:: Be sure to replace ``{arch}`` with the architecture for your operating system. - - System level (recommended) - .. code-block:: bash - - flatpak install --system ./sunshine_{arch}.flatpak - - User level - .. code-block:: bash - - flatpak install --user ./sunshine_{arch}.flatpak - - Additional installation (required) - .. code-block:: bash - - flatpak run --command=additional-install.sh dev.lizardbyte.sunshine - -Start: - X11 and NVFBC capture (X11 Only) - .. code-block:: bash - - flatpak run dev.lizardbyte.sunshine - - KMS capture (Wayland & X11) - .. code-block:: bash - - sudo -i PULSE_SERVER=unix:$(pactl info | awk '/Server String/{print$3}') flatpak run dev.lizardbyte.sunshine - -Uninstall: - .. code-block:: bash - - flatpak run --command=remove-additional-install.sh dev.lizardbyte.sunshine - flatpak uninstall --delete-data dev.lizardbyte.sunshine - -RPM Package -^^^^^^^^^^^ -#. Add `rpmfusion` repositories by running the following code. - - .. code-block:: bash - - sudo dnf install https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm \ - https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-$(rpm -E %fedora).noarch.rpm - -#. Download ``sunshine.rpm`` and run the following code. - - .. code-block:: bash - - sudo dnf install ./sunshine.rpm - -.. Tip:: You can double click the rpm file to see details about the package and begin installation. - -Uninstall: - .. code-block:: bash - - sudo dnf remove sunshine - -macOS ------ -Sunshine on macOS is experimental. Gamepads do not work. Other features may not work as expected. - -dmg -^^^ -.. Warning:: The `dmg` does not include runtime dependencies. - -#. Download the ``sunshine.dmg`` file and install it. - -Uninstall: - .. code-block:: bash - - cd /etc/sunshine/assets - uninstall_pkg.sh - -Portfile -^^^^^^^^ -#. Install `MacPorts `_ -#. Update the Macports sources. - - .. code-block:: bash - - sudo nano /opt/local/etc/macports/sources.conf - - Add this line, replacing your username, below the line that starts with ``rsync``. - ``file:///Users//ports`` - - ``Ctrl+x``, then ``Y`` to exit and save changes. - -#. Download the ``Portfile`` to ``~/Downloads`` and run the following code. - - .. code-block:: bash - - mkdir -p ~/ports/multimedia/sunshine - mv ~/Downloads/Portfile ~/ports/multimedia/sunshine/ - cd ~/ports - portindex - sudo port install sunshine - -#. The first time you start Sunshine, you will be asked to grant access to screen recording and your microphone. - -Uninstall: - .. code-block:: bash - - sudo port uninstall sunshine - -Windows -------- - -Installer -^^^^^^^^^ -#. Download and install ``sunshine-windows-installer.exe`` - -.. Attention:: You should carefully select or unselect the options you want to install. Do not blindly install or enable - features. - -To uninstall, find Sunshine in the list `here `_ and select "Uninstall" from the overflow -menu. Different versions of Windows may provide slightly different steps for uninstall. - -Standalone -^^^^^^^^^^ -#. Download and extract ``sunshine-windows-portable.zip`` - -To uninstall, delete the extracted directory which contains the ``sunshine.exe`` file. - -.. _latest release: https://github.com/LizardByte/Sunshine/releases/latest -.. _Dockerhub.io: https://hub.docker.com/repository/docker/lizardbyte/sunshine -.. _ghcr.io: https://github.com/orgs/LizardByte/packages?repo_name=sunshine diff --git a/docs/source/about/setup.rst b/docs/source/about/setup.rst new file mode 100644 index 00000000000..8ea02937553 --- /dev/null +++ b/docs/source/about/setup.rst @@ -0,0 +1,617 @@ +Setup +===== +.. _latest release: https://github.com/LizardByte/Sunshine/releases/latest + +The recommended method for running Sunshine is to use the `binaries`_ bundled with the `latest release`_. + +Binaries +-------- +Binaries of Sunshine are created for each release. They are available for Linux, macOS, and Windows. +Binaries can be found in the `latest release`_. + +.. tip:: Some third party packages also exist. See + :ref:`Third Party Packages `. + No support will be provided for third party packages! + +Install +------- +.. tab:: Docker + + .. warning:: The Docker images are not recommended for most users. No support will be provided! + + Docker images are available on `Dockerhub.io `__ + and `ghcr.io `__. + + See :ref:`Docker ` for additional information. + +.. tab:: Linux + + **CUDA Compatibility** + + CUDA is used for NVFBC capture. + + .. tip:: See `CUDA GPUS `__ to cross reference Compute Capability to your GPU. + + .. table:: + :widths: auto + + =========================================== ============== ============== ================================ + Package CUDA Version Min Driver CUDA Compute Capabilities + =========================================== ============== ============== ================================ + sunshine.AppImage 11.8.0 450.80.02 35;50;52;60;61;62;70;75;80;86;90 + sunshine.pkg.tar.zst 11.8.0 450.80.02 35;50;52;60;61;62;70;75;80;86;90 + sunshine_{arch}.flatpak 12.0.0 525.60.13 50;52;60;61;62;70;75;80;86;90 + sunshine-debian-bookworm-{arch}.deb 12.0.0 525.60.13 50;52;60;61;62;70;75;80;86;90 + sunshine-debian-bullseye-{arch}.deb 11.8.0 450.80.02 35;50;52;60;61;62;70;75;80;86;90 + sunshine-fedora-39-{arch}.rpm 12.4.0 525.60.13 50;52;60;61;62;70;75;80;86;90 + sunshine-fedora-40-{arch}.rpm n/a n/a n/a + sunshine-ubuntu-22.04-{arch}.deb 11.8.0 450.80.02 35;50;52;60;61;62;70;75;80;86;90 + sunshine-ubuntu-24.04-{arch}.deb 11.8.0 450.80.02 35;50;52;60;61;62;70;75;80;86;90 + =========================================== ============== ============== ================================ + + .. tab:: AppImage + + .. caution:: Use distro-specific packages instead of the AppImage if they are available. + + According to AppImageLint the supported distro matrix of the AppImage is below. + + - ✔ Debian bullseye + - ✔ Debian bookworm + - ✔ Debian trixie + - ✖ Debian sid + - ✔ Ubuntu mantic + - ✔ Ubuntu lunar + - ✔ Ubuntu jammy + - ✔ Ubuntu focal + - ✖ Ubuntu bionic + - ✖ Ubuntu xenial + - ✖ Ubuntu trusty + - ✖ CentOS 7 + + #. Download ``sunshine.AppImage`` to your home directory. + + .. code-block:: bash + + cd ~ + wget https://github.com/LizardByte/Sunshine/releases/latest/download/sunshine.AppImage + + #. Open terminal and run the following code. + + .. code-block:: bash + + ./sunshine.AppImage --install + + Start: + .. code-block:: bash + + ./sunshine.AppImage --install && ./sunshine.AppImage + + Uninstall: + .. code-block:: bash + + ./sunshine.AppImage --remove + + .. tab:: Arch Linux Package + + #. Open terminal and run the following code. + + .. code-block:: bash + + wget https://github.com/LizardByte/Sunshine/releases/latest/download/sunshine.pkg.tar.zst + pacman -U --noconfirm sunshine.pkg.tar.zst + + Uninstall: + .. code-block:: bash + + pacman -R sunshine + + .. tab:: Debian/Ubuntu Package + + #. Download ``sunshine-{distro}-{distro-version}-{arch}.deb`` and run the following code. + + .. code-block:: bash + + sudo apt install -f ./sunshine-{distro}-{distro-version}-{arch}.deb + + .. note:: The ``{distro-version}`` is the version of the distro we built the package on. The ``{arch}`` is the + architecture of your operating system. + + .. tip:: You can double click the deb file to see details about the package and begin installation. + + Uninstall: + .. code-block:: bash + + sudo apt remove sunshine + + .. tab:: Flatpak Package + + .. caution:: Use distro-specific packages instead of the Flatpak if they are available. + + .. important:: The instructions provided here are for the version supplied in the `latest release`_, which does + not necessarily match the version in the Flathub repository! + + #. Install `Flatpak `__ as required. + #. Download ``sunshine_{arch}.flatpak`` and run the following code. + + .. note:: Be sure to replace ``{arch}`` with the architecture for your operating system. + + System level (recommended) + .. code-block:: bash + + flatpak install --system ./sunshine_{arch}.flatpak + + User level + .. code-block:: bash + + flatpak install --user ./sunshine_{arch}.flatpak + + Additional installation (required) + .. code-block:: bash + + flatpak run --command=additional-install.sh dev.lizardbyte.sunshine + + Start: + X11 and NVFBC capture (X11 Only) + .. code-block:: bash + + flatpak run dev.lizardbyte.sunshine + + KMS capture (Wayland & X11) + .. code-block:: bash + + sudo -i PULSE_SERVER=unix:$(pactl info | awk '/Server String/{print$3}') \ + flatpak run dev.lizardbyte.sunshine + + Uninstall: + .. code-block:: bash + + flatpak run --command=remove-additional-install.sh dev.lizardbyte.sunshine + flatpak uninstall --delete-data dev.lizardbyte.sunshine + + .. tab:: RPM Package + + #. Add `rpmfusion` repositories by running the following code. + + .. code-block:: bash + + sudo dnf install \ + https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm \ + https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-$(rpm -E %fedora).noarch.rpm + + #. Download ``sunshine-{distro}-{distro-version}-{arch}.rpm`` and run the following code. + + .. code-block:: bash + + sudo dnf install ./sunshine-{distro}-{distro-version}-{arch}.rpm + + .. note:: The ``{distro-version}`` is the version of the distro we built the package on. The ``{arch}`` is the + architecture of your operating system. + + .. tip:: You can double click the rpm file to see details about the package and begin installation. + + Uninstall: + .. code-block:: bash + + sudo dnf remove sunshine + + The `deb`, `rpm`, `zst`, `Flatpak` and `AppImage` packages should handle these steps automatically. + Third party packages may not. + + Sunshine needs access to `uinput` to create mouse and gamepad events. + + #. Create and reload `udev` rules for uinput. + .. code-block:: bash + + echo 'KERNEL=="uinput", SUBSYSTEM=="misc", OPTIONS+="static_node=uinput", TAG+="uaccess"' | \ + sudo tee /etc/udev/rules.d/60-sunshine.rules + sudo udevadm control --reload-rules + sudo udevadm trigger + sudo modprobe uinput + + #. Enable permissions for KMS capture. + .. warning:: Capture of most Wayland-based desktop environments will fail unless this step is performed. + + .. note:: ``cap_sys_admin`` may as well be root, except you don't need to be root to run it. It is necessary to + allow Sunshine to use KMS capture. + + **Enable** + .. code-block:: bash + + sudo setcap cap_sys_admin+p $(readlink -f $(which sunshine)) + + **Disable (for Xorg/X11 only)** + .. code-block:: bash + + sudo setcap -r $(readlink -f $(which sunshine)) + + #. Optionally, configure autostart service + + - filename: ``~/.config/systemd/user/sunshine.service`` + - contents: + .. code-block:: cfg + + [Unit] + Description=Sunshine self-hosted game stream host for Moonlight. + StartLimitIntervalSec=500 + StartLimitBurst=5 + + [Service] + ExecStart= + Restart=on-failure + RestartSec=5s + #Flatpak Only + #ExecStop=flatpak kill dev.lizardbyte.sunshine + + [Install] + WantedBy=graphical-session.target + + .. table:: + :widths: auto + + ======== ============================================== =============== + package ExecStart Auto Configured + ======== ============================================== =============== + aur /usr/bin/sunshine ✔ + deb /usr/bin/sunshine ✔ + rpm /usr/bin/sunshine ✔ + AppImage ~/sunshine.AppImage ✔ + Flatpak flatpak run dev.lizardbyte.sunshine ✔ + ======== ============================================== =============== + + **Start once** + .. code-block:: bash + + systemctl --user start sunshine + + **Start on boot** + .. code-block:: bash + + systemctl --user enable sunshine + + #. Reboot + .. code-block:: bash + + sudo reboot now + +.. tab:: macOS + + .. important:: Sunshine on macOS is experimental. Gamepads do not work. + + .. tab:: Homebrew + + #. Install `Homebrew `__ + #. Update the Homebrew sources and install Sunshine. + + .. code-block:: bash + + brew tap LizardByte/homebrew + brew install sunshine + + .. tab:: Portfile + + #. Install `MacPorts `__ + #. Update the Macports sources. + + .. code-block:: bash + + sudo nano /opt/local/etc/macports/sources.conf + + Add this line, replacing your username, below the line that starts with ``rsync``. + ``file:///Users//ports`` + + ``Ctrl+x``, then ``Y`` to exit and save changes. + + #. Download and install by running the following code. + + .. code-block:: bash + + mkdir -p ~/ports/multimedia/sunshine + cd ~/ports/multimedia/sunshine + curl -OL https://github.com/LizardByte/Sunshine/releases/latest/download/Portfile + cd ~/ports + portindex + sudo port install sunshine + + #. The first time you start Sunshine, you will be asked to grant access to screen recording and your microphone. + + #. Optionally, install service + + .. code-block:: bash + + sudo port load Sunshine + + Uninstall: + .. code-block:: bash + + sudo port uninstall sunshine + + Sunshine can only access microphones on macOS due to system limitations. To stream system audio use + `Soundflower `__ or + `BlackHole `__. + + .. note:: Command Keys are not forwarded by Moonlight. Right Option-Key is mapped to CMD-Key. + + .. caution:: Gamepads are not currently supported. + +.. tab:: Windows + + .. tab:: Installer + + #. Download and install ``sunshine-windows-installer.exe`` + + .. attention:: You should carefully select or unselect the options you want to install. Do not blindly install or + enable features. + + To uninstall, find Sunshine in the list `here `__ and select "Uninstall" from the + overflow menu. Different versions of Windows may provide slightly different steps for uninstall. + + .. tab:: Standalone + + .. warning:: By using this package instead of the installer, performance will be reduced. This package is not + recommended for most users. No support will be provided! + + #. Download and extract ``sunshine-windows-portable.zip`` + #. Open command prompt as administrator + #. Firewall rules + + Install: + .. code-block:: bash + + cd /d {path to extracted directory} + scripts/add-firewall-rule.bat + + Uninstall: + .. code-block:: bash + + cd /d {path to extracted directory} + scripts/delete-firewall-rule.bat + + #. Virtual Gamepad Support + + Install: + .. code-block:: bash + + cd /d {path to extracted directory} + scripts/install-gamepad.bat + + Uninstall: + .. code-block:: bash + + cd /d {path to extracted directory} + scripts/uninstall-gamepad.bat + + #. Windows service + + Install: + .. code-block:: bash + + cd /d {path to extracted directory} + scripts/install-service.bat + scripts/autostart-service.bat + + Uninstall: + .. code-block:: bash + + cd /d {path to extracted directory} + scripts/uninstall-service.bat + + To uninstall, delete the extracted directory which contains the ``sunshine.exe`` file. + +Usage +----- +#. If Sunshine is not installed/running as a service, then start sunshine with the following command, unless a start + command is listed in the specified package `install`_ instructions above. + + .. note:: A service is a process that runs in the background. This is the default when installing Sunshine from the + Windows installer. Running multiple instances of Sunshine is not advised. + + **Basic usage** + .. code-block:: bash + + sunshine + + **Specify config file** + .. code-block:: bash + + sunshine /sunshine.conf + + .. note:: You do not need to specify a config file. + If no config file is entered the default location will be used. + + .. attention:: The configuration file specified will be created if it doesn't exist. + + **Start Sunshine over SSH (Linux/X11)** + Assuming you are already logged into the host, you can use this command + + .. code-block:: bash + + ssh @ 'export DISPLAY=:0; sunshine' + + If you are logged into the host with only a tty (teletypewriter), you can use ``startx`` to start the + X server prior to executing sunshine. + You nay need to add ``sleep`` between ``startx`` and ``sunshine`` to allow more time for the display to be ready. + + .. code-block:: bash + + ssh @ 'startx &; export DISPLAY=:0; sunshine' + + .. tip:: You could also utilize the ``~/.bash_profile`` or ``~/.bashrc`` files to setup the ``DISPLAY`` + variable. + + .. seealso:: + + See :ref:`Remote SSH Headless Setup + ` on + how to setup a headless streaming server without autologin and dummy plugs (X11 + NVidia GPUs) + +#. Configure Sunshine in the web ui + + The web ui is available on `https://localhost:47990 `__ by default. You may replace + `localhost` with your internal ip address. + + .. attention:: Ignore any warning given by your browser about "insecure website". This is due to the SSL certificate + being self signed. + + .. caution:: If running for the first time, make sure to note the username and password that you created. + + #. Add games and applications. + #. Adjust any configuration settings as needed. + +#. In Moonlight, you may need to add the PC manually. +#. When Moonlight requests for you insert the pin: + + - Login to the web ui + - Go to "PIN" in the Navbar + - Type in your PIN and press Enter, you should get a Success Message + - In Moonlight, select one of the Applications listed + +Network +------- +The Sunshine user interface will be available on port 47990 by default. + +.. warning:: Exposing ports to the internet can be dangerous. Do this at your own risk. + +Arguments +--------- +To get a list of available arguments run the following: + +.. tab:: General + + .. code-block:: bash + + sunshine --help + +.. tab:: AppImage + + .. code-block:: bash + + ./sunshine.AppImage --help + +.. tab:: Flatpak + + .. code-block:: bash + + flatpak run --command=sunshine dev.lizardbyte.Sunshine --help + +Shortcuts +--------- +All shortcuts start with ``CTRL + ALT + SHIFT``, just like Moonlight + +- ``CTRL + ALT + SHIFT + N`` - Hide/Unhide the cursor (This may be useful for Remote Desktop Mode for Moonlight) +- ``CTRL + ALT + SHIFT + F1/F12`` - Switch to different monitor for Streaming + +Application List +---------------- +- Applications should be configured via the web UI. +- A basic understanding of working directories and commands is required. +- You can use Environment variables in place of values +- ``$(HOME)`` will be replaced by the value of ``$HOME`` +- ``$$`` will be replaced by ``$``, e.g. ``$$(HOME)`` will be become ``$(HOME)`` +- ``env`` - Adds or overwrites Environment variables for the commands/applications run by Sunshine +- ``"Variable name":"Variable value"`` +- ``apps`` - The list of applications +- Advanced users may want to edit the application list manually. The format is ``json``. +- Example ``json`` application: + .. code-block:: json + + { + "cmd": "command to open app", + "detached": [ + "some-command", + "another-command" + ], + "image-path": "/full-path/to/png-image", + "name": "An App", + "output": "/full-path/to/command-log-file", + "prep-cmd": [ + { + "do": "some-command", + "undo": "undo-that-command" + } + ], + "working-dir": "/full-path/to/working-directory" + } + + - ``cmd`` - The main application + - ``detached`` - A list of commands to be run and forgotten about + + - If not specified, a process is started that sleeps indefinitely + + - ``image-path`` - The full path to the cover art image to use. + - ``name`` - The name of the application/game + - ``output`` - The file where the output of the command is stored + - ``auto-detach`` - Specifies whether the app should be treated as detached if it exits quickly + - ``wait-all`` - Specifies whether to wait for all processes to terminate rather than just the initial process + - ``exit-timeout`` - Specifies how long to wait in seconds for the process to gracefully exit (default: 5 seconds) + - ``prep-cmd`` - A list of commands to be run before/after the application + + - If any of the prep-commands fail, starting the application is aborted + - ``do`` - Run before the application + + - If it fails, all ``undo`` commands of the previously succeeded ``do`` commands are run + + - ``undo`` - Run after the application has terminated + + - Failures of ``undo`` commands are ignored + + - ``working-dir`` - The working directory to use. If not specified, Sunshine will use the application directory. + +- For more examples see :ref:`app examples `. + +Considerations +-------------- +- On Windows, Sunshine uses the Desktop Duplication API which only supports capturing from the GPU used for display. + If you want to capture and encode on the eGPU, connect a display or HDMI dummy display dongle to it and run the games + on that display. +- When an application is started, if there is an application already running, it will be terminated. +- When the application has been shutdown, the stream shuts down as well. + + - For example, if you attempt to run ``steam`` as a ``cmd`` instead of ``detached`` the stream will immediately fail. + This is due to the method in which the steam process is executed. Other applications may behave similarly. + - This does not apply to ``detached`` applications. + +- The "Desktop" app works the same as any other application except it has no commands. It does not start an application, + instead it simply starts a stream. If you removed it and would like to get it back, just add a new application with + the name "Desktop" and "desktop.png" as the image path. +- For the Linux flatpak you must prepend commands with ``flatpak-spawn --host``. + +HDR Support +----------- +Streaming HDR content is officially supported on Windows hosts and experimentally supported for Linux hosts. + +- General HDR support information and requirements: + + - HDR must be activated in the host OS, which may require an HDR-capable display or EDID emulator dongle connected to your host PC. + - You must also enable the HDR option in your Moonlight client settings, otherwise the stream will be SDR (and probably overexposed if your host is HDR). + - A good HDR experience relies on proper HDR display calibration both in the OS and in game. HDR calibration can differ significantly between client and host displays. + - You may also need to tune the brightness slider or HDR calibration options in game to the different HDR brightness capabilities of your client's display. + - Some GPUs video encoders can produce lower image quality or encoding performance when streaming in HDR compared to SDR. + +- Additional information: + +.. tab:: Windows + + - HDR streaming is supported for Intel, AMD, and NVIDIA GPUs that support encoding HEVC Main 10 or AV1 10-bit profiles. + - We recommend calibrating the display by streaming the Windows HDR Calibration app to your client device and saving an HDR calibration profile to use while streaming. + - Older games that use NVIDIA-specific NVAPI HDR rather than native Windows HDR support may not display properly in HDR. + +.. tab:: Linux + + - HDR streaming is supported for Intel and AMD GPUs that support encoding HEVC Main 10 or AV1 10-bit profiles using VAAPI. + - The KMS capture backend is required for HDR capture. Other capture methods, like NvFBC or X11, do not support HDR. + - You will need a desktop environment with a compositor that supports HDR rendering, such as Gamescope or KDE Plasma 6. + +.. seealso:: + `Arch wiki on HDR Support for Linux `__ and + `Reddit Guide for HDR Support for AMD GPUs + `__ + +Tutorials and Guides +-------------------- +Tutorial videos are available `here `_. + +Guides are available :doc:`here <./guides/guides>`. + +.. admonition:: Community! + + Tutorials and Guides are community generated. Want to contribute? Reach out to us on our discord server. diff --git a/docs/source/about/third_party_packages.rst b/docs/source/about/third_party_packages.rst index fb921c9360a..3d650928f91 100644 --- a/docs/source/about/third_party_packages.rst +++ b/docs/source/about/third_party_packages.rst @@ -1,61 +1,37 @@ Third Party Packages ==================== -.. Danger:: These packages are not maintained by LizardByte. Use at your own risk. +.. danger:: These packages are not maintained by LizardByte. Use at your own risk. AUR --- -.. image:: https://img.shields.io/badge/dynamic/json?color=blue&label=AUR&style=for-the-badge&query=$.results.0.NumVotes&url=https%3A%2F%2Fapp.lizardbyte.dev%2Funo%2Faur%2Fsunshine.json&logo=archlinux +.. image:: https://img.shields.io/badge/dynamic/json.svg?color=blue&label=AUR&style=for-the-badge&query=$.results.0.NumVotes&url=https%3A%2F%2Fapp.lizardbyte.dev%2Funo%2Faur%2Fsunshine.json&logo=archlinux :alt: AUR votes :target: https://aur.archlinux.org/packages/sunshine Chocolatey ---------- -.. image:: https://img.shields.io/chocolatey/v/sunshine?style=for-the-badge&logo=chocolatey +.. image:: https://img.shields.io/badge/dynamic/xml.svg?color=orange&label=chocolatey&style=for-the-badge&prefix=v&query=%2F%2Ftr%5B%40id%3D%27chocolatey%27%5D%2Ftd%5B3%5D%2Fspan%2Fa&url=https%3A%2F%2Frepology.org%2Fproject%2Fsunshine%2Fversions&logo=chocolatey :alt: Chocolatey Version :target: https://community.chocolatey.org/packages/sunshine -.. image:: https://img.shields.io/chocolatey/dt/sunshine?style=for-the-badge&logo=chocolatey - :alt: Chocolatey - nixpkgs ------- -.. image:: https://img.shields.io/badge/dynamic/xml?color=orange&label=nixpkgs&style=for-the-badge&prefix=v&query=%2F%2Ftr%5B%40id%3D%27nix_unstable%27%5D%2Ftd%5B3%5D%2Fspan%2Fa&url=https%3A%2F%2Frepology.org%2Fproject%2Fsunshine%2Fversions&logo=nixos +.. image:: https://img.shields.io/badge/dynamic/xml.svg?color=orange&label=nixpkgs&style=for-the-badge&prefix=v&query=%2F%2Ftr%5B%40id%3D%27nix_unstable%27%5D%2Ftd%5B3%5D%2Fspan%2Fa&url=https%3A%2F%2Frepology.org%2Fproject%2Fsunshine%2Fversions&logo=nixos :alt: nixpgs Version - :target: https://github.com/NixOS/nixpkgs/blob/master/pkgs/servers/sunshine/default.nix + :target: https://github.com/NixOS/nixpkgs/blob/nixos-unstable/pkgs/servers/sunshine/default.nix Scoop ----- -.. image:: https://img.shields.io/scoop/v/sunshine?bucket=extras&style=for-the-badge +.. image:: https://img.shields.io/scoop/v/sunshine.svg?bucket=extras&style=for-the-badge&logo=data:image/vnd.microsoft.icon;base64,AAABAAEAEBAAAAEAGAAhAwAAFgAAAIlQTkcNChoKAAAADUlIRFIAAAAQAAAAEAgGAAAAH/P/YQAAAuhJREFUOE9tk1tIFFEYx7+ZXdfbrhdMElJLFCykCxL20MUW9UkkqeiOFGSWYW75EvjgVlJmlpkaJV5SMtQlMYjEROqpQoiMMEpRW2/p6q67bTuXM2dmOjPu2moNDHPm4/v/Zs7//D9KlmUNAMjkBoqiJOVJapTyqqzXXn49tCohzbRSVERPSi7tokFOSkne2rmzoED4H6C0pHwjT2G2qspsU7U+wBuzWTs8M9mlpen0YEOoMS/73DjrnMuhXFyiLEmjwZH6vmufR5DDNtHBI7b9cWNNpw9AgcVCtw6+P8R43KdkjHMM+vDqI/tywyiN5oy46KQpLEogiG0149+7rG5HGRK5o01N9VYVoPxm/ZXCOMrD95NloihiOj4qhs1K3R8IbqQFogVJAuRifrXNT3wactkGmpvrbni9UregQu7nn87X0XB3w+ZYfcruHRAVJgNtE0EclmCGM8CYC2DE5UK8TJXtzT1ZZTRSeJUHiqOvW29Vb89KKw4kYgEvgIQFGHurg3l7AlitS8CzAohYZgQB5ZU9Ovx8FcBkMkdcKEx5GL1ee1yWGcKjgWMQfHgVDVOjNPD88qHwHAYOe57GbHOcLSoqQiunYC4tT4tL0NYmbwkOx1hO1ukABITg40AkOO0BJCgiYFEAl9sBjGj/pl+nyairq5xdAdy50xbKuH+eFyUMkijdJtHQCAIGxiOQYC0nguMYmJqeVJJW29vfU7wqSErDzeuV6aQ5lUPoIjn7RI5FRIRUMQkbLC05YN42txgaEpTd89IyuNZEaGlpCZqdXsjHAj5Avp7h+c2CIIiqGGMMMzNTgDD/oLev57I3vX+T6IttRUVNvNvpusey3EGeE5QtAkI82B12YFjmXagh5ER39zOrfw7UWfDPvcl0ddP0j+lGjucylDoiZhIbvkboDccsL9q/+Hr/2YI/JDMzZ4/IIyMhRyh1XYBmKCEptqOhoWFlyHwAZZxX/YHXNK/3/tiVUfcV6T8hxMYSf1PeGAAAAABJRU5ErkJggg== :alt: Scoop Version (extras bucket) :target: https://scoop.sh/#/apps?s=0&d=1&o=true&q=sunshine Solus ----- -.. image:: https://img.shields.io/badge/dynamic/xml?color=orange&label=Solus&style=for-the-badge&prefix=v&query=%2F%2Ftr%5B%40id%3D%27solus%27%5D%2Ftd%5B3%5D%2Fspan%2Fa&url=https%3A%2F%2Frepology.org%2Fproject%2Fsunshine%2Fversions&logo=solus +.. image:: https://img.shields.io/badge/dynamic/xml.svg?color=orange&label=Solus&style=for-the-badge&prefix=v&query=%2F%2Ftr%5B%40id%3D%27solus%27%5D%2Ftd%5B3%5D%2Fspan%2Fa&url=https%3A%2F%2Frepology.org%2Fproject%2Fsunshine%2Fversions&logo=solus :alt: Solus Version :target: https://dev.getsol.us/source/sunshine - -Winget ------- -.. image:: https://img.shields.io/badge/dynamic/xml?color=orange&label=Winget&style=for-the-badge&prefix=v&query=%2F%2Ftr%5B%40id%3D%27winget%27%5D%2Ftd%5B3%5D%2Fspan%2Fa&url=https%3A%2F%2Frepology.org%2Fproject%2Fsunshine%2Fversions&logo=microsoft - :alt: Winget Version - :target: https://github.com/microsoft/winget-pkgs/tree/master/manifests/l/LizardByte/Sunshine - -Legacy GitHub Repo ------------------- - -.. Attention:: This repo is not maintained. Thank you to Loki for bringing this amazing project to life! - -.. image:: https://img.shields.io/static/v1?label=repo&message=loki-47-6F-64/sunshine&color=blue&style=for-the-badge&logo=github - :alt: GitHub Maintainer - :target: https://github.com/loki-47-6F-64/sunshine/releases - -.. image:: https://img.shields.io/github/last-commit/loki-47-6F-64/sunshine?style=for-the-badge&logo=github - :alt: GitHub last commit - -.. image:: https://img.shields.io/github/release-date/loki-47-6F-64/sunshine?style=for-the-badge&logo=github - :alt: GitHub Release Date diff --git a/docs/source/about/usage.rst b/docs/source/about/usage.rst deleted file mode 100644 index fa680ba6c2d..00000000000 --- a/docs/source/about/usage.rst +++ /dev/null @@ -1,279 +0,0 @@ -Usage -===== -#. See the `setup`_ section for your specific OS. -#. If you did not install the service, then start sunshine with the following command, unless a start command is listed - in the specified package :ref:`installation ` instructions. - - .. Note:: A service is a process that runs in the background. Running multiple instances of Sunshine is not - advised. - - **Basic usage** - .. code-block:: bash - - sunshine - - **Specify config file** - .. code-block:: bash - - sunshine /sunshine.conf - - .. Note:: You do not need to specify a config file. If no config file is entered the default location will be used. - - .. Attention:: The configuration file specified will be created if it doesn't exist. - -#. Configure Sunshine in the web ui - - The web ui is available on `https://localhost:47990 `_ by default. You may replace - `localhost` with your internal ip address. - - .. Attention:: Ignore any warning given by your browser about "insecure website". This is due to the SSL certificate - being self signed. - - .. Caution:: If running for the first time, make sure to note the username and password that you created. - - **Add games and applications.** - This can be configured in the web ui. - - .. Note:: Additionally, apps can be configured manually. `src_assets//config/apps.json` is an example of a - list of applications that are started just before running a stream. This is the directory within the GitHub - repo. - -#. In Moonlight, you may need to add the PC manually. -#. When Moonlight request you insert the correct pin on sunshine: - - - Login to the web ui - - Go to "PIN" in the Navbar - - Type in your PIN and press Enter, you should get a Success Message - - In Moonlight, select one of the Applications listed - -Network -------- -The Sunshine user interface will be available on port 47990 by default. - -.. Warning:: Exposing ports to the internet can be dangerous. Do this at your own risk. - -Arguments ---------- -To get a list of available arguments run the following: - .. code-block:: bash - - sunshine --help - -Setup ------ - -Linux -^^^^^ -The `deb`, `rpm`, `Flatpak` and `AppImage` packages handle these steps automatically. Third party packages may not. - -Sunshine needs access to `uinput` to create mouse and gamepad events. - -#. Create `udev` rules. - .. code-block:: bash - - echo 'KERNEL=="uinput", SUBSYSTEM=="misc", OPTIONS+="static_node=uinput", TAG+="uaccess"' | \ - sudo tee /etc/udev/rules.d/85-sunshine.rules - -#. Optionally, configure autostart service - - - filename: ``~/.config/systemd/user/sunshine.service`` - - contents: - .. code-block:: - - [Unit] - Description=Sunshine self-hosted game stream host for Moonlight. - StartLimitIntervalSec=500 - StartLimitBurst=5 - - [Service] - ExecStart= - Restart=on-failure - RestartSec=5s - #Flatpak Only - #ExecStop=flatpak kill dev.lizardbyte.sunshine - - [Install] - WantedBy=graphical-session.target - - .. table:: - :widths: auto - - ======== ============================================== =============== - package ExecStart Auto Configured - ======== ============================================== =============== - aur /usr/bin/sunshine ✔ - deb /usr/bin/sunshine ✔ - rpm /usr/bin/sunshine ✔ - AppImage ~/sunshine.AppImage ✔ - Flatpak flatpak run dev.lizardbyte.sunshine ✔ - ======== ============================================== =============== - - **Start once** - .. code-block:: bash - - systemctl --user start sunshine - - **Start on boot** - .. code-block:: bash - - systemctl --user enable sunshine - -#. Additional Setup for KMS - .. Note:: ``cap_sys_admin`` may as well be root, except you don't need to be root to run it. It is necessary to - allow Sunshine to use KMS. - - **Enable** - .. code-block:: bash - - sudo setcap cap_sys_admin+p $(readlink -f $(which sunshine)) - - **Disable (for Xorg/X11)** - .. code-block:: bash - - sudo setcap -r $(readlink -f $(which sunshine)) - -#. Reboot - .. code-block:: bash - - sudo reboot now - -macOS -^^^^^ -Sunshine can only access microphones on macOS due to system limitations. To stream system audio use -`Soundflower `_ or -`BlackHole `_. - -.. Note:: Command Keys are not forwarded by Moonlight. Right Option-Key is mapped to CMD-Key. - -.. Caution:: Gamepads are not currently supported. - -Configure autostart service - **MacPorts** - .. code-block:: bash - - sudo port load Sunshine - -Windows -^^^^^^^ -For gamepad support, install `ViGEmBus `_ - -Sunshine firewall - **Add rule** - .. code-block:: batch - - cd /d "C:\Program Files\Sunshine\scripts" - add-firewall-rule.bat - - **Remove rule** - .. code-block:: batch - - cd /d "C:\Program Files\Sunshine\scripts" - remove-firewall-rule.bat - -Sunshine service - **Enable** - .. code-block:: batch - - cd /d "C:\Program Files\Sunshine\scripts" - install-service.bat - - **Disable** - .. code-block:: batch - - cd /d "C:\Program Files\Sunshine\scripts" - uninstall-service.bat - -Shortcuts ---------- -All shortcuts start with ``CTRL + ALT + SHIFT``, just like Moonlight - -- ``CTRL + ALT + SHIFT + N`` - Hide/Unhide the cursor (This may be useful for Remote Desktop Mode for Moonlight) -- ``CTRL + ALT + SHIFT + F1/F12`` - Switch to different monitor for Streaming - -Application List ----------------- -- Applications should be configured via the web UI. -- A basic understanding of working directories and commands is required. -- You can use Environment variables in place of values -- ``$(HOME)`` will be replaced by the value of ``$HOME`` -- ``$$`` will be replaced by ``$``, e.g. ``$$(HOME)`` will be become ``$(HOME)`` -- ``env`` - Adds or overwrites Environment variables for the commands/applications run by Sunshine -- ``"Variable name":"Variable value"`` -- ``apps`` - The list of applications -- Advanced users may want to edit the application list manually. The format is ``json``. -- Example ``json`` application: - .. code-block:: json - - { - "cmd": "command to open app", - "detached": [ - "some-command", - "another-command" - ], - "image-path": "/full-path/to/png-image", - "name": "An App", - "output": "/full-path/to/command-log-file", - "prep-cmd": [ - { - "do": "some-command", - "undo": "undo-that-command" - } - ], - "working-dir": "/full-path/to/working-directory" - } - - - ``cmd`` - The main application - - ``detached`` - A list of commands to be run and forgotten about - - - If not specified, a process is started that sleeps indefinitely - - - ``image-path`` - The full path to the cover art image to use. - - ``name`` - The name of the application/game - - ``output`` - The file where the output of the command is stored - - ``prep-cmd`` - A list of commands to be run before/after the application - - - If any of the prep-commands fail, starting the application is aborted - - ``do`` - Run before the application - - - If it fails, all ``undo`` commands of the previously succeeded ``do`` commands are run - - - ``undo`` - Run after the application has terminated - - - Failures of ``undo`` commands are ignored - - - ``working-dir`` - The working directory to use. If not specified, Sunshine will use the application directory. - -- For more examples see :ref:`app examples `. - -Considerations --------------- -- When an application is started, if there is an application already running, it will be terminated. -- When the application has been shutdown, the stream shuts down as well. - - - For example, if you attempt to run ``steam`` as a ``cmd`` instead of ``detached`` the stream will immediately fail. - This is due to the method in which the steam process is executed. Other applications may behave similarly. - -- The "Desktop" app works the same as any other application except it has no commands. It does not start an application, - instead it simply starts a stream. If you removed it and would like to get it back, just add a new application with - the name "Desktop" and "desktop.png" as the image path. -- For the Linux flatpak you must prepend commands with ``flatpak-spawn --host``. - -HDR Support ------------ -Streaming HDR content is supported for Windows hosts with NVIDIA, AMD, or Intel GPUs that support encoding HEVC Main 10. -You must have an HDR-capable display or EDID emulator dongle connected to your host PC to activate HDR in Windows. - -- Ensure you enable the HDR option in your Moonlight client settings, otherwise the stream will be SDR. -- A good HDR experience relies on proper HDR display calibration both in Windows and in game. HDR calibration can differ significantly between client and host displays. -- We recommend calibrating the display by streaming the Windows HDR Calibration app to your client device and saving an HDR calibration profile to use while streaming. -- You may also need to tune the brightness slider or HDR calibration options in game to the different HDR brightness capabilities of your client's display. -- Older games that use NVIDIA-specific NVAPI HDR rather than native Windows 10 OS HDR support may not display in HDR. -- Some GPUs can produce lower image quality or encoding performance when streaming in HDR compared to SDR. - -Tutorials ---------- -Tutorial videos are available `here `_. - -.. admonition:: Community! - - Tutorials are community generated. Want to contribute? Reach out to us on our discord server. diff --git a/docs/source/building/build.rst b/docs/source/building/build.rst index 013c9709a60..97693205ae4 100644 --- a/docs/source/building/build.rst +++ b/docs/source/building/build.rst @@ -1,6 +1,6 @@ Build ===== -Sunshine binaries are built using `CMake `_. Cross compilation is not +Sunshine binaries are built using `CMake `__. Cross compilation is not supported. That means the binaries must be built on the target operating system and architecture. Building Locally @@ -8,7 +8,7 @@ Building Locally Clone ^^^^^ -Ensure `git `_ is installed and run the following: +Ensure `git `__ is installed and run the following: .. code-block:: bash git clone https://github.com/lizardbyte/sunshine.git --recurse-submodules diff --git a/docs/source/building/linux.rst b/docs/source/building/linux.rst index 2f629efbb7b..d0d0af6ed9c 100644 --- a/docs/source/building/linux.rst +++ b/docs/source/building/linux.rst @@ -4,9 +4,10 @@ Linux Requirements ------------ -Debian Bullseye -^^^^^^^^^^^^^^^ -End of Life: TBD +Debian Bullseye/Bookworm +^^^^^^^^^^^^^^^^^^^^^^^^ +End of Life (Bullseye): July, 2024 +End of Life (Bookworm): TBD Install Requirements .. code-block:: bash @@ -14,22 +15,23 @@ Install Requirements sudo apt update && sudo apt install \ build-essential \ cmake \ - libavdevice-dev \ + libayatana-appindicator3-dev \ libboost-filesystem-dev \ libboost-locale-dev \ libboost-log-dev \ libboost-program-options-dev \ - libboost-thread-dev \ libcap-dev \ # KMS libcurl4-openssl-dev \ libdrm-dev \ # KMS libevdev-dev \ + libminiupnpc-dev \ libmfx-dev \ # x86_64 only + libnotify-dev \ libnuma-dev \ libopus-dev \ libpulse-dev \ libssl-dev \ - libva-dev \ + libva-dev \ # VA-API libvdpau-dev \ libwayland-dev \ # Wayland libx11-dev \ # X11 @@ -44,9 +46,8 @@ Install Requirements nvidia-cuda-dev \ # Cuda, NvFBC nvidia-cuda-toolkit # Cuda, NvFBC -Fedora 36, 37 +Fedora 39, 40 ^^^^^^^^^^^^^ -End of Life: TBD Install Requirements .. code-block:: bash @@ -64,7 +65,8 @@ Install Requirements libcurl-devel \ libdrm-devel \ libevdev-devel \ - libva-devel \ + libnotify-devel \ + libva-devel \ # VA-API libvdpau-devel \ libX11-devel \ # X11 libxcb-devel \ # X11 @@ -75,6 +77,7 @@ Install Requirements libXrandr-devel \ # X11 libXtst-devel \ # X11 mesa-libGL-devel \ + miniupnpc-devel \ npm \ numactl-devel \ openssl-devel \ @@ -84,9 +87,8 @@ Install Requirements wget \ # necessary for cuda install with `run` file which # necessary for cuda install with `run` file -Ubuntu 20.04 +Ubuntu 22.04 ^^^^^^^^^^^^ -End of Life: April 2030 Install Requirements .. code-block:: bash @@ -94,24 +96,23 @@ Install Requirements sudo apt update && sudo apt install \ build-essential \ cmake \ - g++-10 \ libappindicator3-dev \ - libavdevice-dev \ libboost-filesystem-dev \ libboost-locale-dev \ libboost-log-dev \ - libboost-thread-dev \ libboost-program-options-dev \ libcap-dev \ # KMS + libcurl4-openssl-dev \ libdrm-dev \ # KMS libevdev-dev \ + libminiupnpc-dev \ libmfx-dev \ # x86_64 only + libnotify-dev \ libnuma-dev \ libopus-dev \ libpulse-dev \ libssl-dev \ - libva-dev \ - libvdpau-dev \ + libva-dev \ # VA-API libwayland-dev \ # Wayland libx11-dev \ # X11 libxcb-shm0-dev \ # X11 @@ -122,21 +123,11 @@ Install Requirements libxtst-dev \ # X11 nodejs \ npm \ - wget # necessary for cuda install with `run` file - -Update gcc alias - .. code-block:: bash - - update-alternatives --install \ - /usr/bin/gcc gcc /usr/bin/gcc-10 100 \ - --slave /usr/bin/g++ g++ /usr/bin/g++-10 \ - --slave /usr/bin/gcov gcov /usr/bin/gcov-10 \ - --slave /usr/bin/gcc-ar gcc-ar /usr/bin/gcc-ar-10 \ - --slave /usr/bin/gcc-ranlib gcc-ranlib /usr/bin/gcc-ranlib-10 + nvidia-cuda-dev \ # CUDA, NvFBC + nvidia-cuda-toolkit # CUDA, NvFBC -Ubuntu 22.04 +Ubuntu 24.04 ^^^^^^^^^^^^ -End of Life: April 2027 Install Requirements .. code-block:: bash @@ -144,21 +135,25 @@ Install Requirements sudo apt update && sudo apt install \ build-essential \ cmake \ + gcc-11 \ + g++-11 \ libappindicator3-dev \ - libavdevice-dev \ libboost-filesystem-dev \ libboost-locale-dev \ libboost-log-dev \ - libboost-thread-dev \ libboost-program-options-dev \ libcap-dev \ # KMS + libcurl4-openssl-dev \ libdrm-dev \ # KMS libevdev-dev \ + libminiupnpc-dev \ libmfx-dev \ # x86_64 only + libnotify-dev \ libnuma-dev \ libopus-dev \ libpulse-dev \ libssl-dev \ + libva-dev \ # VA-API libwayland-dev \ # Wayland libx11-dev \ # X11 libxcb-shm0-dev \ # X11 @@ -172,34 +167,38 @@ Install Requirements nvidia-cuda-dev \ # CUDA, NvFBC nvidia-cuda-toolkit # CUDA, NvFBC +Update gcc alias + .. code-block:: bash + + update-alternatives --install \ + /usr/bin/gcc gcc /usr/bin/gcc-11 100 \ + --slave /usr/bin/g++ g++ /usr/bin/g++-11 \ + --slave /usr/bin/gcov gcov /usr/bin/gcov-11 \ + --slave /usr/bin/gcc-ar gcc-ar /usr/bin/gcc-ar-11 \ + --slave /usr/bin/gcc-ranlib gcc-ranlib /usr/bin/gcc-ranlib-11 + CUDA ---- If the version of CUDA available from your distro is not adequate, manually install CUDA. -.. Tip:: The version of CUDA you use will determine compatibility with various GPU generations. - See `CUDA compatibility `_ for more info. +.. tip:: The version of CUDA you use will determine compatibility with various GPU generations. + At the time of writing, the recommended version to use is CUDA ~11.8. + See `CUDA compatibility `__ for more info. Select the appropriate run file based on your desired CUDA version and architecture according to - `CUDA Toolkit Archive `_. + `CUDA Toolkit Archive `__. .. code-block:: bash - wget https://developer.download.nvidia.com/compute/cuda/11.4.2/local_installers/cuda_11.4.2_470.57.02_linux.run \ + wget https://developer.download.nvidia.com/compute/cuda/11.8.0/local_installers/cuda_11.8.0_520.61.05_linux.run \ --progress=bar:force:noscroll -q --show-progress -O ./cuda.run chmod a+x ./cuda.run ./cuda.run --silent --toolkit --toolkitpath=/usr --no-opengl-libs --no-man-page --no-drm rm ./cuda.run -npm dependencies ----------------- -Install npm dependencies. - .. code-block:: bash - - npm install - Build ----- -.. Attention:: Ensure you are in the build directory created during the clone step earlier before continuing. +.. attention:: Ensure you are in the build directory created during the clone step earlier before continuing. .. code-block:: bash diff --git a/docs/source/building/macos.rst b/docs/source/building/macos.rst index 5ff3e74e980..1b874d71a11 100644 --- a/docs/source/building/macos.rst +++ b/docs/source/building/macos.rst @@ -5,40 +5,47 @@ Requirements ------------ macOS Big Sur and Xcode 12.5+ -Use either `MacPorts `_ or `Homebrew `_ +Use either `MacPorts `__ or `Homebrew `__ MacPorts """""""" Install Requirements .. code-block:: bash - sudo port install avahi boost180 cmake curl libopus npm9 pkgconfig + sudo port install avahi boost180 cmake curl doxygen graphviz libopus miniupnpc npm9 pkgconfig python311 py311-pip Homebrew """""""" Install Requirements .. code-block:: bash - brew install boost cmake node opus - # if there are issues with an SSL header that is not found: - cd /usr/local/include - ln -s ../opt/openssl/include/openssl . + brew install boost cmake doxygen graphviz miniupnpc node opus pkg-config python@3.11 -npm dependencies ----------------- -Install npm dependencies. - .. code-block:: bash +If there are issues with an SSL header that is not found: + .. tab:: Intel + + .. code-block:: bash + + pushd /usr/local/include + ln -s ../opt/openssl/include/openssl . + popd + + .. tab:: Apple Silicon + + .. code-block:: bash - npm install + pushd /opt/homebrew/include + ln -s ../opt/openssl/include/openssl . + popd Build ----- -.. Attention:: Ensure you are in the build directory created during the clone step earlier before continuing. +.. attention:: Ensure you are in the build directory created during the clone step earlier before continuing. .. code-block:: bash cmake .. - make -j ${nproc} + make -j $(sysctl -n hw.ncpu) cpack -G DragNDrop # optionally, create a macOS dmg package diff --git a/docs/source/building/windows.rst b/docs/source/building/windows.rst index 0cfb53a488a..084c492f59a 100644 --- a/docs/source/building/windows.rst +++ b/docs/source/building/windows.rst @@ -3,7 +3,7 @@ Windows Requirements ------------ -First you need to install `MSYS2 `_, then startup "MSYS2 MinGW 64-bit" and execute the following +First you need to install `MSYS2 `__, then startup "MSYS2 UCRT64" and execute the following codes. Update all packages: @@ -14,23 +14,29 @@ Update all packages: Install dependencies: .. code-block:: bash - pacman -S base-devel cmake diffutils gcc git make mingw-w64-x86_64-binutils \ - mingw-w64-x86_64-boost mingw-w64-x86_64-cmake mingw-w64-x86_64-curl \ - mingw-w64-x86_64-libmfx mingw-w64-x86_64-openssl mingw-w64-x86_64-opus \ - mingw-w64-x86_64-toolchain - -npm dependencies ----------------- -Install nodejs and npm. Downloads available `here `_. - -Install npm dependencies. - .. code-block:: bash - - npm install + pacman -S \ + doxygen \ + git \ + mingw-w64-ucrt-x86_64-boost \ + mingw-w64-ucrt-x86_64-cmake \ + mingw-w64-ucrt-x86_64-cppwinrt \ + mingw-w64-ucrt-x86_64-curl \ + mingw-w64-ucrt-x86_64-graphviz \ + mingw-w64-ucrt-x86_64-miniupnpc \ + mingw-w64-ucrt-x86_64-nlohmann-json \ + mingw-w64-ucrt-x86_64-nodejs \ + mingw-w64-ucrt-x86_64-nsis \ + mingw-w64-ucrt-x86_64-onevpl \ + mingw-w64-ucrt-x86_64-openssl \ + mingw-w64-ucrt-x86_64-opus \ + mingw-w64-ucrt-x86_64-rust \ + mingw-w64-ucrt-x86_64-toolchain \ + python \ + python-pip Build ----- -.. Attention:: Ensure you are in the build directory created during the clone step earlier before continuing. +.. attention:: Ensure you are in the build directory created during the clone step earlier before continuing. .. code-block:: bash diff --git a/docs/source/conf.py b/docs/source/conf.py index e581783865f..1b66fe09572 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -7,7 +7,6 @@ # standard imports from datetime import datetime import os -import re import subprocess @@ -27,16 +26,8 @@ author = 'ReenigneArcher' # The full version, including alpha/beta/rc tags -with open(os.path.join(root_dir, 'CMakeLists.txt'), 'r') as f: - version = re.search(r"project\(Sunshine VERSION ((\d+)\.(\d+)\.(\d+))", str(f.read())).group(1) -""" -To use cmake method for obtaining version instead of regex, -1. Within CMakeLists.txt add the following line without backticks: - ``configure_file(docs/source/conf.py.in "${CMAKE_CURRENT_SOURCE_DIR}/docs/source/conf.py" @ONLY)`` -2. Rename this file to ``conf.py.in`` -3. Uncomment the next line -""" -# version = '@PROJECT_VERSION@' # use this for cmake configure_file method +# https://docs.readthedocs.io/en/stable/reference/environment-variables.html#envvar-READTHEDOCS_VERSION +version = os.getenv('READTHEDOCS_VERSION', 'dirty') # -- General configuration --------------------------------------------------- @@ -51,6 +42,7 @@ 'sphinx.ext.graphviz', # enable graphs for breathe 'sphinx.ext.viewcode', # add links to view source code 'sphinx_copybutton', # add a copy button to code blocks + 'sphinx_inline_tabs', # add tabs ] # Add any paths that contain templates here, relative to this directory. @@ -68,7 +60,7 @@ # -- Options for HTML output ------------------------------------------------- # images -html_favicon = os.path.join(root_dir, 'src_assets', 'common', 'assets', 'web', 'images', 'favicon.ico') +html_favicon = os.path.join(root_dir, 'src_assets', 'common', 'assets', 'web', 'public', 'images', 'sunshine.ico') html_logo = os.path.join(root_dir, 'sunshine.png') # Add any paths that contain custom static files (such as style sheets) here, @@ -82,7 +74,7 @@ html_theme_options = { "top_of_page_button": "edit", - "source_edit_link": "https://github.com/lizardbyte/sunshine/tree/nightly/docs/source/{filename}", + "source_edit_link": "https://github.com/lizardbyte/sunshine/tree/master/docs/source/{filename}", } # extension config options @@ -104,6 +96,17 @@ doxy_version = doxy_proc.stdout.decode('utf-8').strip() print('doxygen version: ' + doxy_version) +# create build directories, as doxygen fails to create it in macports and docker +directories = [ + os.path.join(source_dir, 'build'), + os.path.join(source_dir, 'build', 'doxyxml'), +] +for d in directories: + os.makedirs( + name=d, + exist_ok=True, + ) + # run doxygen doxy_proc = subprocess.run('doxygen Doxyfile', shell=True, cwd=source_dir) if doxy_proc.returncode != 0: diff --git a/docs/source/contributing/contributing.rst b/docs/source/contributing/contributing.rst index 217bda130bd..ab85e13deee 100644 --- a/docs/source/contributing/contributing.rst +++ b/docs/source/contributing/contributing.rst @@ -2,4 +2,24 @@ Contributing ============ Read our contribution guide in our organization level -`docs `_. +`docs `__. + +Web UI +------ +The Web UI uses `Vite `__ as its build system, to handle the integration of the NPM libraries. + +The HTML pages used by the Web UI are found in ``src_assets/common/assets/web``. + +`EJS `__ is used as a templating system for the pages (check ``template_header.html`` and ``template_header_main.html``). + +The Style System is provided by `Bootstrap `__. + +The JS framework used by the more interactive pages is `Vue `__. + +Building +^^^^^^^^ +Sunshine already builds the UI as part of its build process, but you can make faster changes by starting vite manually. + +.. code-block:: bash + + npm run dev \ No newline at end of file diff --git a/docs/source/contributing/localization.rst b/docs/source/contributing/localization.rst index dc3e26da877..617cba48399 100644 --- a/docs/source/contributing/localization.rst +++ b/docs/source/contributing/localization.rst @@ -1,26 +1,14 @@ Localization ============ -Sunshine is being localized into various languages. The default language is `en` (English) and is highlighted green. +Sunshine and related LizardByte projects are being localized into various languages. The default language is +`en` (English). -.. image:: https://img.shields.io/badge/dynamic/json?color=blue&label=de&style=for-the-badge&query=%24.progress.0.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15178612-503956.json -.. image:: https://img.shields.io/badge/dynamic/json?color=green&label=en&style=for-the-badge&query=%24.progress.1.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15178612-503956.json -.. image:: https://img.shields.io/badge/dynamic/json?color=blue&label=en-GB&style=for-the-badge&query=%24.progress.2.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15178612-503956.json -.. image:: https://img.shields.io/badge/dynamic/json?color=blue&label=en-US&style=for-the-badge&query=%24.progress.3.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15178612-503956.json -.. image:: https://img.shields.io/badge/dynamic/json?color=blue&label=es-ES&style=for-the-badge&query=%24.progress.4.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15178612-503956.json -.. image:: https://img.shields.io/badge/dynamic/json?color=blue&label=fr&style=for-the-badge&query=%24.progress.5.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15178612-503956.json -.. image:: https://img.shields.io/badge/dynamic/json?color=blue&label=it&style=for-the-badge&query=%24.progress.6.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15178612-503956.json -.. image:: https://img.shields.io/badge/dynamic/json?color=blue&label=ru&style=for-the-badge&query=%24.progress.7.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15178612-503956.json - -Graph - .. image:: https://badges.awesome-crowdin.com/translation-15178612-503956.png + .. image:: https://app.lizardbyte.dev/uno/crowdin/LizardByte_graph.svg CrowdIn ------- -The translations occur on -`CrowdIn `_. Feel free to contribute to localization there. -Only elements of the API are planned to be translated. - -.. Attention:: The rest API has not yet been implemented. +The translations occur on `CrowdIn `__. Anyone is free to contribute to +localization there. **Translations Basics** - The brand names `LizardByte` and `Sunshine` should never be translated. @@ -36,46 +24,90 @@ Only elements of the API are planned to be translated. When a change is made to sunshine source code, a workflow generates new translation templates that get pushed to CrowdIn automatically. - When translations are updated on CrowdIn, a push gets made to the `l10n_nightly` branch and a PR is made against the - `nightly` branch. Once PR is merged, all updated translations are part of the project and will be included in the + When translations are updated on CrowdIn, a push gets made to the `l10n_master` branch and a PR is made against the + `master` branch. Once PR is merged, all updated translations are part of the project and will be included in the next release. Extraction ---------- -There should be minimal cases where strings need to be extracted from source code; however it may be necessary in some -situations. For example if a system tray icon is added it should be localized as it is user interfacing. -- Wrap the string to be extracted in a function as shown. - .. code-block:: cpp +.. tab:: UI + + Sunshine uses `Vue I18n `__ for localizing the UI. + The following is a simple example of how to use it. + + - Add the string to `src_assets/common/assets/web/public/assets/locale/en.json`, in English. + .. code-block:: json + + { + "index": { + "welcome": "Hello, Sunshine!" + } + } + + .. note:: The json keys should be sorted alphabetically. You can use `jsonabc `__ + to sort the keys. + + .. attention:: Due to the integration with Crowdin, it is important to only add strings to the `en.json` file, + and to not modify any other language files. After the PR is merged, the translations can take place + on `CrowdIn `__. Once the translations are complete, a PR will be made + to merge the translations into Sunshine. + + - Use the string in a Vue component. + .. code-block:: html + + + + .. tip:: More formatting examples can be found in the + `Vue I18n guide `__. + +.. tab:: C++ + + There should be minimal cases where strings need to be extracted from C++ source code; however it may be necessary in + some situations. For example the system tray icon could be localized as it is user interfacing. + + - Wrap the string to be extracted in a function as shown. + .. code-block:: cpp + + #include + #include + + std::string msg = boost::locale::translate("Hello world!"); - #include - boost::locale::translate("Hello world!") + .. tip:: More examples can be found in the documentation for + `boost locale `__. -.. Tip:: More examples can be found in the documentation for - `boost locale `_. + .. warning:: This is for information only. Contributors should never include manually updated template files, or + manually compiled language files in Pull Requests. -.. Warning:: This is for information only. Contributors should never include manually updated template files, or - manually compiled language files in Pull Requests. + Strings are automatically extracted from the code to the `locale/sunshine.po` template file. The generated file is + used by CrowdIn to generate language specific template files. The file is generated using the + `.github/workflows/localize.yml` workflow and is run on any push event into the `master` branch. Jobs are only run if + any of the following paths are modified. -Strings are automatically extracted from the code to the `locale/sunshine.po` template file. The generated file is -used by CrowdIn to generate language specific template files. The file is generated using the -`.github/workflows/localize.yml` workflow and is run on any push event into the `nightly` branch. Jobs are only run if -any of the following paths are modified. + .. code-block:: yaml -.. code-block:: yaml + - 'src/**' - - 'src/**' + When testing locally it may be desirable to manually extract, initialize, update, and compile strings. Python is + required for this, along with the python dependencies in the `./scripts/requirements.txt` file. Additionally, + `xgettext `__ must be installed. -When testing locally it may be desirable to manually extract, initialize, update, and compile strings. Python is -required for this, along with the python dependencies in the `./scripts/requirements.txt` file. Additionally, -`xgettext `_ must be installed. + **Extract, initialize, and update** + .. code-block:: bash -**Extract, initialize, and update** - .. code-block:: bash + python ./scripts/_locale.py --extract --init --update - python ./scripts/_locale.py --extract --init --update + **Compile** + .. code-block:: bash -**Compile** - .. code-block:: bash + python ./scripts/_locale.py --compile - python ./scripts/_locale.py --compile + .. attention:: Due to the integration with Crowdin, it is important to not include any extracted or compiled files in + Pull Requests. The files are automatically generated and updated by the workflow. Once the PR is merged, the + translations can take place on `CrowdIn `__. Once the translations are + complete, a PR will be made to merge the translations into Sunshine. diff --git a/docs/source/contributing/testing.rst b/docs/source/contributing/testing.rst index aca4a82dc8d..2d9f6290d3f 100644 --- a/docs/source/contributing/testing.rst +++ b/docs/source/contributing/testing.rst @@ -13,12 +13,17 @@ Test clang-format locally. Sphinx ------ -Sunshine uses `Sphinx `_ for documentation building. Sphinx, along with other +Sunshine uses `Sphinx `__ for documentation building. Sphinx, along with other required python dependencies are included in the `./docs/requirements.txt` file. Python is required to build sphinx docs. Installation and setup of python will not be covered here. Doxygen is used to generate the XML files required by Sphinx. Doxygen can be obtained from -`Doxygen downloads `_. Ensure that the `doxygen` executable is in your path. +`Doxygen downloads `__. Ensure that the `doxygen` executable is in your path. + +.. seealso:: + Sphinx is configured to use the graphviz extension. To obtain the dot executable from the Graphviz library, + see the `library’s downloads section `__. + The config file for Sphinx is `docs/source/conf.py`. This is already included in the repo and should not be modified. @@ -37,7 +42,98 @@ Test with Sphinx cd docs sphinx-build -b html source build +Lint with rstcheck + .. code-block:: bash + + rstcheck -r . + +Check formatting with rstfmt + .. code-block:: bash + + rstfmt --check --diff -w 120 . + +Format inplace with rstfmt + .. code-block:: bash + + rstfmt -w 120 . + Unit Testing ------------ -.. Todo:: Sunshine does not currently have any unit tests. If you would like to help us improve please get in contact - with us, or make a PR with suggested changes. +Sunshine uses `Google Test `__ for unit testing. Google Test is included in the +repo as a submodule. The test sources are located in the `./tests` directory. + +The tests need to be compiled into an executable, and then run. The tests are built using the normal build process, but +can be disabled by setting the `BUILD_TESTS` CMake option to `OFF`. + +To run the tests, execute the following command from the build directory: + +.. tab:: Linux + + .. code-block:: bash + + pushd tests + ./test_sunshine + popd + +.. tab:: macOS + + .. code-block:: bash + + pushd tests + ./test_sunshine + popd + +.. tab:: Windows + + .. code-block:: bash + + pushd tests + test_sunshine.exe + popd + +To see all available options, run the tests with the `--help` option. + +.. tab:: Linux + + .. code-block:: bash + + pushd tests + ./test_sunshine --help + popd + +.. tab:: macOS + + .. code-block:: bash + + pushd tests + ./test_sunshine --help + popd + +.. tab:: Windows + + .. code-block:: bash + + pushd tests + test_sunshine.exe --help + popd + +Some tests rely on Python to run. CMake will search for Python and enable the docs tests if it is found, otherwise +cmake will fail. You can manually disable the tests by setting the `TESTS_ENABLE_PYTHON_TESTS` CMake option to +`OFF`. + +.. tip:: + + See the googletest `FAQ `__ for more information on how to use + Google Test. + +We use `gcovr `__ to generate code coverage reports, +and `Codecov `__ to analyze the reports for all PRs and commits. + +Codecov will fail a PR if the total coverage is reduced too much, or if not enough of the diff is covered by tests. +In some cases, the code cannot be covered when running the tests inside of GitHub runners. For example, any test that +needs access to the GPU will not be able to run. In these cases, the coverage can be omitted by adding comments to the +code. See the `gcovr documentation `__ for +more information. + +Even if your changes cannot be covered in the CI, we still encourage you to write the tests for them. This will allow +maintainers to run the tests locally. diff --git a/docs/source/gamestream/gamestream.rst b/docs/source/gamestream/gamestream.rst index aed014b287c..74e468f4bb3 100644 --- a/docs/source/gamestream/gamestream.rst +++ b/docs/source/gamestream/gamestream.rst @@ -7,8 +7,8 @@ outperforms GameStream, so rest assured that Sunshine will be equally performant Migration --------- We have developed a simple migration tool to help you migrate your GameStream games and apps to Sunshine automatically. -Please check out our `GSMS `_ project if you're interested in an automated -migration option. At the time of writing this GSMS offers the ability to migrate your custom games and apps. The +Please check out our `GSMS `__ project if you're interested in an automated +migration option. GSMS offers the ability to migrate your custom and auto-detected games and apps. The working directory, command, and image are all set in Sunshine's ``apps.json`` file. The box-art image is also copied to a specified directory. diff --git a/docs/source/history/changelog.rst b/docs/source/history/changelog.rst new file mode 100644 index 00000000000..3689dd705f5 --- /dev/null +++ b/docs/source/history/changelog.rst @@ -0,0 +1,17 @@ +Changelog +========= + +.. only:: epub + + You can view the changelog in the + `online version `__. + +.. only:: html + + .. raw:: html + + + + diff --git a/docs/source/images/discord_calls_01.png b/docs/source/images/discord_calls_01.png new file mode 100644 index 00000000000..d26e62a811e Binary files /dev/null and b/docs/source/images/discord_calls_01.png differ diff --git a/docs/source/images/discord_calls_02.png b/docs/source/images/discord_calls_02.png new file mode 100644 index 00000000000..6a739be788b Binary files /dev/null and b/docs/source/images/discord_calls_02.png differ diff --git a/docs/source/images/discord_calls_03.png b/docs/source/images/discord_calls_03.png new file mode 100644 index 00000000000..0dd34500233 Binary files /dev/null and b/docs/source/images/discord_calls_03.png differ diff --git a/docs/source/images/discord_calls_04.png b/docs/source/images/discord_calls_04.png new file mode 100644 index 00000000000..ec38513e25d Binary files /dev/null and b/docs/source/images/discord_calls_04.png differ diff --git a/docs/source/images/discord_calls_05.png b/docs/source/images/discord_calls_05.png new file mode 100644 index 00000000000..efb4e2beeab Binary files /dev/null and b/docs/source/images/discord_calls_05.png differ diff --git a/docs/source/legal/legal.rst b/docs/source/legal/legal.rst index 13bc3660140..3fd3b8f2c28 100644 --- a/docs/source/legal/legal.rst +++ b/docs/source/legal/legal.rst @@ -1,10 +1,10 @@ Legal ===== -.. Attention:: This documentation is for informational purposes only and is not intended as legal advice. If you have +.. attention:: This documentation is for informational purposes only and is not intended as legal advice. If you have any legal questions or concerns about using Sunshine, we recommend consulting with a lawyer. Sunshine is licensed under the GPL-3.0 license, which allows for free use and modification of the software. -The full text of the license can be reviewed `here `_. +The full text of the license can be reviewed `here `__. Commercial Use -------------- diff --git a/docs/source/source/src/platform.rst b/docs/source/source/src/platform.rst deleted file mode 100644 index ca74092992e..00000000000 --- a/docs/source/source/src/platform.rst +++ /dev/null @@ -1,10 +0,0 @@ -platform -======== - -.. toctree:: - :maxdepth: 1 - - platform/common - platform/linux - platform/macos - platform/windows diff --git a/docs/source/source/src/platform/common.rst b/docs/source/source/src/platform/common.rst deleted file mode 100644 index 3e09f6db049..00000000000 --- a/docs/source/source/src/platform/common.rst +++ /dev/null @@ -1,4 +0,0 @@ -common -====== - -.. Todo:: Add common.h diff --git a/docs/source/source/src/platform/linux.rst b/docs/source/source/src/platform/linux.rst deleted file mode 100644 index f67235b6438..00000000000 --- a/docs/source/source/src/platform/linux.rst +++ /dev/null @@ -1,12 +0,0 @@ -linux -===== - -.. toctree:: - :maxdepth: 1 - - linux/cuda - linux/graphics - linux/misc - linux/vaapi - linux/wayland - linux/x11grab diff --git a/docs/source/source/src/platform/linux/graphics.rst b/docs/source/source/src/platform/linux/graphics.rst deleted file mode 100644 index 0ecd4b99403..00000000000 --- a/docs/source/source/src/platform/linux/graphics.rst +++ /dev/null @@ -1,4 +0,0 @@ -graphics -======== - -.. Todo:: Add graphics.h diff --git a/docs/source/source/src/platform/linux/misc.rst b/docs/source/source/src/platform/linux/misc.rst deleted file mode 100644 index 155a8302eee..00000000000 --- a/docs/source/source/src/platform/linux/misc.rst +++ /dev/null @@ -1,4 +0,0 @@ -misc -==== - -.. Todo:: Add misc.h diff --git a/docs/source/source/src/platform/linux/x11grab.rst b/docs/source/source/src/platform/linux/x11grab.rst deleted file mode 100644 index 8b8ef0f362a..00000000000 --- a/docs/source/source/src/platform/linux/x11grab.rst +++ /dev/null @@ -1,4 +0,0 @@ -x11grab -======= - -.. Todo:: Add x11grab.h diff --git a/docs/source/source/src/platform/macos.rst b/docs/source/source/src/platform/macos.rst deleted file mode 100644 index 1031eb63551..00000000000 --- a/docs/source/source/src/platform/macos.rst +++ /dev/null @@ -1,11 +0,0 @@ -macos -===== - -.. toctree:: - :maxdepth: 1 - - macos/av_audio - macos/av_img_t - macos/av_video - macos/misc - macos/nv12_zero_device diff --git a/docs/source/source/src/platform/macos/av_audio.rst b/docs/source/source/src/platform/macos/av_audio.rst deleted file mode 100644 index 4d36dfd764e..00000000000 --- a/docs/source/source/src/platform/macos/av_audio.rst +++ /dev/null @@ -1,4 +0,0 @@ -av_audio -======== - -.. Todo:: Add av_audio.h diff --git a/docs/source/source/src/platform/macos/av_img_t.rst b/docs/source/source/src/platform/macos/av_img_t.rst deleted file mode 100644 index 74ef60c98ab..00000000000 --- a/docs/source/source/src/platform/macos/av_img_t.rst +++ /dev/null @@ -1,4 +0,0 @@ -av_img_t -======== - -.. Todo:: Add av_img_t.h diff --git a/docs/source/source/src/platform/macos/av_video.rst b/docs/source/source/src/platform/macos/av_video.rst deleted file mode 100644 index f6fd1e5ba36..00000000000 --- a/docs/source/source/src/platform/macos/av_video.rst +++ /dev/null @@ -1,4 +0,0 @@ -av_video -======== - -.. Todo:: Add av_video.h diff --git a/docs/source/source/src/platform/windows.rst b/docs/source/source/src/platform/windows.rst deleted file mode 100644 index 9bcece68c3a..00000000000 --- a/docs/source/source/src/platform/windows.rst +++ /dev/null @@ -1,9 +0,0 @@ -windows -======= - -.. toctree:: - :maxdepth: 1 - - windows/display - windows/misc - windows/PolicyConfig diff --git a/docs/source/source/src/platform/windows/PolicyConfig.rst b/docs/source/source/src/platform/windows/PolicyConfig.rst deleted file mode 100644 index 1e6bb024e13..00000000000 --- a/docs/source/source/src/platform/windows/PolicyConfig.rst +++ /dev/null @@ -1,4 +0,0 @@ -PolicyConfig -============ - -.. Todo:: Add PolicyConfig.h diff --git a/docs/source/source/src/platform/windows/display.rst b/docs/source/source/src/platform/windows/display.rst deleted file mode 100644 index b8566de8ed3..00000000000 --- a/docs/source/source/src/platform/windows/display.rst +++ /dev/null @@ -1,4 +0,0 @@ -display -======= - -.. Todo:: Add display.h diff --git a/docs/source/source/src/utility.rst b/docs/source/source/src/utility.rst deleted file mode 100644 index 08174df61ce..00000000000 --- a/docs/source/source/src/utility.rst +++ /dev/null @@ -1,4 +0,0 @@ -utility -======= - -.. Todo:: Add utility.h diff --git a/docs/source/source/src.rst b/docs/source/source_code/source_code.rst similarity index 72% rename from docs/source/source/src.rst rename to docs/source/source_code/source_code.rst index b5b79228cf9..fecc6801c92 100644 --- a/docs/source/source/src.rst +++ b/docs/source/source_code/source_code.rst @@ -1,5 +1,5 @@ -src -=== +Source Code +=========== We are in process of improving the source code documentation. Code should be documented using Doxygen syntax. Some examples exist in `main.h` and `main.cpp`. In order for documentation within the code to appear in the rendered docs, the definition of the object must be in a header file, although the documentation itself can (and @@ -10,7 +10,7 @@ Example Documentation Blocks **file.h** -.. code-block:: cpp +.. code-block:: c // functions int main(int argc, char *argv[]); @@ -52,35 +52,40 @@ Example Documentation Blocks // do stuff } -Code ----- +Source +------ .. toctree:: - :maxdepth: 2 :caption: src + :maxdepth: 1 + :glob: + + src/* + +.. toctree:: + :caption: src/platform + :maxdepth: 1 + :glob: + + src/platform/* + +.. toctree:: + :caption: src/platform/linux + :maxdepth: 1 + :glob: + + src/platform/linux/* + +.. toctree:: + :caption: src/platform/macos + :maxdepth: 1 + :glob: + + src/platform/macos/* + +.. toctree:: + :caption: src/platform/windows + :maxdepth: 1 + :glob: - src/main - src/audio - src/cbs - src/config - src/confighttp - src/crypto - src/httpcommon - src/input - src/move_by_copy - src/network - src/nvhttp - src/process - src/round_robin - src/rtsp - src/stream - src/sync - src/system_tray - src/task_pool - src/thread_pool - src/thread_safe - src/upnp - src/utility - src/uuid - src/video - src/platform + src/platform/windows/* diff --git a/docs/source/source/src/audio.rst b/docs/source/source_code/src/audio.rst similarity index 100% rename from docs/source/source/src/audio.rst rename to docs/source/source_code/src/audio.rst diff --git a/docs/source/source/src/cbs.rst b/docs/source/source_code/src/cbs.rst similarity index 100% rename from docs/source/source/src/cbs.rst rename to docs/source/source_code/src/cbs.rst diff --git a/docs/source/source/src/config.rst b/docs/source/source_code/src/config.rst similarity index 100% rename from docs/source/source/src/config.rst rename to docs/source/source_code/src/config.rst diff --git a/docs/source/source/src/confighttp.rst b/docs/source/source_code/src/confighttp.rst similarity index 100% rename from docs/source/source/src/confighttp.rst rename to docs/source/source_code/src/confighttp.rst diff --git a/docs/source/source/src/crypto.rst b/docs/source/source_code/src/crypto.rst similarity index 100% rename from docs/source/source/src/crypto.rst rename to docs/source/source_code/src/crypto.rst diff --git a/docs/source/source_code/src/entry_handler.rst b/docs/source/source_code/src/entry_handler.rst new file mode 100644 index 00000000000..c522b065652 --- /dev/null +++ b/docs/source/source_code/src/entry_handler.rst @@ -0,0 +1,5 @@ +entry_handler +============= + +.. doxygenfile:: entry_handler.h + :allow-dot-graphs: diff --git a/docs/source/source_code/src/file_handler.rst b/docs/source/source_code/src/file_handler.rst new file mode 100644 index 00000000000..221b8cbde03 --- /dev/null +++ b/docs/source/source_code/src/file_handler.rst @@ -0,0 +1,5 @@ +file_handler +============ + +.. doxygenfile:: file_handler.h + :allow-dot-graphs: diff --git a/docs/source/source_code/src/globals.rst b/docs/source/source_code/src/globals.rst new file mode 100644 index 00000000000..ed70cecf692 --- /dev/null +++ b/docs/source/source_code/src/globals.rst @@ -0,0 +1,5 @@ +globals +======= + +.. doxygenfile:: globals.h + :allow-dot-graphs: diff --git a/docs/source/source/src/httpcommon.rst b/docs/source/source_code/src/httpcommon.rst similarity index 100% rename from docs/source/source/src/httpcommon.rst rename to docs/source/source_code/src/httpcommon.rst diff --git a/docs/source/source/src/input.rst b/docs/source/source_code/src/input.rst similarity index 100% rename from docs/source/source/src/input.rst rename to docs/source/source_code/src/input.rst diff --git a/docs/source/source_code/src/logging.rst b/docs/source/source_code/src/logging.rst new file mode 100644 index 00000000000..6b037c20e46 --- /dev/null +++ b/docs/source/source_code/src/logging.rst @@ -0,0 +1,5 @@ +logging +======= + +.. doxygenfile:: logging.h + :allow-dot-graphs: diff --git a/docs/source/source/src/main.rst b/docs/source/source_code/src/main.rst similarity index 100% rename from docs/source/source/src/main.rst rename to docs/source/source_code/src/main.rst diff --git a/docs/source/source/src/move_by_copy.rst b/docs/source/source_code/src/move_by_copy.rst similarity index 100% rename from docs/source/source/src/move_by_copy.rst rename to docs/source/source_code/src/move_by_copy.rst diff --git a/docs/source/source/src/network.rst b/docs/source/source_code/src/network.rst similarity index 100% rename from docs/source/source/src/network.rst rename to docs/source/source_code/src/network.rst diff --git a/docs/source/source/src/nvhttp.rst b/docs/source/source_code/src/nvhttp.rst similarity index 100% rename from docs/source/source/src/nvhttp.rst rename to docs/source/source_code/src/nvhttp.rst diff --git a/docs/source/source_code/src/platform/common.rst b/docs/source/source_code/src/platform/common.rst new file mode 100644 index 00000000000..ec0ca47a9b6 --- /dev/null +++ b/docs/source/source_code/src/platform/common.rst @@ -0,0 +1,4 @@ +common +====== + +.. todo:: Add common.h diff --git a/docs/source/source/src/platform/linux/cuda.rst b/docs/source/source_code/src/platform/linux/cuda.rst similarity index 100% rename from docs/source/source/src/platform/linux/cuda.rst rename to docs/source/source_code/src/platform/linux/cuda.rst diff --git a/docs/source/source_code/src/platform/linux/graphics.rst b/docs/source/source_code/src/platform/linux/graphics.rst new file mode 100644 index 00000000000..2f44e10986e --- /dev/null +++ b/docs/source/source_code/src/platform/linux/graphics.rst @@ -0,0 +1,4 @@ +graphics +======== + +.. todo:: Add graphics.h diff --git a/docs/source/source_code/src/platform/linux/misc.rst b/docs/source/source_code/src/platform/linux/misc.rst new file mode 100644 index 00000000000..2e30a58eda1 --- /dev/null +++ b/docs/source/source_code/src/platform/linux/misc.rst @@ -0,0 +1,4 @@ +misc +==== + +.. todo:: Add misc.h diff --git a/docs/source/source/src/platform/linux/vaapi.rst b/docs/source/source_code/src/platform/linux/vaapi.rst similarity index 100% rename from docs/source/source/src/platform/linux/vaapi.rst rename to docs/source/source_code/src/platform/linux/vaapi.rst diff --git a/docs/source/source/src/platform/linux/wayland.rst b/docs/source/source_code/src/platform/linux/wayland.rst similarity index 100% rename from docs/source/source/src/platform/linux/wayland.rst rename to docs/source/source_code/src/platform/linux/wayland.rst diff --git a/docs/source/source_code/src/platform/linux/x11grab.rst b/docs/source/source_code/src/platform/linux/x11grab.rst new file mode 100644 index 00000000000..c0c2f7cccce --- /dev/null +++ b/docs/source/source_code/src/platform/linux/x11grab.rst @@ -0,0 +1,4 @@ +x11grab +======= + +.. todo:: Add x11grab.h diff --git a/docs/source/source_code/src/platform/macos/av_audio.rst b/docs/source/source_code/src/platform/macos/av_audio.rst new file mode 100644 index 00000000000..92f04868e19 --- /dev/null +++ b/docs/source/source_code/src/platform/macos/av_audio.rst @@ -0,0 +1,4 @@ +av_audio +======== + +.. todo:: Add av_audio.h diff --git a/docs/source/source_code/src/platform/macos/av_img_t.rst b/docs/source/source_code/src/platform/macos/av_img_t.rst new file mode 100644 index 00000000000..4fa8510ddea --- /dev/null +++ b/docs/source/source_code/src/platform/macos/av_img_t.rst @@ -0,0 +1,4 @@ +av_img_t +======== + +.. todo:: Add av_img_t.h diff --git a/docs/source/source_code/src/platform/macos/av_video.rst b/docs/source/source_code/src/platform/macos/av_video.rst new file mode 100644 index 00000000000..179b180b8b9 --- /dev/null +++ b/docs/source/source_code/src/platform/macos/av_video.rst @@ -0,0 +1,4 @@ +av_video +======== + +.. todo:: Add av_video.h diff --git a/docs/source/source/src/platform/macos/misc.rst b/docs/source/source_code/src/platform/macos/misc.rst similarity index 100% rename from docs/source/source/src/platform/macos/misc.rst rename to docs/source/source_code/src/platform/macos/misc.rst diff --git a/docs/source/source/src/platform/macos/nv12_zero_device.rst b/docs/source/source_code/src/platform/macos/nv12_zero_device.rst similarity index 51% rename from docs/source/source/src/platform/macos/nv12_zero_device.rst rename to docs/source/source_code/src/platform/macos/nv12_zero_device.rst index ecab415e0d8..8e236a5125e 100644 --- a/docs/source/source/src/platform/macos/nv12_zero_device.rst +++ b/docs/source/source_code/src/platform/macos/nv12_zero_device.rst @@ -1,4 +1,4 @@ nv12_zero_device ================ -.. Todo:: Add nv12_zero_device.h +.. todo:: Add nv12_zero_device.h diff --git a/docs/source/source_code/src/platform/windows/PolicyConfig.rst b/docs/source/source_code/src/platform/windows/PolicyConfig.rst new file mode 100644 index 00000000000..a47e89ac4b0 --- /dev/null +++ b/docs/source/source_code/src/platform/windows/PolicyConfig.rst @@ -0,0 +1,4 @@ +PolicyConfig +============ + +.. todo:: Add PolicyConfig.h diff --git a/docs/source/source_code/src/platform/windows/display.rst b/docs/source/source_code/src/platform/windows/display.rst new file mode 100644 index 00000000000..033e22e29fa --- /dev/null +++ b/docs/source/source_code/src/platform/windows/display.rst @@ -0,0 +1,4 @@ +display +======= + +.. todo:: Add display.h diff --git a/docs/source/source/src/platform/windows/misc.rst b/docs/source/source_code/src/platform/windows/misc.rst similarity index 100% rename from docs/source/source/src/platform/windows/misc.rst rename to docs/source/source_code/src/platform/windows/misc.rst diff --git a/docs/source/source/src/process.rst b/docs/source/source_code/src/process.rst similarity index 100% rename from docs/source/source/src/process.rst rename to docs/source/source_code/src/process.rst diff --git a/docs/source/source/src/round_robin.rst b/docs/source/source_code/src/round_robin.rst similarity index 100% rename from docs/source/source/src/round_robin.rst rename to docs/source/source_code/src/round_robin.rst diff --git a/docs/source/source/src/rtsp.rst b/docs/source/source_code/src/rtsp.rst similarity index 100% rename from docs/source/source/src/rtsp.rst rename to docs/source/source_code/src/rtsp.rst diff --git a/docs/source/source/src/stream.rst b/docs/source/source_code/src/stream.rst similarity index 100% rename from docs/source/source/src/stream.rst rename to docs/source/source_code/src/stream.rst diff --git a/docs/source/source/src/sync.rst b/docs/source/source_code/src/sync.rst similarity index 100% rename from docs/source/source/src/sync.rst rename to docs/source/source_code/src/sync.rst diff --git a/docs/source/source/src/system_tray.rst b/docs/source/source_code/src/system_tray.rst similarity index 100% rename from docs/source/source/src/system_tray.rst rename to docs/source/source_code/src/system_tray.rst diff --git a/docs/source/source/src/task_pool.rst b/docs/source/source_code/src/task_pool.rst similarity index 100% rename from docs/source/source/src/task_pool.rst rename to docs/source/source_code/src/task_pool.rst diff --git a/docs/source/source/src/thread_pool.rst b/docs/source/source_code/src/thread_pool.rst similarity index 100% rename from docs/source/source/src/thread_pool.rst rename to docs/source/source_code/src/thread_pool.rst diff --git a/docs/source/source/src/thread_safe.rst b/docs/source/source_code/src/thread_safe.rst similarity index 100% rename from docs/source/source/src/thread_safe.rst rename to docs/source/source_code/src/thread_safe.rst diff --git a/docs/source/source/src/upnp.rst b/docs/source/source_code/src/upnp.rst similarity index 100% rename from docs/source/source/src/upnp.rst rename to docs/source/source_code/src/upnp.rst diff --git a/docs/source/source_code/src/utility.rst b/docs/source/source_code/src/utility.rst new file mode 100644 index 00000000000..80ffd14ad5a --- /dev/null +++ b/docs/source/source_code/src/utility.rst @@ -0,0 +1,4 @@ +utility +======= + +.. todo:: Add utility.h diff --git a/docs/source/source/src/uuid.rst b/docs/source/source_code/src/uuid.rst similarity index 100% rename from docs/source/source/src/uuid.rst rename to docs/source/source_code/src/uuid.rst diff --git a/docs/source/source/src/video.rst b/docs/source/source_code/src/video.rst similarity index 100% rename from docs/source/source/src/video.rst rename to docs/source/source_code/src/video.rst diff --git a/docs/source/toc.rst b/docs/source/toc.rst index e4c4398bda6..53ffe687c92 100644 --- a/docs/source/toc.rst +++ b/docs/source/toc.rst @@ -3,13 +3,11 @@ :caption: About about/overview - about/installation + about/setup about/docker about/third_party_packages - about/usage - about/app_examples + about/guides/guides about/advanced_usage - about/changelog .. toctree:: :maxdepth: 2 @@ -53,4 +51,10 @@ :maxdepth: 2 :caption: source - source/src + source_code/source_code + +.. toctree:: + :maxdepth: 2 + :caption: History + + history/changelog diff --git a/docs/source/troubleshooting/general.rst b/docs/source/troubleshooting/general.rst index 3b6d6aad241..116d27223cc 100644 --- a/docs/source/troubleshooting/general.rst +++ b/docs/source/troubleshooting/general.rst @@ -4,9 +4,24 @@ General Forgotten Credentials --------------------- If you forgot your credentials to the web UI, try this. - .. code-block:: bash + .. tab:: General + + .. code-block:: bash + + sunshine --creds {new-username} {new-password} + + .. tab:: AppImage + + .. code-block:: bash + + ./sunshine.AppImage --creds {new-username} {new-password} + + .. tab:: Flatpak + + .. code-block:: bash + + flatpak run --command=sunshine dev.lizardbyte.Sunshine --creds {new-username} {new-password} - sunshine --creds Web UI Access ------------- @@ -17,8 +32,24 @@ Nvidia issues ------------- NvFBC, NvENC, or general issues with Nvidia graphics card. - Consumer grade Nvidia cards are software limited to a specific number of encodes. See - `Video Encode and Decode GPU Support Matrix `_ + `Video Encode and Decode GPU Support Matrix `__ for more info. - You can usually bypass the restriction with a driver patch. See Keylase's - `Linux `_ - or `Windows `_ patches for more guidance. + `Linux `__ + or `Windows `__ patches for more guidance. + +Controller works on Steam but not in games +------------------------------------------ +One trick might be to change Steam settings and check or uncheck the configuration to support Xbox/Playstation +controllers and leave only support for Generic controllers. + +Also, if you have many controllers already directly connected to the host, it might help to disable them so that the +Sunshine provided controller (connected to the guest) is the "first" one. In Linux this can be accomplished on USB +devices by finding the device in `/sys/bus/usb/devices/` and writing `0` to the `authorized` file. + +Packet loss +----------- +Albeit unlikely, some guests might work better with a lower `MTU +`__ from the host. For example, a LG TV was found to have 30-60% +packet loss when the host had MTU set to 1500 and 1472, but 0% packet loss with a MTU of 1428 set in the network card +serving the stream (a Linux PC). It's unclear how that helped precisely so it's a last resort suggestion. diff --git a/docs/source/troubleshooting/linux.rst b/docs/source/troubleshooting/linux.rst index 5c2d7c8a37c..8fc08b2e5e4 100644 --- a/docs/source/troubleshooting/linux.rst +++ b/docs/source/troubleshooting/linux.rst @@ -1,9 +1,35 @@ Linux ===== +Hardware Encoding fails +----------------------- +Due to legal concerns, Mesa has disabled hardware decoding and encoding by default. + +.. code-block:: text + + Error: Could not open codec [h264_vaapi]: Function not implemented + +If you see the above error in the Sunshine logs, compiling `Mesa` +manually, may be required. See the official Mesa3D `Compiling and Installing `__ +documentation for instructions. + +.. important:: You must re-enable the disabled encoders. You can do so, by passing the following argument to the build + system. You may also want to enable decoders, however that is not required for Sunshine and is not covered here. + + .. code-block:: bash + + -Dvideo-codecs=h264enc,h265enc + +.. note:: Other build options are listed in the + `meson options `__ file. + KMS Streaming fails ------------------- If screencasting fails with KMS, you may need to run the following to force unprivileged screencasting. .. code-block:: bash sudo setcap -r $(readlink -f $(which sunshine)) + +Gamescope compatibility +----------------------- +Some users have reported stuttering issues when streaming games running within Gamescope. diff --git a/docs/source/troubleshooting/windows.rst b/docs/source/troubleshooting/windows.rst index 31a1a6bafbe..313bef9a3ee 100644 --- a/docs/source/troubleshooting/windows.rst +++ b/docs/source/troubleshooting/windows.rst @@ -3,4 +3,12 @@ Windows No gamepad detected ------------------- -#. Verify that you've installed `ViGEmBus `_. +#. Verify that you've installed `Nefarius Virtual Gamepad `__. + +Permission denied +----------------- +Since Sunshine runs as a service on Windows, it may not have the same level of access that your regular user account +has. You may get permission denied errors when attempting to launch a game or application from a non system drive. + +You will need to modify the security permissions on your disk. Ensure that user/principal SYSTEM has full +permissions on the disk. diff --git a/gh-pages-template/assets/images/AdobeStock_231616343.jpeg b/gh-pages-template/assets/images/AdobeStock_231616343.jpeg new file mode 100644 index 00000000000..818e82d24c2 Binary files /dev/null and b/gh-pages-template/assets/images/AdobeStock_231616343.jpeg differ diff --git a/gh-pages-template/assets/images/AdobeStock_231616343_1920x1280.jpg b/gh-pages-template/assets/images/AdobeStock_231616343_1920x1280.jpg new file mode 100644 index 00000000000..d235cd5fc3a Binary files /dev/null and b/gh-pages-template/assets/images/AdobeStock_231616343_1920x1280.jpg differ diff --git a/gh-pages-template/assets/images/AdobeStock_303330124.jpeg b/gh-pages-template/assets/images/AdobeStock_303330124.jpeg new file mode 100644 index 00000000000..4a5ab376e0e Binary files /dev/null and b/gh-pages-template/assets/images/AdobeStock_303330124.jpeg differ diff --git a/gh-pages-template/assets/images/AdobeStock_303330124_1920x1280.jpg b/gh-pages-template/assets/images/AdobeStock_303330124_1920x1280.jpg new file mode 100644 index 00000000000..8e047dc3d12 Binary files /dev/null and b/gh-pages-template/assets/images/AdobeStock_303330124_1920x1280.jpg differ diff --git a/gh-pages-template/assets/images/AdobeStock_305732536.jpeg b/gh-pages-template/assets/images/AdobeStock_305732536.jpeg new file mode 100644 index 00000000000..d79c40a2466 Binary files /dev/null and b/gh-pages-template/assets/images/AdobeStock_305732536.jpeg differ diff --git a/gh-pages-template/assets/images/AdobeStock_305732536_1920x1280.jpg b/gh-pages-template/assets/images/AdobeStock_305732536_1920x1280.jpg new file mode 100644 index 00000000000..bdde5c9f4f5 Binary files /dev/null and b/gh-pages-template/assets/images/AdobeStock_305732536_1920x1280.jpg differ diff --git a/gh-pages-template/index.html b/gh-pages-template/index.html new file mode 100644 index 00000000000..fdbfc4a3dd6 --- /dev/null +++ b/gh-pages-template/index.html @@ -0,0 +1,464 @@ + + + + LizardByte - Sunshine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+ +
+
+ + +
+
+

+ Sunshine is a self-hosted game stream host for Moonlight. Offering low latency, cloud gaming + server capabilities with support for AMD, Intel, and Nvidia GPUs for hardware encoding. Software + encoding is also available. You can connect to Sunshine from any Moonlight client on a variety + of devices. A web UI is provided to allow configuration, and client pairing, from your favorite + web browser. Pair from the local server or any mobile device. +

+
+
+ + +
+
+

Features

+ +
+
+
+
+
+
+ +
+
+
Self-hosted
+

+ Run Sunshine on your own hardware. No need to pay monthly fees to a + cloud gaming provider. +

+
+
+
+
+
+
+
+
+
+
+ +
+
+
Moonlight Support
+

+ Connect to Sunshine from any Moonlight client. Moonlight is available + for Windows, macOS, Linux, Android, iOS, Xbox, and more. +

+
+
+
+
+
+
+
+
+
+
+ +
+
+
Hardware Encoding
+

+ Sunshine supports AMD, Intel, and Nvidia GPUs for hardware encoding. + Software encoding is also available. +

+
+
+
+
+
+
+
+
+
+
+ +
+
+
Low Latency
+

+ Sunshine is designed to provide the lowest latency possible to achieve optimal gaming performance. +

+
+
+
+
+
+
+
+
+
+
+ +
+
+
Control
+

+ Sunshine emulates an Xbox 360 or DS4 controller. Use nearly any + controller on your Moonlight client!
+ +

    +
  • DS4 emulation is only available on Windows.
  • +
  • Gamepad emulation is not currently supported on macOS.
  • +
+ +

+
+
+
+
+
+
+
+
+
+
+ +
+
+
Configurable
+

+ Sunshine offers many configuration options to customize your experience. +

+
+
+
+
+
+
+
+
+ + +
+
+ +
+
+
+
+ +
+

Documentation

+

+ Read the documentation to learn how to install, use, and configure Sunshine. +

+
+
+
+ +
+
+ + +
+
+
+
+ +
+

Download

+

+ Download Sunshine for your platform. +

+
+
+
+ +
+
+
+
+ + + + + +
+
+
+
+
+
Support Center
+
Find answers and ask questions.
+
+
+

+ The one who knows all the answers has not been asked all the questions. + – Confucius. +

+ +
+
+
+
+
+ +
+ +
+ + + + + + + + + + + + + + + + + + + diff --git a/package.json b/package.json index 3b6d33ada01..6cba6f53b3e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,17 @@ { + "scripts": { + "build": "vite build --debug", + "build-clean": "vite build --debug --emptyOutDir", + "dev": "vite build --watch" + }, "dependencies": { - "@fortawesome/fontawesome-free": "6.4.0", - "bootstrap": "5.2.3", - "vue": "2.6.12" + "@fortawesome/fontawesome-free": "6.5.2", + "@popperjs/core": "2.11.8", + "@vitejs/plugin-vue": "4.6.2", + "bootstrap": "5.3.3", + "vite": "4.5.2", + "vite-plugin-ejs": "1.6.4", + "vue": "3.4.27", + "vue-i18n": "9.13.1" } } diff --git a/packaging/linux/AppImage/AppRun b/packaging/linux/AppImage/AppRun index ddc5fd38455..404704c34d3 100644 --- a/packaging/linux/AppImage/AppRun +++ b/packaging/linux/AppImage/AppRun @@ -46,7 +46,9 @@ echo " function install() { # user input rules # shellcheck disable=SC2002 - cat "$SUNSHINE_SHARE_HERE/udev/rules.d/85-sunshine.rules" | sudo tee /etc/udev/rules.d/85-sunshine.rules + cat "$SUNSHINE_SHARE_HERE/udev/rules.d/60-sunshine.rules" | sudo tee /etc/udev/rules.d/60-sunshine.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --property-match=DEVNAME=/dev/uinput # sunshine service mkdir -p ~/.config/systemd/user @@ -56,30 +58,11 @@ function install() { # setcap sudo setcap cap_sys_admin+p "$(readlink -f "$SUNSHINE_BIN_HERE")" - - while true - do - read -r -p "This installation requires a reboot. Do you want to reboot NOW? [y/n] " input - - case $input in - [yY][eE][sS]|[yY]) - echo "Yes" - sudo reboot now - ;; - [nN][oO]|[nN]) - echo "No" - break - ;; - *) - echo "Invalid input..." - ;; - esac - done } function remove() { # remove input rules - sudo rm -f /etc/udev/rules.d/85-sunshine.rules + sudo rm -f /etc/udev/rules.d/60-sunshine.rules # remove service sudo rm -f ~/.config/systemd/user/sunshine.service diff --git a/packaging/linux/AppImage/sunshine.desktop b/packaging/linux/AppImage/sunshine.desktop new file mode 100644 index 00000000000..911735c8c2f --- /dev/null +++ b/packaging/linux/AppImage/sunshine.desktop @@ -0,0 +1,13 @@ +[Desktop Entry] +Type=Application +Name=@PROJECT_NAME@ +Exec=sunshine +Version=1.0 +Comment=@PROJECT_DESCRIPTION@ +Icon=sunshine +Keywords=gamestream;stream;moonlight;remote play; +Categories=AudioVideo;Network;RemoteAccess; +Terminal=true +X-AppImage-Name=sunshine +X-AppImage-Version=@PROJECT_VERSION@ +X-AppImage-Arch=x86_64 diff --git a/packaging/linux/Arch/PKGBUILD b/packaging/linux/Arch/PKGBUILD new file mode 100644 index 00000000000..42e5843b029 --- /dev/null +++ b/packaging/linux/Arch/PKGBUILD @@ -0,0 +1,95 @@ +# Edit on github: https://github.com/LizardByte/Sunshine/blob/master/packaging/linux/Arch/PKGBUILD +# Reference: https://wiki.archlinux.org/title/PKGBUILD + +pkgname='sunshine' +pkgver=@PROJECT_VERSION@@SUNSHINE_SUB_VERSION@ +pkgrel=1 +pkgdesc="@PROJECT_DESCRIPTION@" +arch=('x86_64' 'aarch64') +url=@PROJECT_HOMEPAGE_URL@ +license=('GPL-3.0-only') +install=sunshine.install + +_gcc_version=13 + +depends=('avahi' + 'boost-libs' + 'curl' + 'libayatana-appindicator' + 'libcap' + 'libdrm' + 'libevdev' + 'libmfx' + 'libnotify' + 'libpulse' + 'libva' + 'libvdpau' + 'libx11' + 'libxcb' + 'libxfixes' + 'libxrandr' + 'libxtst' + 'miniupnpc' + 'numactl' + 'openssl' + 'opus' + 'python' + 'udev') +checkdepends=('doxygen' + 'graphviz') +makedepends=('boost' + 'cmake' + "gcc${_gcc_version}" + 'git' + 'make' + 'nodejs' + 'npm') +optdepends=('cuda: Nvidia GPU encoding support' + 'libva-mesa-driver: AMD GPU encoding support' + 'intel-media-driver: Intel GPU encoding support' + 'xorg-server-xvfb: Virtual X server for headless testing') + +provides=('sunshine') + +source=("$pkgname::git+@GITHUB_CLONE_URL@#commit=@GITHUB_COMMIT@") +sha256sums=('SKIP') + +prepare() { + cd "$pkgname" + git submodule update --recursive --init +} + +build() { + export BRANCH="@GITHUB_BRANCH@" + export BUILD_VERSION="@GITHUB_BUILD_VERSION@" + export COMMIT="@GITHUB_COMMIT@" + + export CC="gcc-${_gcc_version}" + export CXX="g++-${_gcc_version}" + + export CFLAGS="${CFLAGS/-Werror=format-security/}" + export CXXFLAGS="${CXXFLAGS/-Werror=format-security/}" + + cmake \ + -S "$pkgname" \ + -B build \ + -Wno-dev \ + -D BUILD_WERROR=ON \ + -D CMAKE_INSTALL_PREFIX=/usr \ + -D SUNSHINE_EXECUTABLE_PATH=/usr/bin/sunshine \ + -D SUNSHINE_ASSETS_DIR="share/sunshine" + + make -C build +} + +check() { + export CC="gcc-${_gcc_version}" + export CXX="g++-${_gcc_version}" + + cd "${srcdir}/build/tests" + ./test_sunshine --gtest_color=yes +} + +package() { + make -C build install DESTDIR="$pkgdir" +} diff --git a/packaging/linux/Arch/sunshine.install b/packaging/linux/Arch/sunshine.install new file mode 100644 index 00000000000..a8a700f1f1c --- /dev/null +++ b/packaging/linux/Arch/sunshine.install @@ -0,0 +1,20 @@ +do_setcap() { + setcap cap_sys_admin+p $(readlink -f $(which sunshine)) +} + +do_udev_reload() { + udevadm control --reload-rules + udevadm trigger --property-match=DEVNAME=/dev/uinput + modprobe uinput || true +} + +post_install() { + do_setcap + do_udev_reload +} + +post_upgrade() { + do_setcap + do_udev_reload +} + diff --git a/packaging/linux/aur/PKGBUILD b/packaging/linux/aur/PKGBUILD deleted file mode 100644 index 02eaf0b5c33..00000000000 --- a/packaging/linux/aur/PKGBUILD +++ /dev/null @@ -1,70 +0,0 @@ -# Edit on github: https://github.com/LizardByte/Sunshine/tree/nightly/packaging/linux/aur/PKGBUILD -# Reference: https://wiki.archlinux.org/title/PKGBUILD - -pkgname='sunshine' -pkgver=@PROJECT_VERSION@@SUNSHINE_SUB_VERSION@ -pkgrel=1 -pkgdesc="@PROJECT_DESCRIPTION@" -arch=('x86_64' 'aarch64') -url=@PROJECT_HOMEPAGE_URL@ -license=('GPL3') - -depends=('avahi' 'boost-libs' 'curl' 'libappindicator-gtk3' 'libevdev' 'libmfx' 'libpulse' 'libva' 'libvdpau' 'libx11' 'libxcb' 'libxfixes' 'libxrandr' 'libxtst' 'numactl' 'openssl' 'opus' 'udev') -makedepends=('boost' 'cmake' 'git' 'make' 'nodejs' 'npm') -optdepends=('cuda: NvFBC capture support' - 'libcap' - 'libdrm') - -provides=('sunshine') - -source=("$pkgname::git+@GITHUB_CLONE_URL@#commit=@GITHUB_COMMIT@") -sha256sums=('SKIP') - -prepare() { - cd "$pkgname" - # Skip submodules that we don't want - if [[ $CARCH == "x86_64" ]]; then - git -c submodule."ffmpeg-macos-x86_64".update=none \ - -c submodule."ffmpeg-windows-x86_64".update=none \ - -c submodule."ffmpeg-linux-aarch64".update=none \ - -c submodule."ffmpeg-macos-aarch64".update=none \ - submodule update --recursive --init - elif [[ $CARCH == "aarch64" ]]; then - git -c submodule."ffmpeg-macos-x86_64".update=none \ - -c submodule."ffmpeg-windows-x86_64".update=none \ - -c submodule."ffmpeg-linux-x86_64".update=none \ - -c submodule."ffmpeg-macos-aarch64".update=none \ - submodule update --recursive --init - - # It's unlikely that someone could get this far on a system with an incorrect arch, but we should handle it anyway - # Pull linux ffmpeg submodules - else - git -c submodule."ffmpeg-macos-x86_64".update=none \ - -c submodule."ffmpeg-windows-x86_64".update=none \ - -c submodule."ffmpeg-macos-aarch64".update=none \ - submodule update --recursive --init - fi -} - -build() { - pushd "$pkgname" - npm install - popd - - export CFLAGS="${CFLAGS/-Werror=format-security/}" - export CXXFLAGS="${CXXFLAGS/-Werror=format-security/}" - - cmake \ - -S "$pkgname" \ - -B build \ - -Wno-dev \ - -D CMAKE_INSTALL_PREFIX=/usr \ - -D SUNSHINE_EXECUTABLE_PATH=/usr/bin/sunshine \ - -D SUNSHINE_ASSETS_DIR="share/sunshine" - - make -C build -} - -package() { - make -C build install DESTDIR="$pkgdir" -} diff --git a/packaging/linux/flatpak/deps/org.flatpak.Builder.BaseApp b/packaging/linux/flatpak/deps/org.flatpak.Builder.BaseApp new file mode 160000 index 00000000000..e5246830bb1 --- /dev/null +++ b/packaging/linux/flatpak/deps/org.flatpak.Builder.BaseApp @@ -0,0 +1 @@ +Subproject commit e5246830bb1b23397b2f9fd1813a79f5c6d2ccb0 diff --git a/packaging/linux/flatpak/deps/shared-modules b/packaging/linux/flatpak/deps/shared-modules new file mode 160000 index 00000000000..782d3cc04cc --- /dev/null +++ b/packaging/linux/flatpak/deps/shared-modules @@ -0,0 +1 @@ +Subproject commit 782d3cc04ccdd8071017f622d4bacd35faecbd86 diff --git a/packaging/linux/flatpak/dev.lizardbyte.sunshine.yml b/packaging/linux/flatpak/dev.lizardbyte.sunshine.yml index 403c6db01d4..cba627807c3 100644 --- a/packaging/linux/flatpak/dev.lizardbyte.sunshine.yml +++ b/packaging/linux/flatpak/dev.lizardbyte.sunshine.yml @@ -5,11 +5,13 @@ runtime-version: "22.08" sdk: org.freedesktop.Sdk sdk-extensions: - org.freedesktop.Sdk.Extension.node18 + - org.freedesktop.Sdk.Extension.vala command: sunshine separate-locales: false finish-args: - --device=all # access all devices - --env=PULSE_PROP_media.category=Manager # allow sunshine to manage audio sinks + - --env=SUNSHINE_MIGRATE_CONFIG=1 # migrate config files to the new location - --filesystem=home # need to save files in user's home directory - --share=ipc # required for X11 shared memory extension - --share=network # access network @@ -27,7 +29,13 @@ cleanup: - /lib/*.a - /share/man +build-options: + append-path: /usr/lib/sdk/vala/bin + prepend-ld-library-path: /usr/lib/sdk/vala/lib + modules: + - "org.flatpak.Builder.BaseApp/xvfb.json" + - name: boost disabled: false buildsystem: simple @@ -41,8 +49,8 @@ modules: url: http://archive.ubuntu.com/ubuntu/pool/main/b/boost1.74/boost1.74_1.74.0.orig.tar.xz sha256: 2467be4af625b5ae4b3c93fc7af196a09eba39c11a7338cd9e8b356fa44d2f45 - type: archive - url: http://archive.ubuntu.com/ubuntu/pool/main/b/boost1.74/boost1.74_1.74.0-17ubuntu1.debian.tar.xz - sha256: 22e623d98c84eb3fec57e19ea371157a5bc8225ba4c5907f7e5155072317a31d + url: http://archive.ubuntu.com/ubuntu/pool/main/b/boost1.74/boost1.74_1.74.0-18.1ubuntu3.debian.tar.xz + sha256: d5660bdce3ea4ac66194b0c4bc6dc3b9d43d41cc16af8bc6024980d965e40ae2 - type: shell commands: - for n in $(cat patches/series); do if [[ $n != "#"* ]]; then patch -Np1 -i "patches/$n" -d .; fi; done @@ -88,6 +96,79 @@ modules: - for n in $(cat patches/series); do if [[ $n != "#"* ]]; then patch -Np1 -i "patches/$n" -d .; fi; done - autoreconf -ivf + # yamllint disable-line rule:line-length + # https://github.com/flathub/org.localsend.localsend_app/blob/7465669c22a2a4fc35e707e1e4e7e882772adc0e/org.localsend.localsend_app.yml#L27-L106 + # https://github.com/flathub/app.vup.Vup/blob/8c5073c7c5b8f24805013abc85a0860ca2439396/app.vup.Vup.yaml#L30-L78 + - name: libayatana-appindicator + buildsystem: cmake-ninja + config-opts: + - -DENABLE_BINDINGS_MONO=NO + - -DENABLE_BINDINGS_VALA=NO + modules: + - shared-modules/intltool/intltool-0.51.json + - name: libdbusmenu-gtk3 # Dependency of libayatana-appindicator + buildsystem: autotools + build-options: + cflags: -Wno-error + env: + HAVE_VALGRIND_FALSE: '#' + HAVE_VALGRIND_TRUE: '' + config-opts: + - --with-gtk=3 + - --disable-dumper + - --disable-static + - --disable-tests + - --disable-gtk-doc + - --enable-introspection=no + - --disable-vala + sources: + - type: archive + url: https://launchpad.net/libdbusmenu/16.04/16.04.0/+download/libdbusmenu-16.04.0.tar.gz + sha256: b9cc4a2acd74509435892823607d966d424bd9ad5d0b00938f27240a1bfa878a + cleanup: + - /include + - /libexec + - /lib/pkgconfig + - /lib/*.la + - /share/doc + - /share/libdbusmenu + - /share/gtk-doc + - /share/gir-1.0 + - name: ayatana-ido + buildsystem: cmake-ninja + sources: + - type: git + url: https://github.com/AyatanaIndicators/ayatana-ido.git + tag: 0.10.1 + commit: 13402a2cc4616b4b5f4244413599e635fcfc1401 + x-checker-data: + type: anitya + project-id: 18445 + tag-template: $version + stable-only: true + - name: libayatana-indicator + buildsystem: cmake-ninja + sources: + - type: git + url: https://github.com/AyatanaIndicators/libayatana-indicator.git + tag: 0.9.3 + commit: a62e8ca13040554a8fc2536ce7e6aa888c5729d9 + x-checker-data: + type: anitya + project-id: 18447 + tag-template: $version + stable-only: true + sources: + - type: git + url: https://github.com/AyatanaIndicators/libayatana-appindicator.git + tag: 0.5.92 + commit: d214fe3e7a6b1ba8faea68d70586310b34dc643c + x-checker-data: + type: anitya + project-id: 18446 + tag-template: $version + stable-only: true + - name: libevdev disabled: false buildsystem: meson @@ -107,6 +188,30 @@ modules: commands: - for n in $(cat patches/series); do if [[ $n != "#"* ]]; then patch -Np1 -i "patches/$n" -d .; fi; done + - name: libnotify + buildsystem: meson + config-opts: + - -Dtests=false + - -Dintrospection=disabled + - -Dman=false + - -Dgtk_doc=false + - -Ddocbook_docs=disabled + sources: + - type: archive + url: https://download.gnome.org/sources/libnotify/0.8/libnotify-0.8.2.tar.xz + sha256: c5f4ed3d1f86e5b118c76415aacb861873ed3e6f0c6b3181b828cf584fc5c616 + x-checker-data: + type: gnome + name: libnotify + stable-only: true + - type: archive + url: https://download.gnome.org/sources/gnome-common/3.18/gnome-common-3.18.0.tar.xz + sha256: 22569e370ae755e04527b76328befc4c73b62bfd4a572499fde116b8318af8cf + x-checker-data: + type: gnome + name: gnome-common + stable-only: true + - name: intel-mediasdk disabled: false buildsystem: cmake @@ -151,6 +256,23 @@ modules: commands: - for n in $(cat patches/series); do if [[ $n != "#"* ]]; then patch -Np1 -i "patches/$n" -d .; fi; done + - name: miniupnpc + buildsystem: cmake + config-opts: + - -DUPNPC_BUILD_SAMPLE=OFF + - -DUPNPC_BUILD_SHARED=ON + - -DUPNPC_BUILD_TESTS=OFF + sources: + - type: archive + url: http://archive.ubuntu.com/ubuntu/pool/main/m/miniupnpc/miniupnpc_2.2.5.orig.tar.gz + sha256: 38acd5f4602f7cf8bcdc1ec30b2d58db2e9912e5d9f5350dd99b06bfdffb517c + - type: archive + url: http://archive.ubuntu.com/ubuntu/pool/main/m/miniupnpc/miniupnpc_2.2.5-1.debian.tar.xz + sha256: f6ab181f3c999ae0630508ea1e6c76ae302262414061acaab12bf8763431ffd1 + - type: shell + commands: + - for n in $(cat patches/series); do if [[ $n != "#"* ]]; then patch -Np1 -i "patches/$n" -d .; fi; done + - name: numactl buildsystem: autotools make-args: @@ -206,14 +328,14 @@ modules: append-path: /usr/lib/sdk/node18/bin build-args: - --share=network - cxxflags: -I${C_INCLUDE_PATH}/libevdev-1.0 env: + BUILD_VERSION: "@BUILD_VERSION@" + BRANCH: "@GITHUB_BRANCH@" + COMMIT: "@GITHUB_COMMIT@" npm_config_nodedir: /usr/lib/sdk/node18 NPM_CONFIG_LOGLEVEL: info - build-commands: - # Install npm dependencies - - cd ${FLATPAK_BUILDER_BUILDDIR} && npm install config-opts: + - -DBUILD_WERROR=ON - -DCMAKE_BUILD_TYPE=Release - -DCMAKE_INSTALL_PREFIX=/app - -DCMAKE_CUDA_COMPILER=/app/cuda/bin/nvcc @@ -223,7 +345,8 @@ modules: - -DSUNSHINE_ENABLE_X11=ON - -DSUNSHINE_ENABLE_DRM=ON - -DSUNSHINE_ENABLE_CUDA=ON - - -DSUNSHINE_CONFIGURE_FLATPAK=ON + - -DSUNSHINE_BUILD_FLATPAK=ON + - -DTESTS_ENABLE_PYTHON_TESTS=OFF sources: - type: git url: "@GITHUB_CLONE_URL@" @@ -241,3 +364,7 @@ modules: 's%/app/bin/sunshine%flatpak run dev.lizardbyte.sunshine\nExecStop=flatpak kill dev.lizardbyte.sunshine%g' /app/share/sunshine/systemd/user/sunshine.service - install -D $FLATPAK_BUILDER_BUILDDIR/packaging/linux/flatpak/scripts/* /app/bin + run-tests: true + test-rule: "" # empty to disable + test-commands: + - xvfb-run tests/test_sunshine --gtest_color=yes diff --git a/packaging/linux/flatpak/scripts/additional-install.sh b/packaging/linux/flatpak/scripts/additional-install.sh index 8a905b53810..a27db4e09ba 100644 --- a/packaging/linux/flatpak/scripts/additional-install.sh +++ b/packaging/linux/flatpak/scripts/additional-install.sh @@ -7,7 +7,7 @@ echo Sunshine User Service has been installed. echo Use [systemctl --user enable sunshine] once to autostart Sunshine on login. # Udev rule -UDEV=$(cat /app/share/sunshine/udev/rules.d/85-sunshine.rules) +UDEV=$(cat /app/share/sunshine/udev/rules.d/60-sunshine.rules) echo Configuring mouse permission. -flatpak-spawn --host pkexec sh -c "echo '$UDEV' > /etc/udev/rules.d/85-sunshine.rules" +flatpak-spawn --host pkexec sh -c "echo '$UDEV' > /etc/udev/rules.d/60-sunshine.rules" echo Restart computer for mouse permission to take effect. diff --git a/packaging/linux/flatpak/scripts/remove-additional-install.sh b/packaging/linux/flatpak/scripts/remove-additional-install.sh index 6148f62ea1e..0d13baeb62c 100644 --- a/packaging/linux/flatpak/scripts/remove-additional-install.sh +++ b/packaging/linux/flatpak/scripts/remove-additional-install.sh @@ -7,5 +7,5 @@ systemctl --user daemon-reload echo Sunshine User Service has been removed. # Udev rule -flatpak-spawn --host pkexec sh -c "rm /etc/udev/rules.d/85-sunshine.rules" +flatpak-spawn --host pkexec sh -c "rm /etc/udev/rules.d/60-sunshine.rules" echo Mouse permission removed. Restart computer to take effect. diff --git a/packaging/linux/flatpak/sunshine.desktop b/packaging/linux/flatpak/sunshine.desktop new file mode 100644 index 00000000000..1c5fe13a409 --- /dev/null +++ b/packaging/linux/flatpak/sunshine.desktop @@ -0,0 +1,20 @@ +[Desktop Entry] +Type=Application +Name=@PROJECT_NAME@ +Exec=flatpak run dev.lizardbyte.sunshine +Version=1.0 +Comment=@PROJECT_DESCRIPTION@ +Icon=sunshine +Keywords=gamestream;stream;moonlight;remote play; +Categories=AudioVideo;Network;RemoteAccess; +Actions=RunInTerminal;KMS; + +[Desktop Action RunInTerminal] +Name=Run in Terminal +Icon=application-x-executable +Exec=gio launch @CMAKE_INSTALL_FULL_DATAROOTDIR@/applications/sunshine_terminal.desktop + +[Desktop Action KMS] +Name=Run in Terminal (KMS) +Icon=application-x-executable +Exec=gio launch @CMAKE_INSTALL_FULL_DATAROOTDIR@/applications/sunshine_kms.desktop diff --git a/packaging/linux/flatpak/sunshine_kms.desktop b/packaging/linux/flatpak/sunshine_kms.desktop new file mode 100644 index 00000000000..521f4b936ae --- /dev/null +++ b/packaging/linux/flatpak/sunshine_kms.desktop @@ -0,0 +1,6 @@ +[Desktop Entry] +Name=@PROJECT_NAME@ (KMS) +Exec=sudo -i PULSE_SERVER=unix:$(pactl info | awk '/Server String/{print$3}') flatpak run dev.lizardbyte.sunshine +Terminal=true +Type=Application +NoDisplay=true diff --git a/packaging/linux/sunshine.appdata.xml b/packaging/linux/sunshine.appdata.xml new file mode 100644 index 00000000000..fdb6b029bb8 --- /dev/null +++ b/packaging/linux/sunshine.appdata.xml @@ -0,0 +1,20 @@ + + + @PROJECT_NAME@.desktop + @PROJECT_LICENSE@ + @PROJECT_LICENSE@ + @PROJECT_NAME@ + @CMAKE_PROJECT_HOMEPAGE_URL@ + @PROJECT_DESCRIPTION@ + +

+ @PROJECT_LONG_DESCRIPTION@ +

+
+ + + https://app.lizardbyte.dev/Sunshine/assets/images/AdobeStock_305732536_1920x1280.jpg + Sunshine + + +
diff --git a/packaging/linux/sunshine.desktop b/packaging/linux/sunshine.desktop index a345e5dc467..719555301d5 100644 --- a/packaging/linux/sunshine.desktop +++ b/packaging/linux/sunshine.desktop @@ -1,12 +1,15 @@ [Desktop Entry] Type=Application Name=@PROJECT_NAME@ -Exec=sunshine +Exec=/usr/bin/env systemctl start --u sunshine Version=1.0 Comment=@PROJECT_DESCRIPTION@ Icon=sunshine -Categories=Utility; -Terminal=true -X-AppImage-Name=sunshine -X-AppImage-Version=@PROJECT_VERSION@ -X-AppImage-Arch=x86_64 +Keywords=gamestream;stream;moonlight;remote play; +Categories=AudioVideo;Network;RemoteAccess; +Actions=RunInTerminal; + +[Desktop Action RunInTerminal] +Name=Run in Terminal +Icon=application-x-executable +Exec=gio launch @CMAKE_INSTALL_FULL_DATAROOTDIR@/applications/sunshine_terminal.desktop diff --git a/sunshine.service.in b/packaging/linux/sunshine.service.in similarity index 100% rename from sunshine.service.in rename to packaging/linux/sunshine.service.in diff --git a/packaging/linux/sunshine_terminal.desktop b/packaging/linux/sunshine_terminal.desktop new file mode 100644 index 00000000000..c26889123cb --- /dev/null +++ b/packaging/linux/sunshine_terminal.desktop @@ -0,0 +1,6 @@ +[Desktop Entry] +Name=@PROJECT_NAME@ +Exec=sunshine +Terminal=true +Type=Application +NoDisplay=true diff --git a/packaging/macos/Portfile b/packaging/macos/Portfile index fb5fb13a8f0..e0cc9ef34f6 100644 --- a/packaging/macos/Portfile +++ b/packaging/macos/Portfile @@ -31,17 +31,30 @@ post-fetch { system -W ${worksrcpath} "${git.cmd} submodule update --init --recursive" } +# https://guide.macports.org/chunked/reference.dependencies.html +depends_build-append port:npm9 \ + port:pkgconfig + depends_lib port:avahi \ port:curl \ port:libopus \ - port:npm9 \ - port:pkgconfig + port:miniupnpc \ + port:python311 \ + port:py311-pip + +depends_test port:doxygen \ + port:graphviz -boost.version 1.80 +boost.version 1.81 -configure.args -DCMAKE_INSTALL_PREFIX=${prefix} \ +configure.args -DBUILD_WERROR=ON \ + -DCMAKE_INSTALL_PREFIX=${prefix} \ -DSUNSHINE_ASSETS_DIR=etc/sunshine/assets +configure.env-append BRANCH=@GITHUB_BRANCH@ +configure.env-append BUILD_VERSION=@BUILD_VERSION@ +configure.env-append COMMIT=@GITHUB_COMMIT@ + startupitem.create yes startupitem.executable "${prefix}/bin/{$name}" startupitem.location LaunchDaemons @@ -55,11 +68,13 @@ platform darwin { } } -pre-build { - system -W ${worksrcpath} "npm install" -} - notes-append "Run @PROJECT_NAME@ by executing 'sunshine ', e.g. 'sunshine ~/sunshine.conf' " notes-append "The config file will be created if it doesn't exist." notes-append "It is recommended to set a location for the apps file in the config." notes-append "See our documentation at 'https://docs.lizardbyte.dev/projects/sunshine/en/v@PROJECT_VERSION@/' for further info." + +test.run yes +test.dir ${build.dir}/tests +test.target "" +test.cmd ./test_sunshine +test.args --gtest_color=yes diff --git a/packaging/macos/sunshine.rb b/packaging/macos/sunshine.rb new file mode 100644 index 00000000000..e119d1df1a7 --- /dev/null +++ b/packaging/macos/sunshine.rb @@ -0,0 +1,69 @@ +require "language/node" + +class @PROJECT_NAME@ < Formula + desc "@PROJECT_DESCRIPTION@" + homepage "@PROJECT_HOMEPAGE_URL@" + url "@GITHUB_CLONE_URL@", + tag: "@GITHUB_BRANCH@" + version "@PROJECT_VERSION@" + license all_of: ["GPL-3.0-only"] + head "@GITHUB_CLONE_URL@", branch: "@GITHUB_DEFAULT_BRANCH@" + + depends_on "boost" => :build + depends_on "cmake" => :build + depends_on "node" => :build + depends_on "pkg-config" => :build + depends_on "curl" + depends_on "miniupnpc" + depends_on "openssl" + depends_on "opus" + + def install + ENV["BRANCH"] = "@GITHUB_BRANCH@" + ENV["BUILD_VERSION"] = "@BUILD_VERSION@" + ENV["COMMIT"] = "@GITHUB_COMMIT@" + + args = %W[ + -DBUILD_WERROR=ON + -DCMAKE_INSTALL_PREFIX=#{prefix} + -DOPENSSL_ROOT_DIR=#{Formula["openssl"].opt_prefix} + -DSUNSHINE_ASSETS_DIR=sunshine/assets + -DSUNSHINE_BUILD_HOMEBREW=ON + -DTESTS_ENABLE_PYTHON_TESTS=OFF + ] + system "cmake", "-S", ".", "-B", "build", *std_cmake_args, *args + + cd "build" do + system "make", "-j" + system "make", "install" + bin.install "tests/test_sunshine" + end + end + + service do + run [opt_bin/"sunshine", "~/.config/sunshine/sunshine.conf"] + end + + def caveats + <<~EOS + Thanks for installing @PROJECT_NAME@! + + To get started, review the documentation at: + https://docs.lizardbyte.dev/projects/sunshine/en/latest/ + + Sunshine can only access microphones on macOS due to system limitations. + To stream system audio use "Soundflower" or "BlackHole". + + Gamepads are not currently supported on macOS. + EOS + end + + test do + # test that the binary runs at all + system "#{bin}/sunshine", "--version" + + # run the test suite + # cannot build tests with python tests because homebrew destroys the source directory + system "#{bin}/test_sunshine", "--gtest_color=yes" + end +end diff --git a/qodana-js.yaml b/qodana-js.yaml deleted file mode 100644 index d130951323f..00000000000 --- a/qodana-js.yaml +++ /dev/null @@ -1,21 +0,0 @@ ---- -version: "1.0" -linter: jetbrains/qodana-js:2023.1-eap - -bootstrap: | - # install npm dependencies - npm install - -exclude: - - name: All - paths: - - gh-pages - - third-party - -failThreshold: 100 - -include: - - name: CheckDependencyLicenses - -profile: - name: qodana.recommended diff --git a/qodana-python.yaml b/qodana-python.yaml deleted file mode 100644 index efbf876ee3d..00000000000 --- a/qodana-python.yaml +++ /dev/null @@ -1,29 +0,0 @@ ---- -version: "1.0" -linter: jetbrains/qodana-python:2023.1-eap - -bootstrap: | - # setup python - - python3 -m venv /data/cache/venv - source /data/cache/venv/bin/activate - python3 -m pip install -r /data/project/docs/requirements.txt - python3 -m pip install -r /data/project/scripts/requirements.txt - - # remove idea directory (No Python interpreter configured for the project) - # https://github.com/JetBrains/Qodana/discussions/134#discussioncomment-4329981 - rm -rf .idea - -exclude: - - name: All - paths: - - gh-pages - - third-party - -failThreshold: 100 - -include: - - name: CheckDependencyLicenses - -profile: - name: qodana.recommended diff --git a/scripts/_locale.py b/scripts/_locale.py index d967974e3f5..d035414917d 100644 --- a/scripts/_locale.py +++ b/scripts/_locale.py @@ -22,16 +22,20 @@ year = datetime.datetime.now().year -# retroarcher target locales +# target locales target_locales = [ - 'de', # Deutsch + 'de', # German 'en', # English 'en_GB', # English (United Kingdom) 'en_US', # English (United States) - 'es', # español - 'fr', # français - 'it', # italiano - 'ru', # русский + 'es', # Spanish + 'fr', # French + 'it', # Italian + 'ja', # Japanese + 'pt', # Portuguese + 'ru', # Russian + 'sv', # Swedish + 'zh', # Chinese ] diff --git a/scripts/icons/convert_and_pack.sh b/scripts/icons/convert_and_pack.sh new file mode 100644 index 00000000000..950effc98d6 --- /dev/null +++ b/scripts/icons/convert_and_pack.sh @@ -0,0 +1,61 @@ +#!/bin/bash + +if ! [ -x "$(command -v ./go-png2ico)" ]; then + echo "./go-png2ico not found" + echo "download the executable from https://github.com/J-Siu/go-png2ico" + echo "and drop it in this folder" + exit 1 +fi + +if ! [ -x "$(command -v ./oxipng)" ]; then + echo "./oxipng executable not found" + echo "download the executable from https://github.com/shssoichiro/oxipng" + echo "and drop it in this folder" + exit 1 +fi + +if ! [ -x "$(command -v inkscape)" ]; then + echo "inkscape executable not found" + exit 1 +fi + +icon_base_sizes=(16 64) +icon_sizes_keys=() # associative array to prevent duplicates +icon_sizes_keys[256]=1 + +for icon_base_size in ${icon_base_sizes[@]}; do + # increment in 25% till 400% + icon_size_increment=$((icon_base_size / 4)) + for ((i = 0; i <= 12; i++)); do + icon_sizes_keys[$((icon_base_size + i * icon_size_increment))]=1 + done +done + +# convert to normal array +icon_sizes=${!icon_sizes_keys[@]} + +echo "using icon sizes:" +echo ${icon_sizes[@]} + +src_vectors=("../../src_assets/common/assets/web/public/images/sunshine-locked.svg" + "../../src_assets/common/assets/web/public/images/sunshine-pausing.svg" + "../../src_assets/common/assets/web/public/images/sunshine-playing.svg" + "../../sunshine.svg") + +echo "using sources vectors:" +echo ${src_vectors[@]} + +for src_vector in ${src_vectors[@]}; do + file_name=`basename "$src_vector" .svg` + png_files=() + for icon_size in ${icon_sizes[@]}; do + png_file="${file_name}${icon_size}.png" + echo "converting ${png_file}" + inkscape -w $icon_size -h $icon_size "$src_vector" --export-filename "${png_file}" && + ./oxipng -o max --strip safe --alpha "${png_file}" && + png_files+=("${png_file}") + done + + echo "packing ${file_name}.ico" + ./go-png2ico "${png_files[@]}" "${file_name}.ico" +done diff --git a/scripts/requirements.txt b/scripts/requirements.txt index c3bc8a72530..9cfd158f3fd 100644 --- a/scripts/requirements.txt +++ b/scripts/requirements.txt @@ -1 +1,2 @@ -Babel==2.12.1 +Babel==2.15.0 +clang-format diff --git a/scripts/update_clang_format.py b/scripts/update_clang_format.py index 2624e794405..9e0dacda847 100644 --- a/scripts/update_clang_format.py +++ b/scripts/update_clang_format.py @@ -5,10 +5,10 @@ # variables directories = [ 'src', + 'tests', 'tools', os.path.join('third-party', 'glad'), os.path.join('third-party', 'nvfbc'), - os.path.join('third-party', 'wayland-protocols') ] file_types = [ 'cpp', diff --git a/src/audio.cpp b/src/audio.cpp index b2c8f458f7f..b608466d5c1 100644 --- a/src/audio.cpp +++ b/src/audio.cpp @@ -10,7 +10,8 @@ #include "audio.h" #include "config.h" -#include "main.h" +#include "globals.h" +#include "logging.h" #include "thread_safe.h" #include "utility.h" @@ -33,12 +34,16 @@ namespace audio { start_audio_control(audio_ctx_t &ctx); static void stop_audio_control(audio_ctx_t &); + static void + apply_surround_params(opus_stream_config_t &stream, const stream_params_t ¶ms); int map_stream(int channels, bool quality); constexpr auto SAMPLE_RATE = 48000; + // NOTE: If you adjust the bitrates listed here, make sure to update the + // corresponding bitrate adjustment logic in rtsp_stream::cmd_announce() opus_stream_config_t stream_configs[MAX_STREAM_CONFIG] { { SAMPLE_RATE, @@ -95,24 +100,31 @@ namespace audio { void encodeThread(sample_queue_t samples, config_t config, void *channel_data) { auto packets = mail::man->queue(mail::audio_packets); - auto stream = &stream_configs[map_stream(config.channels, config.flags[config_t::HIGH_QUALITY])]; + auto stream = stream_configs[map_stream(config.channels, config.flags[config_t::HIGH_QUALITY])]; + if (config.flags[config_t::CUSTOM_SURROUND_PARAMS]) { + apply_surround_params(stream, config.customStreamParams); + } // Encoding takes place on this thread platf::adjust_thread_priority(platf::thread_priority_e::high); opus_t opus { opus_multistream_encoder_create( - stream->sampleRate, - stream->channelCount, - stream->streams, - stream->coupledStreams, - stream->mapping, + stream.sampleRate, + stream.channelCount, + stream.streams, + stream.coupledStreams, + stream.mapping, OPUS_APPLICATION_RESTRICTED_LOWDELAY, nullptr) }; - opus_multistream_encoder_ctl(opus.get(), OPUS_SET_BITRATE(stream->bitrate)); + opus_multistream_encoder_ctl(opus.get(), OPUS_SET_BITRATE(stream.bitrate)); opus_multistream_encoder_ctl(opus.get(), OPUS_SET_VBR(0)); - auto frame_size = config.packetDuration * stream->sampleRate / 1000; + BOOST_LOG(info) << "Opus initialized: "sv << stream.sampleRate / 1000 << " kHz, "sv + << stream.channelCount << " channels, "sv + << stream.bitrate / 1000 << " kbps (total), LOWDELAY"sv; + + auto frame_size = config.packetDuration * stream.sampleRate / 1000; while (auto sample = samples->pop()) { buffer_t packet { 1400 }; @@ -132,7 +144,10 @@ namespace audio { void capture(safe::mail_t mail, config_t config, void *channel_data) { auto shutdown_event = mail->event(mail::shutdown); - auto stream = &stream_configs[map_stream(config.channels, config.flags[config_t::HIGH_QUALITY])]; + auto stream = stream_configs[map_stream(config.channels, config.flags[config_t::HIGH_QUALITY])]; + if (config.flags[config_t::CUSTOM_SURROUND_PARAMS]) { + apply_surround_params(stream, config.customStreamParams); + } auto ref = control_shared.ref(); if (!ref) { @@ -164,7 +179,7 @@ namespace audio { // Prefer the virtual sink if host playback is disabled or there's no other sink if (ref->sink.null && (!config.flags[config_t::HOST_AUDIO] || sink->empty())) { auto &null = *ref->sink.null; - switch (stream->channelCount) { + switch (stream.channelCount) { case 2: sink = &null.stereo; break; @@ -188,8 +203,8 @@ namespace audio { } } - auto frame_size = config.packetDuration * stream->sampleRate / 1000; - auto mic = control->microphone(stream->mapping, stream->channelCount, stream->sampleRate, frame_size); + auto frame_size = config.packetDuration * stream.sampleRate / 1000; + auto mic = control->microphone(stream.mapping, stream.channelCount, stream.sampleRate, frame_size); if (!mic) { return; } @@ -210,7 +225,7 @@ namespace audio { shutdown_event->view(); }); - int samples_per_frame = frame_size * stream->channelCount; + int samples_per_frame = frame_size * stream.channelCount; while (!shutdown_event->peek()) { std::vector sample_buffer; @@ -226,7 +241,7 @@ namespace audio { BOOST_LOG(info) << "Reinitializing audio capture"sv; mic.reset(); do { - mic = control->microphone(stream->mapping, stream->channelCount, stream->sampleRate, frame_size); + mic = control->microphone(stream.mapping, stream.channelCount, stream.sampleRate, frame_size); if (!mic) { BOOST_LOG(warning) << "Couldn't re-initialize audio input"sv; } @@ -296,4 +311,12 @@ namespace audio { ctx.control->set_sink(sink); } } + + void + apply_surround_params(opus_stream_config_t &stream, const stream_params_t ¶ms) { + stream.channelCount = params.channelCount; + stream.streams = params.streams; + stream.coupledStreams = params.coupledStreams; + stream.mapping = params.mapping; + } } // namespace audio diff --git a/src/audio.h b/src/audio.h index fe22c94611d..cbe4ae98b1c 100644 --- a/src/audio.h +++ b/src/audio.h @@ -26,12 +26,20 @@ namespace audio { int bitrate; }; + struct stream_params_t { + int channelCount; + int streams; + int coupledStreams; + std::uint8_t mapping[8]; + }; + extern opus_stream_config_t stream_configs[MAX_STREAM_CONFIG]; struct config_t { enum flags_e : int { HIGH_QUALITY, HOST_AUDIO, + CUSTOM_SURROUND_PARAMS, MAX_FLAGS }; @@ -39,6 +47,8 @@ namespace audio { int channels; int mask; + stream_params_t customStreamParams; + std::bitset flags; }; diff --git a/src/cbs.cpp b/src/cbs.cpp index 52a3ed93f5e..a2ba6f2517e 100644 --- a/src/cbs.cpp +++ b/src/cbs.cpp @@ -11,7 +11,7 @@ extern "C" { } #include "cbs.h" -#include "main.h" +#include "logging.h" #include "utility.h" using namespace std::literals; @@ -87,92 +87,61 @@ namespace cbs { return write(cbs_ctx, nal, uh, codec_id); } - util::buffer_t - make_sps_h264(const AVCodecContext *ctx) { - H264RawSPS sps {}; - - // b_per_p == ctx->max_b_frames for h264 - // desired_b_depth == avoption("b_depth") == 1 - // max_b_depth == std::min(av_log2(ctx->b_per_p) + 1, desired_b_depth) ==> 1 - auto max_b_depth = 1; - auto dpb_frame = ctx->gop_size == 1 ? 0 : 1 + max_b_depth; - auto mb_width = (FFALIGN(ctx->width, 16) / 16) * 16; - auto mb_height = (FFALIGN(ctx->height, 16) / 16) * 16; - - sps.nal_unit_header.nal_ref_idc = 3; - sps.nal_unit_header.nal_unit_type = H264_NAL_SPS; - - sps.profile_idc = FF_PROFILE_H264_HIGH & 0xFF; - - sps.constraint_set1_flag = 1; - - if (ctx->level != FF_LEVEL_UNKNOWN) { - sps.level_idc = ctx->level; + h264_t + make_sps_h264(const AVCodecContext *avctx, const AVPacket *packet) { + cbs::ctx_t ctx; + if (ff_cbs_init(&ctx, AV_CODEC_ID_H264, nullptr)) { + return {}; } - else { - auto framerate = ctx->framerate; - auto level = ff_h264_guess_level( - sps.profile_idc, - ctx->bit_rate, - framerate.num / framerate.den, - mb_width, - mb_height, - dpb_frame); + cbs::frag_t frag; - if (!level) { - BOOST_LOG(error) << "Could not guess h264 level"sv; + int err = ff_cbs_read_packet(ctx.get(), &frag, packet); + if (err < 0) { + char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 }; + BOOST_LOG(error) << "Couldn't read packet: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err); - return {}; - } - sps.level_idc = level->level_idc; + return {}; } - sps.seq_parameter_set_id = 0; - sps.chroma_format_idc = 1; - - sps.log2_max_frame_num_minus4 = 3; // 4; - sps.pic_order_cnt_type = 0; - sps.log2_max_pic_order_cnt_lsb_minus4 = 0; // 4; + auto sps_p = ((CodedBitstreamH264Context *) ctx->priv_data)->active_sps; - sps.max_num_ref_frames = dpb_frame; + // This is a very large struct that cannot safely be stored on the stack + auto sps = std::make_unique(*sps_p); - sps.pic_width_in_mbs_minus1 = mb_width / 16 - 1; - sps.pic_height_in_map_units_minus1 = mb_height / 16 - 1; - - sps.frame_mbs_only_flag = 1; - sps.direct_8x8_inference_flag = 1; - - if (ctx->width != mb_width || ctx->height != mb_height) { - sps.frame_cropping_flag = 1; - sps.frame_crop_left_offset = 0; - sps.frame_crop_top_offset = 0; - sps.frame_crop_right_offset = (mb_width - ctx->width) / 2; - sps.frame_crop_bottom_offset = (mb_height - ctx->height) / 2; + if (avctx->refs > 0) { + sps->max_num_ref_frames = avctx->refs; } - sps.vui_parameters_present_flag = 1; + sps->vui_parameters_present_flag = 1; - auto &vui = sps.vui; + auto &vui = sps->vui; + std::memset(&vui, 0, sizeof(vui)); vui.video_format = 5; vui.colour_description_present_flag = 1; vui.video_signal_type_present_flag = 1; - vui.video_full_range_flag = ctx->color_range == AVCOL_RANGE_JPEG; - vui.colour_primaries = ctx->color_primaries; - vui.transfer_characteristics = ctx->color_trc; - vui.matrix_coefficients = ctx->colorspace; + vui.video_full_range_flag = avctx->color_range == AVCOL_RANGE_JPEG; + vui.colour_primaries = avctx->color_primaries; + vui.transfer_characteristics = avctx->color_trc; + vui.matrix_coefficients = avctx->colorspace; vui.low_delay_hrd_flag = 1 - vui.fixed_frame_rate_flag; vui.bitstream_restriction_flag = 1; vui.motion_vectors_over_pic_boundaries_flag = 1; - vui.log2_max_mv_length_horizontal = 15; - vui.log2_max_mv_length_vertical = 15; - vui.max_num_reorder_frames = max_b_depth; - vui.max_dec_frame_buffering = max_b_depth + 1; + vui.log2_max_mv_length_horizontal = 16; + vui.log2_max_mv_length_vertical = 16; + vui.max_num_reorder_frames = 0; + vui.max_dec_frame_buffering = sps->max_num_ref_frames; - return write(sps.nal_unit_header.nal_unit_type, (void *) &sps.nal_unit_header, AV_CODEC_ID_H264); + cbs::ctx_t write_ctx; + ff_cbs_init(&write_ctx, AV_CODEC_ID_H264, nullptr); + + return h264_t { + write(write_ctx, sps->nal_unit_header.nal_unit_type, (void *) &sps->nal_unit_header, AV_CODEC_ID_H264), + write(ctx, sps_p->nal_unit_header.nal_unit_type, (void *) &sps_p->nal_unit_header, AV_CODEC_ID_H264) + }; } hevc_t @@ -195,16 +164,17 @@ namespace cbs { auto vps_p = ((CodedBitstreamH265Context *) ctx->priv_data)->active_vps; auto sps_p = ((CodedBitstreamH265Context *) ctx->priv_data)->active_sps; - H265RawSPS sps { *sps_p }; - H265RawVPS vps { *vps_p }; + // These are very large structs that cannot safely be stored on the stack + auto sps = std::make_unique(*sps_p); + auto vps = std::make_unique(*vps_p); - vps.profile_tier_level.general_profile_compatibility_flag[4] = 1; - sps.profile_tier_level.general_profile_compatibility_flag[4] = 1; + vps->profile_tier_level.general_profile_compatibility_flag[4] = 1; + sps->profile_tier_level.general_profile_compatibility_flag[4] = 1; - auto &vui = sps.vui; + auto &vui = sps->vui; std::memset(&vui, 0, sizeof(vui)); - sps.vui_parameters_present_flag = 1; + sps->vui_parameters_present_flag = 1; // skip sample aspect ratio @@ -216,11 +186,11 @@ namespace cbs { vui.transfer_characteristics = avctx->color_trc; vui.matrix_coefficients = avctx->colorspace; - vui.vui_timing_info_present_flag = vps.vps_timing_info_present_flag; - vui.vui_num_units_in_tick = vps.vps_num_units_in_tick; - vui.vui_time_scale = vps.vps_time_scale; - vui.vui_poc_proportional_to_timing_flag = vps.vps_poc_proportional_to_timing_flag; - vui.vui_num_ticks_poc_diff_one_minus1 = vps.vps_num_ticks_poc_diff_one_minus1; + vui.vui_timing_info_present_flag = vps->vps_timing_info_present_flag; + vui.vui_num_units_in_tick = vps->vps_num_units_in_tick; + vui.vui_time_scale = vps->vps_time_scale; + vui.vui_poc_proportional_to_timing_flag = vps->vps_poc_proportional_to_timing_flag; + vui.vui_num_ticks_poc_diff_one_minus1 = vps->vps_num_ticks_poc_diff_one_minus1; vui.vui_hrd_parameters_present_flag = 0; vui.bitstream_restriction_flag = 1; @@ -236,46 +206,17 @@ namespace cbs { return hevc_t { nal_t { - write(write_ctx, vps.nal_unit_header.nal_unit_type, (void *) &vps.nal_unit_header, AV_CODEC_ID_H265), + write(write_ctx, vps->nal_unit_header.nal_unit_type, (void *) &vps->nal_unit_header, AV_CODEC_ID_H265), write(ctx, vps_p->nal_unit_header.nal_unit_type, (void *) &vps_p->nal_unit_header, AV_CODEC_ID_H265), }, nal_t { - write(write_ctx, sps.nal_unit_header.nal_unit_type, (void *) &sps.nal_unit_header, AV_CODEC_ID_H265), + write(write_ctx, sps->nal_unit_header.nal_unit_type, (void *) &sps->nal_unit_header, AV_CODEC_ID_H265), write(ctx, sps_p->nal_unit_header.nal_unit_type, (void *) &sps_p->nal_unit_header, AV_CODEC_ID_H265), }, }; } - util::buffer_t - read_sps_h264(const AVPacket *packet) { - cbs::ctx_t ctx; - if (ff_cbs_init(&ctx, AV_CODEC_ID_H264, nullptr)) { - return {}; - } - - cbs::frag_t frag; - - int err = ff_cbs_read_packet(ctx.get(), &frag, &*packet); - if (err < 0) { - char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 }; - BOOST_LOG(error) << "Couldn't read packet: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err); - - return {}; - } - - auto h264 = (H264RawNALUnitHeader *) ((CodedBitstreamH264Context *) ctx->priv_data)->active_sps; - return write(h264->nal_unit_type, (void *) h264, AV_CODEC_ID_H264); - } - - h264_t - make_sps_h264(const AVCodecContext *ctx, const AVPacket *packet) { - return h264_t { - make_sps_h264(ctx), - read_sps_h264(packet), - }; - } - bool validate_sps(const AVPacket *packet, int codec_id) { cbs::ctx_t ctx; diff --git a/src/config.cpp b/src/config.cpp index 6e4c93e37f5..4bede1a4267 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -7,7 +7,9 @@ #include #include #include +#include #include +#include #include #include @@ -15,7 +17,11 @@ #include #include "config.h" -#include "main.h" +#include "entry_handler.h" +#include "file_handler.h" +#include "logging.h" +#include "nvhttp.h" +#include "rtsp.h" #include "utility.h" #include "platform/common.h" @@ -24,6 +30,11 @@ #include #endif +#ifndef __APPLE__ + // For NVENC legacy constants + #include +#endif + namespace fs = std::filesystem; using namespace std::literals; @@ -35,107 +46,34 @@ using namespace std::literals; namespace config { namespace nv { -#ifdef __APPLE__ - // values accurate as of 27/12/2022, but aren't strictly necessary for MacOS build - #define NV_ENC_TUNING_INFO_HIGH_QUALITY 1 - #define NV_ENC_TUNING_INFO_LOW_LATENCY 2 - #define NV_ENC_TUNING_INFO_ULTRA_LOW_LATENCY 3 - #define NV_ENC_TUNING_INFO_LOSSLESS 4 - #define NV_ENC_PARAMS_RC_CONSTQP 0x0 - #define NV_ENC_PARAMS_RC_VBR 0x1 - #define NV_ENC_PARAMS_RC_CBR 0x2 - #define NV_ENC_H264_ENTROPY_CODING_MODE_CABAC 1 - #define NV_ENC_H264_ENTROPY_CODING_MODE_CAVLC 2 -#else - #include -#endif - - enum preset_e : int { - p1 = 12, // PRESET_P1, // must be kept in sync with - p2, // PRESET_P2, - p3, // PRESET_P3, - p4, // PRESET_P4, - p5, // PRESET_P5, - p6, // PRESET_P6, - p7 // PRESET_P7 - }; - - enum tune_e : int { - hq = NV_ENC_TUNING_INFO_HIGH_QUALITY, - ll = NV_ENC_TUNING_INFO_LOW_LATENCY, - ull = NV_ENC_TUNING_INFO_ULTRA_LOW_LATENCY, - lossless = NV_ENC_TUNING_INFO_LOSSLESS - }; - - enum rc_e : int { - constqp = NV_ENC_PARAMS_RC_CONSTQP, /**< Constant QP mode */ - vbr = NV_ENC_PARAMS_RC_VBR, /**< Variable bitrate mode */ - cbr = NV_ENC_PARAMS_RC_CBR /**< Constant bitrate mode */ - }; - - enum coder_e : int { - _auto = 0, - cabac = NV_ENC_H264_ENTROPY_CODING_MODE_CABAC, - cavlc = NV_ENC_H264_ENTROPY_CODING_MODE_CAVLC, - }; - - std::optional - preset_from_view(const std::string_view &preset) { -#define _CONVERT_(x) \ - if (preset == #x##sv) return x - _CONVERT_(p1); - _CONVERT_(p2); - _CONVERT_(p3); - _CONVERT_(p4); - _CONVERT_(p5); - _CONVERT_(p6); - _CONVERT_(p7); -#undef _CONVERT_ - return std::nullopt; - } - std::optional - tune_from_view(const std::string_view &tune) { -#define _CONVERT_(x) \ - if (tune == #x##sv) return x - _CONVERT_(hq); - _CONVERT_(ll); - _CONVERT_(ull); - _CONVERT_(lossless); -#undef _CONVERT_ - return std::nullopt; + nvenc::nvenc_two_pass + twopass_from_view(const std::string_view &preset) { + if (preset == "disabled") return nvenc::nvenc_two_pass::disabled; + if (preset == "quarter_res") return nvenc::nvenc_two_pass::quarter_resolution; + if (preset == "full_res") return nvenc::nvenc_two_pass::full_resolution; + BOOST_LOG(warning) << "config: unknown nvenc_twopass value: " << preset; + return nvenc::nvenc_two_pass::quarter_resolution; } - std::optional - rc_from_view(const std::string_view &rc) { -#define _CONVERT_(x) \ - if (rc == #x##sv) return x - _CONVERT_(constqp); - _CONVERT_(vbr); - _CONVERT_(cbr); -#undef _CONVERT_ - return std::nullopt; - } - - int - coder_from_view(const std::string_view &coder) { - if (coder == "auto"sv) return _auto; - if (coder == "cabac"sv || coder == "ac"sv) return cabac; - if (coder == "cavlc"sv || coder == "vlc"sv) return cavlc; - - return -1; - } } // namespace nv namespace amd { #ifdef __APPLE__ // values accurate as of 27/12/2022, but aren't strictly necessary for MacOS build + #define AMF_VIDEO_ENCODER_AV1_QUALITY_PRESET_SPEED 100 + #define AMF_VIDEO_ENCODER_AV1_QUALITY_PRESET_QUALITY 30 + #define AMF_VIDEO_ENCODER_AV1_QUALITY_PRESET_BALANCED 70 #define AMF_VIDEO_ENCODER_HEVC_QUALITY_PRESET_SPEED 10 #define AMF_VIDEO_ENCODER_HEVC_QUALITY_PRESET_QUALITY 0 #define AMF_VIDEO_ENCODER_HEVC_QUALITY_PRESET_BALANCED 5 #define AMF_VIDEO_ENCODER_QUALITY_PRESET_SPEED 1 #define AMF_VIDEO_ENCODER_QUALITY_PRESET_QUALITY 2 #define AMF_VIDEO_ENCODER_QUALITY_PRESET_BALANCED 0 + #define AMF_VIDEO_ENCODER_AV1_RATE_CONTROL_METHOD_CONSTANT_QP 0 + #define AMF_VIDEO_ENCODER_AV1_RATE_CONTROL_METHOD_CBR 3 + #define AMF_VIDEO_ENCODER_AV1_RATE_CONTROL_METHOD_PEAK_CONSTRAINED_VBR 2 + #define AMF_VIDEO_ENCODER_AV1_RATE_CONTROL_METHOD_LATENCY_CONSTRAINED_VBR 1 #define AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_CONSTANT_QP 0 #define AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_CBR 3 #define AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_PEAK_CONSTRAINED_VBR 2 @@ -144,22 +82,36 @@ namespace config { #define AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_CBR 1 #define AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_PEAK_CONSTRAINED_VBR 2 #define AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_LATENCY_CONSTRAINED_VBR 3 - #define AMF_VIDEO_ENCODER_HEVC_USAGE_TRANSCONDING 0 + #define AMF_VIDEO_ENCODER_AV1_USAGE_TRANSCODING 0 + #define AMF_VIDEO_ENCODER_AV1_USAGE_LOW_LATENCY 1 + #define AMF_VIDEO_ENCODER_AV1_USAGE_ULTRA_LOW_LATENCY 2 + #define AMF_VIDEO_ENCODER_AV1_USAGE_WEBCAM 3 + #define AMF_VIDEO_ENCODER_AV1_USAGE_LOW_LATENCY_HIGH_QUALITY 5 + #define AMF_VIDEO_ENCODER_HEVC_USAGE_TRANSCODING 0 #define AMF_VIDEO_ENCODER_HEVC_USAGE_ULTRA_LOW_LATENCY 1 #define AMF_VIDEO_ENCODER_HEVC_USAGE_LOW_LATENCY 2 #define AMF_VIDEO_ENCODER_HEVC_USAGE_WEBCAM 3 - #define AMF_VIDEO_ENCODER_USAGE_TRANSCONDING 0 + #define AMF_VIDEO_ENCODER_HEVC_USAGE_LOW_LATENCY_HIGH_QUALITY 5 + #define AMF_VIDEO_ENCODER_USAGE_TRANSCODING 0 #define AMF_VIDEO_ENCODER_USAGE_ULTRA_LOW_LATENCY 1 #define AMF_VIDEO_ENCODER_USAGE_LOW_LATENCY 2 #define AMF_VIDEO_ENCODER_USAGE_WEBCAM 3 + #define AMF_VIDEO_ENCODER_USAGE_LOW_LATENCY_HIGH_QUALITY 5 #define AMF_VIDEO_ENCODER_UNDEFINED 0 #define AMF_VIDEO_ENCODER_CABAC 1 #define AMF_VIDEO_ENCODER_CALV 2 #else + #include #include #include #endif + enum class quality_av1_e : int { + speed = AMF_VIDEO_ENCODER_AV1_QUALITY_PRESET_SPEED, + quality = AMF_VIDEO_ENCODER_AV1_QUALITY_PRESET_QUALITY, + balanced = AMF_VIDEO_ENCODER_AV1_QUALITY_PRESET_BALANCED + }; + enum class quality_hevc_e : int { speed = AMF_VIDEO_ENCODER_HEVC_QUALITY_PRESET_SPEED, quality = AMF_VIDEO_ENCODER_HEVC_QUALITY_PRESET_QUALITY, @@ -172,30 +124,47 @@ namespace config { balanced = AMF_VIDEO_ENCODER_QUALITY_PRESET_BALANCED }; + enum class rc_av1_e : int { + cbr = AMF_VIDEO_ENCODER_AV1_RATE_CONTROL_METHOD_CBR, + cqp = AMF_VIDEO_ENCODER_AV1_RATE_CONTROL_METHOD_CONSTANT_QP, + vbr_latency = AMF_VIDEO_ENCODER_AV1_RATE_CONTROL_METHOD_LATENCY_CONSTRAINED_VBR, + vbr_peak = AMF_VIDEO_ENCODER_AV1_RATE_CONTROL_METHOD_PEAK_CONSTRAINED_VBR + }; + enum class rc_hevc_e : int { + cbr = AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_CBR, cqp = AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_CONSTANT_QP, vbr_latency = AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_LATENCY_CONSTRAINED_VBR, - vbr_peak = AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_PEAK_CONSTRAINED_VBR, - cbr = AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_CBR + vbr_peak = AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_PEAK_CONSTRAINED_VBR }; enum class rc_h264_e : int { + cbr = AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_CBR, cqp = AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_CONSTANT_QP, vbr_latency = AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_LATENCY_CONSTRAINED_VBR, - vbr_peak = AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_PEAK_CONSTRAINED_VBR, - cbr = AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_CBR + vbr_peak = AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_PEAK_CONSTRAINED_VBR + }; + + enum class usage_av1_e : int { + transcoding = AMF_VIDEO_ENCODER_AV1_USAGE_TRANSCODING, + webcam = AMF_VIDEO_ENCODER_AV1_USAGE_WEBCAM, + lowlatency_high_quality = AMF_VIDEO_ENCODER_AV1_USAGE_LOW_LATENCY_HIGH_QUALITY, + lowlatency = AMF_VIDEO_ENCODER_AV1_USAGE_LOW_LATENCY, + ultralowlatency = AMF_VIDEO_ENCODER_AV1_USAGE_ULTRA_LOW_LATENCY }; enum class usage_hevc_e : int { - transcoding = AMF_VIDEO_ENCODER_HEVC_USAGE_TRANSCONDING, + transcoding = AMF_VIDEO_ENCODER_HEVC_USAGE_TRANSCODING, webcam = AMF_VIDEO_ENCODER_HEVC_USAGE_WEBCAM, + lowlatency_high_quality = AMF_VIDEO_ENCODER_HEVC_USAGE_LOW_LATENCY_HIGH_QUALITY, lowlatency = AMF_VIDEO_ENCODER_HEVC_USAGE_LOW_LATENCY, ultralowlatency = AMF_VIDEO_ENCODER_HEVC_USAGE_ULTRA_LOW_LATENCY }; enum class usage_h264_e : int { - transcoding = AMF_VIDEO_ENCODER_USAGE_TRANSCONDING, + transcoding = AMF_VIDEO_ENCODER_USAGE_TRANSCODING, webcam = AMF_VIDEO_ENCODER_USAGE_WEBCAM, + lowlatency_high_quality = AMF_VIDEO_ENCODER_USAGE_LOW_LATENCY_HIGH_QUALITY, lowlatency = AMF_VIDEO_ENCODER_USAGE_LOW_LATENCY, ultralowlatency = AMF_VIDEO_ENCODER_USAGE_ULTRA_LOW_LATENCY }; @@ -206,39 +175,43 @@ namespace config { cavlc = AMF_VIDEO_ENCODER_CALV }; + template std::optional - quality_from_view(const std::string_view &quality_type, int codec) { + quality_from_view(const std::string_view &quality_type, const std::optional(&original)) { #define _CONVERT_(x) \ - if (quality_type == #x##sv) return codec == 0 ? (int) quality_hevc_e::x : (int) quality_h264_e::x + if (quality_type == #x##sv) return (int) T::x + _CONVERT_(balanced); _CONVERT_(quality); _CONVERT_(speed); - _CONVERT_(balanced); #undef _CONVERT_ - return std::nullopt; + return original; } + template std::optional - rc_from_view(const std::string_view &rc, int codec) { + rc_from_view(const std::string_view &rc, const std::optional(&original)) { #define _CONVERT_(x) \ - if (rc == #x##sv) return codec == 0 ? (int) rc_hevc_e::x : (int) rc_h264_e::x + if (rc == #x##sv) return (int) T::x + _CONVERT_(cbr); _CONVERT_(cqp); _CONVERT_(vbr_latency); _CONVERT_(vbr_peak); - _CONVERT_(cbr); #undef _CONVERT_ - return std::nullopt; + return original; } + template std::optional - usage_from_view(const std::string_view &rc, int codec) { + usage_from_view(const std::string_view &usage, const std::optional(&original)) { #define _CONVERT_(x) \ - if (rc == #x##sv) return codec == 0 ? (int) usage_hevc_e::x : (int) usage_h264_e::x - _CONVERT_(transcoding); - _CONVERT_(webcam); + if (usage == #x##sv) return (int) T::x _CONVERT_(lowlatency); + _CONVERT_(lowlatency_high_quality); + _CONVERT_(transcoding); _CONVERT_(ultralowlatency); + _CONVERT_(webcam); #undef _CONVERT_ - return std::nullopt; + return original; } int @@ -247,7 +220,7 @@ namespace config { if (coder == "cabac"sv || coder == "ac"sv) return cabac; if (coder == "cavlc"sv || coder == "vlc"sv) return cavlc; - return -1; + return _auto; } } // namespace amd @@ -333,36 +306,61 @@ namespace config { } // namespace vt + namespace sw { + int + svtav1_preset_from_view(const std::string_view &preset) { +#define _CONVERT_(x, y) \ + if (preset == #x##sv) return y + _CONVERT_(veryslow, 1); + _CONVERT_(slower, 2); + _CONVERT_(slow, 4); + _CONVERT_(medium, 5); + _CONVERT_(fast, 7); + _CONVERT_(faster, 9); + _CONVERT_(veryfast, 10); + _CONVERT_(superfast, 11); + _CONVERT_(ultrafast, 12); +#undef _CONVERT_ + return 11; // Default to superfast + } + } // namespace sw + video_t video { 28, // qp 0, // hevc_mode + 0, // av1_mode - 1, // min_threads + 2, // min_threads { "superfast"s, // preset "zerolatency"s, // tune + 11, // superfast }, // software - { - nv::p4, // preset - nv::ull, // tune - nv::cbr, // rc - nv::_auto // coder - }, // nv + {}, // nv + true, // nv_realtime_hags + true, // nv_opengl_vulkan_on_dxgi + true, // nv_sunshine_high_power_mode + {}, // nv_legacy { qsv::medium, // preset qsv::_auto, // cavlc + false, // slow_hevc }, // qsv { - (int) amd::quality_h264_e::balanced, // quality (h264) - (int) amd::quality_hevc_e::balanced, // quality (hevc) - (int) amd::rc_h264_e::vbr_latency, // rate control (h264) - (int) amd::rc_hevc_e::vbr_latency, // rate control (hevc) (int) amd::usage_h264_e::ultralowlatency, // usage (h264) (int) amd::usage_hevc_e::ultralowlatency, // usage (hevc) + (int) amd::usage_av1_e::ultralowlatency, // usage (av1) + (int) amd::rc_h264_e::vbr_latency, // rate control (h264) + (int) amd::rc_hevc_e::vbr_latency, // rate control (hevc) + (int) amd::rc_av1_e::vbr_latency, // rate control (av1) + 0, // enforce_hrd + (int) amd::quality_h264_e::balanced, // quality (h264) + (int) amd::quality_hevc_e::balanced, // quality (hevc) + (int) amd::quality_av1_e::balanced, // quality (av1) 0, // preanalysis 1, // vbaq (int) amd::coder_e::_auto, // coder @@ -379,7 +377,6 @@ namespace config { {}, // encoder {}, // adapter_name {}, // output_name - true // dwmflush }; audio_t audio { @@ -394,11 +391,13 @@ namespace config { APPS_JSON_PATH, 20, // fecPercentage - 1 // channels + 1, // channels + + ENCRYPTION_MODE_NEVER, // lan_encryption_mode + ENCRYPTION_MODE_OPPORTUNISTIC, // wan_encryption_mode }; nvhttp_t nvhttp { - "pc", // origin_pin "lan", // origin web manager PRIVATE_KEY_FILE, @@ -414,6 +413,7 @@ namespace config { "1280x720"s, "1920x1080"s, "2560x1080"s, + "2560x1440"s, "3440x1440"s, "1920x1200"s, "3840x2160"s, @@ -437,14 +437,20 @@ namespace config { platf::supported_gamepads().front().data(), platf::supported_gamepads().front().size(), }, // Default gamepad + true, // back as touchpad click enabled (manual DS4 only) + true, // client gamepads with motion events are emulated as DS4 + true, // client gamepads with touchpads are emulated as DS4 true, // keyboard enabled true, // mouse enabled true, // controller enabled true, // always send scancodes + true, // high resolution scrolling + true, // native pen/touch support }; sunshine_t sunshine { + "en", // locale 2, // min_log_level 0, // flags {}, // User file @@ -453,8 +459,10 @@ namespace config { {}, // Password Salt platf::appdata().string() + "/sunshine.conf", // config file {}, // cmd args - 47989, + 47989, // Base port number + "ipv4", // Address family platf::appdata().string() + "/sunshine.log", // log file + false, // notify_pre_releases {}, // prep commands }; @@ -581,6 +589,16 @@ namespace config { vars.erase(it); } + template + void + generic_f(std::unordered_map &vars, const std::string &name, T &input, F &&f) { + std::string tmp; + string_f(vars, name, tmp); + if (!tmp.empty()) { + input = f(tmp); + } + } + void string_restricted_f(std::unordered_map &vars, const std::string &name, std::string &input, const std::vector &allowed_vals) { std::string temp; @@ -842,6 +860,15 @@ namespace config { std::vector list; list_string_f(vars, name, list); + // check if list is empty, i.e. when the value doesn't exist in the config file + if (list.empty()) { + return; + } + + // The framerate list must be cleared before adding values from the file configuration. + // If the list is not cleared, then the specified parameters do not affect the behavior of the sunshine server. + // That is, if you set only 30 fps in the configuration file, it will not work because by default, during initialization the list includes 10, 30, 60, 90 and 120 fps. + input.clear(); for (auto &el : list) { std::string_view val = el; @@ -924,40 +951,64 @@ namespace config { int_f(vars, "qp", video.qp); int_f(vars, "min_threads", video.min_threads); int_between_f(vars, "hevc_mode", video.hevc_mode, { 0, 3 }); + int_between_f(vars, "av1_mode", video.av1_mode, { 0, 3 }); string_f(vars, "sw_preset", video.sw.sw_preset); + if (!video.sw.sw_preset.empty()) { + video.sw.svtav1_preset = sw::svtav1_preset_from_view(video.sw.sw_preset); + } string_f(vars, "sw_tune", video.sw.sw_tune); - int_f(vars, "nv_preset", video.nv.nv_preset, nv::preset_from_view); - int_f(vars, "nv_tune", video.nv.nv_tune, nv::tune_from_view); - int_f(vars, "nv_rc", video.nv.nv_rc, nv::rc_from_view); - int_f(vars, "nv_coder", video.nv.nv_coder, nv::coder_from_view); + + int_between_f(vars, "nvenc_preset", video.nv.quality_preset, { 1, 7 }); + int_between_f(vars, "nvenc_vbv_increase", video.nv.vbv_percentage_increase, { 0, 400 }); + bool_f(vars, "nvenc_spatial_aq", video.nv.adaptive_quantization); + generic_f(vars, "nvenc_twopass", video.nv.two_pass, nv::twopass_from_view); + bool_f(vars, "nvenc_h264_cavlc", video.nv.h264_cavlc); + bool_f(vars, "nvenc_realtime_hags", video.nv_realtime_hags); + bool_f(vars, "nvenc_opengl_vulkan_on_dxgi", video.nv_opengl_vulkan_on_dxgi); + bool_f(vars, "nvenc_latency_over_power", video.nv_sunshine_high_power_mode); + +#ifndef __APPLE__ + video.nv_legacy.preset = video.nv.quality_preset + 11; + video.nv_legacy.multipass = video.nv.two_pass == nvenc::nvenc_two_pass::quarter_resolution ? NV_ENC_TWO_PASS_QUARTER_RESOLUTION : + video.nv.two_pass == nvenc::nvenc_two_pass::full_resolution ? NV_ENC_TWO_PASS_FULL_RESOLUTION : + NV_ENC_MULTI_PASS_DISABLED; + video.nv_legacy.h264_coder = video.nv.h264_cavlc ? NV_ENC_H264_ENTROPY_CODING_MODE_CAVLC : NV_ENC_H264_ENTROPY_CODING_MODE_CABAC; + video.nv_legacy.aq = video.nv.adaptive_quantization; + video.nv_legacy.vbv_percentage_increase = video.nv.vbv_percentage_increase; +#endif int_f(vars, "qsv_preset", video.qsv.qsv_preset, qsv::preset_from_view); int_f(vars, "qsv_coder", video.qsv.qsv_cavlc, qsv::coder_from_view); + bool_f(vars, "qsv_slow_hevc", video.qsv.qsv_slow_hevc); std::string quality; string_f(vars, "amd_quality", quality); if (!quality.empty()) { - video.amd.amd_quality_h264 = amd::quality_from_view(quality, 1); - video.amd.amd_quality_hevc = amd::quality_from_view(quality, 0); + video.amd.amd_quality_h264 = amd::quality_from_view(quality, video.amd.amd_quality_h264); + video.amd.amd_quality_hevc = amd::quality_from_view(quality, video.amd.amd_quality_hevc); + video.amd.amd_quality_av1 = amd::quality_from_view(quality, video.amd.amd_quality_av1); } std::string rc; string_f(vars, "amd_rc", rc); int_f(vars, "amd_coder", video.amd.amd_coder, amd::coder_from_view); if (!rc.empty()) { - video.amd.amd_rc_h264 = amd::rc_from_view(rc, 1); - video.amd.amd_rc_hevc = amd::rc_from_view(rc, 0); + video.amd.amd_rc_h264 = amd::rc_from_view(rc, video.amd.amd_rc_h264); + video.amd.amd_rc_hevc = amd::rc_from_view(rc, video.amd.amd_rc_hevc); + video.amd.amd_rc_av1 = amd::rc_from_view(rc, video.amd.amd_rc_av1); } std::string usage; string_f(vars, "amd_usage", usage); if (!usage.empty()) { - video.amd.amd_usage_h264 = amd::usage_from_view(rc, 1); - video.amd.amd_usage_hevc = amd::usage_from_view(rc, 0); + video.amd.amd_usage_h264 = amd::usage_from_view(usage, video.amd.amd_usage_h264); + video.amd.amd_usage_hevc = amd::usage_from_view(usage, video.amd.amd_usage_hevc); + video.amd.amd_usage_av1 = amd::usage_from_view(usage, video.amd.amd_usage_av1); } bool_f(vars, "amd_preanalysis", (bool &) video.amd.amd_preanalysis); bool_f(vars, "amd_vbaq", (bool &) video.amd.amd_vbaq); + bool_f(vars, "amd_enforce_hrd", (bool &) video.amd.amd_enforce_hrd); int_f(vars, "vt_coder", video.vt.vt_coder, vt::coder_from_view); int_f(vars, "vt_software", video.vt.vt_allow_sw, vt::allow_software_from_view); @@ -968,7 +1019,6 @@ namespace config { string_f(vars, "encoder", video.encoder); string_f(vars, "adapter_name", video.adapter_name); string_f(vars, "output_name", video.output_name); - bool_f(vars, "dwmflush", video.dwmflush); path_f(vars, "pkey", nvhttp.pkey); path_f(vars, "cert", nvhttp.cert); @@ -989,7 +1039,6 @@ namespace config { string_f(vars, "virtual_sink", audio.virtual_sink); bool_f(vars, "install_steam_audio_drivers", audio.install_steam_drivers); - string_restricted_f(vars, "origin_pin_allowed", nvhttp.origin_pin_allowed, { "pc"sv, "lan"sv, "wan"sv }); string_restricted_f(vars, "origin_web_ui_allowed", nvhttp.origin_web_ui_allowed, { "pc"sv, "lan"sv, "wan"sv }); int to = -1; @@ -1000,6 +1049,9 @@ namespace config { int_between_f(vars, "channels", stream.channels, { 1, std::numeric_limits::max() }); + int_between_f(vars, "lan_encryption_mode", stream.lan_encryption_mode, { 0, 2 }); + int_between_f(vars, "wan_encryption_mode", stream.wan_encryption_mode, { 0, 2 }); + path_f(vars, "file_apps", stream.file_apps); int_between_f(vars, "fec_percentage", stream.fec_percentage, { 1, 255 }); @@ -1035,6 +1087,9 @@ namespace config { } string_restricted_f(vars, "gamepad"s, input.gamepad, platf::supported_gamepads()); + bool_f(vars, "ds4_back_as_touchpad_click", input.ds4_back_as_touchpad_click); + bool_f(vars, "motion_as_ds4", input.motion_as_ds4); + bool_f(vars, "touchpad_as_ds4", input.touchpad_as_ds4); bool_f(vars, "mouse", input.mouse); bool_f(vars, "keyboard", input.keyboard); @@ -1042,10 +1097,17 @@ namespace config { bool_f(vars, "always_send_scancodes", input.always_send_scancodes); + bool_f(vars, "high_resolution_scrolling", input.high_resolution_scrolling); + bool_f(vars, "native_pen_touch", input.native_pen_touch); + + bool_f(vars, "notify_pre_releases", sunshine.notify_pre_releases); + int port = sunshine.port; - int_f(vars, "port"s, port); + int_between_f(vars, "port"s, port, { 1024 + nvhttp::PORT_HTTPS, 65535 - rtsp_stream::RTSP_SETUP_PORT }); sunshine.port = (std::uint16_t) port; + string_restricted_f(vars, "address_family", sunshine.address_family, { "ipv4"sv, "both"sv }); + bool upnp = false; bool_f(vars, "upnp"s, upnp); @@ -1053,6 +1115,21 @@ namespace config { config::sunshine.flags[config::flag::UPNP].flip(); } + string_restricted_f(vars, "locale", config::sunshine.locale, { + "de"sv, // German + "en"sv, // English + "en_GB"sv, // English (UK) + "en_US"sv, // English (US) + "es"sv, // Spanish + "fr"sv, // French + "it"sv, // Italian + "ja"sv, // Japanese + "pt"sv, // Portuguese + "ru"sv, // Russian + "sv"sv, // Swedish + "zh"sv, // Chinese + }); + std::string log_level_string; string_f(vars, "min_log_level", log_level_string); @@ -1113,7 +1190,7 @@ namespace config { auto line = argv[x]; if (line == "--help"sv) { - print_help(*argv); + logging::print_help(*argv); return 1; } #ifdef _WIN32 @@ -1133,7 +1210,7 @@ namespace config { break; } if (apply_flags(line + 1)) { - print_help(*argv); + logging::print_help(*argv); return -1; } } @@ -1147,7 +1224,7 @@ namespace config { else { TUPLE_EL(var, 1, parse_option(line, line_end)); if (!var) { - print_help(*argv); + logging::print_help(*argv); return -1; } @@ -1176,7 +1253,7 @@ namespace config { } // Read config file - auto vars = parse_config(read_file(sunshine.config_file.c_str())); + auto vars = parse_config(file_handler::read_file(sunshine.config_file.c_str())); for (auto &[name, value] : cmd_vars) { vars.insert_or_assign(std::move(name), std::move(value)); @@ -1195,10 +1272,16 @@ namespace config { BOOST_LOG(fatal) << "Failed to apply config: "sv << err.what(); } - if (!config_loaded) { #ifdef _WIN32 + // UCRT64 raises an access denied exception if launching from the shortcut + // as non-admin and the config folder is not yet present; we can defer + // so that service instance will do the work instead. + + if (!config_loaded && !shortcut_launch) { BOOST_LOG(fatal) << "To relaunch Sunshine successfully, use the shortcut in the Start Menu. Do not run Sunshine.exe manually."sv; std::this_thread::sleep_for(10s); +#else + if (!config_loaded) { #endif return -1; } @@ -1206,6 +1289,8 @@ namespace config { #ifdef _WIN32 // We have to wait until the config is loaded to handle these launches, // because we need to have the correct base port loaded in our config. + // Exception: UCRT64 shortcut_launch instances may have no config loaded due to + // insufficient permissions to create folder; port defaults will be acceptable. if (service_admin_launch) { // This is a relaunch as admin to start the service service_ctrl::start_service(); diff --git a/src/config.h b/src/config.h index 2c32e7afd1b..80ff698aaee 100644 --- a/src/config.h +++ b/src/config.h @@ -11,38 +11,53 @@ #include #include +#include "nvenc/nvenc_config.h" + namespace config { struct video_t { // ffmpeg params int qp; // higher == more compression and less quality int hevc_mode; + int av1_mode; int min_threads; // Minimum number of threads/slices for CPU encoding struct { std::string sw_preset; std::string sw_tune; + std::optional svtav1_preset; } sw; + nvenc::nvenc_config nv; + bool nv_realtime_hags; + bool nv_opengl_vulkan_on_dxgi; + bool nv_sunshine_high_power_mode; + struct { - std::optional nv_preset; - std::optional nv_tune; - std::optional nv_rc; - int nv_coder; - } nv; + int preset; + int multipass; + int h264_coder; + int aq; + int vbv_percentage_increase; + } nv_legacy; struct { std::optional qsv_preset; std::optional qsv_cavlc; + bool qsv_slow_hevc; } qsv; struct { - std::optional amd_quality_h264; - std::optional amd_quality_hevc; - std::optional amd_rc_h264; - std::optional amd_rc_hevc; std::optional amd_usage_h264; std::optional amd_usage_hevc; + std::optional amd_usage_av1; + std::optional amd_rc_h264; + std::optional amd_rc_hevc; + std::optional amd_rc_av1; + std::optional amd_enforce_hrd; + std::optional amd_quality_h264; + std::optional amd_quality_hevc; + std::optional amd_quality_av1; std::optional amd_preanalysis; std::optional amd_vbaq; int amd_coder; @@ -59,7 +74,6 @@ namespace config { std::string encoder; std::string adapter_name; std::string output_name; - bool dwmflush; }; struct audio_t { @@ -68,6 +82,10 @@ namespace config { bool install_steam_drivers; }; + constexpr int ENCRYPTION_MODE_NEVER = 0; // Never use video encryption, even if the client supports it + constexpr int ENCRYPTION_MODE_OPPORTUNISTIC = 1; // Use video encryption if available, but stream without it if not supported + constexpr int ENCRYPTION_MODE_MANDATORY = 2; // Always use video encryption and refuse clients that can't encrypt + struct stream_t { std::chrono::milliseconds ping_timeout; @@ -77,16 +95,19 @@ namespace config { // max unique instances of video and audio streams int channels; + + // Video encryption settings for LAN and WAN streams + int lan_encryption_mode; + int wan_encryption_mode; }; struct nvhttp_t { // Could be any of the following values: // pc|lan|wan - std::string origin_pin_allowed; std::string origin_web_ui_allowed; - std::string pkey; // must be 2048 bits - std::string cert; // must be signed with a key of 2048 bits + std::string pkey; + std::string cert; std::string sunshine_name; @@ -105,12 +126,18 @@ namespace config { std::chrono::duration key_repeat_period; std::string gamepad; + bool ds4_back_as_touchpad_click; + bool motion_as_ds4; + bool touchpad_as_ds4; bool keyboard; bool mouse; bool controller; bool always_send_scancodes; + + bool high_resolution_scrolling; + bool native_pen_touch; }; namespace flag { @@ -134,6 +161,7 @@ namespace config { bool elevated; }; struct sunshine_t { + std::string locale; int min_log_level; std::bitset flags; std::string credentials_file; @@ -151,8 +179,10 @@ namespace config { } cmd; std::uint16_t port; - std::string log_file; + std::string address_family; + std::string log_file; + bool notify_pre_releases; std::vector prep_cmds; }; diff --git a/src/confighttp.cpp b/src/confighttp.cpp index 6e8b2393730..a237c1fc3a7 100644 --- a/src/confighttp.cpp +++ b/src/confighttp.cpp @@ -29,8 +29,10 @@ #include "config.h" #include "confighttp.h" #include "crypto.h" +#include "file_handler.h" +#include "globals.h" #include "httpcommon.h" -#include "main.h" +#include "logging.h" #include "network.h" #include "nvhttp.h" #include "platform/common.h" @@ -76,7 +78,7 @@ namespace confighttp { void send_unauthorized(resp_https_t response, req_https_t request) { - auto address = request->remote_endpoint().address().to_string(); + auto address = net::addr_to_normalized_string(request->remote_endpoint().address()); BOOST_LOG(info) << "Web UI: ["sv << address << "] -- not authorized"sv; const SimpleWeb::CaseInsensitiveMultimap headers { { "WWW-Authenticate", R"(Basic realm="Sunshine Gamestream Host", charset="UTF-8")" } @@ -86,7 +88,7 @@ namespace confighttp { void send_redirect(resp_https_t response, req_https_t request, const char *path) { - auto address = request->remote_endpoint().address().to_string(); + auto address = net::addr_to_normalized_string(request->remote_endpoint().address()); BOOST_LOG(info) << "Web UI: ["sv << address << "] -- not authorized"sv; const SimpleWeb::CaseInsensitiveMultimap headers { { "Location", path } @@ -96,7 +98,7 @@ namespace confighttp { bool authenticate(resp_https_t response, req_https_t request) { - auto address = request->remote_endpoint().address().to_string(); + auto address = net::addr_to_normalized_string(request->remote_endpoint().address()); auto ip_type = net::from_address(address); if (ip_type > http::origin_web_ui_allowed) { @@ -161,11 +163,10 @@ namespace confighttp { print_req(request); - std::string header = read_file(WEB_DIR "header.html"); - std::string content = read_file(WEB_DIR "index.html"); + std::string content = file_handler::read_file(WEB_DIR "index.html"); SimpleWeb::CaseInsensitiveMultimap headers; headers.emplace("Content-Type", "text/html; charset=utf-8"); - response->write(header + content, headers); + response->write(content, headers); } void @@ -174,11 +175,10 @@ namespace confighttp { print_req(request); - std::string header = read_file(WEB_DIR "header.html"); - std::string content = read_file(WEB_DIR "pin.html"); + std::string content = file_handler::read_file(WEB_DIR "pin.html"); SimpleWeb::CaseInsensitiveMultimap headers; headers.emplace("Content-Type", "text/html; charset=utf-8"); - response->write(header + content, headers); + response->write(content, headers); } void @@ -187,12 +187,11 @@ namespace confighttp { print_req(request); - std::string header = read_file(WEB_DIR "header.html"); - std::string content = read_file(WEB_DIR "apps.html"); + std::string content = file_handler::read_file(WEB_DIR "apps.html"); SimpleWeb::CaseInsensitiveMultimap headers; headers.emplace("Content-Type", "text/html; charset=utf-8"); headers.emplace("Access-Control-Allow-Origin", "https://images.igdb.com/"); - response->write(header + content, headers); + response->write(content, headers); } void @@ -201,11 +200,10 @@ namespace confighttp { print_req(request); - std::string header = read_file(WEB_DIR "header.html"); - std::string content = read_file(WEB_DIR "clients.html"); + std::string content = file_handler::read_file(WEB_DIR "clients.html"); SimpleWeb::CaseInsensitiveMultimap headers; headers.emplace("Content-Type", "text/html; charset=utf-8"); - response->write(header + content, headers); + response->write(content, headers); } void @@ -214,11 +212,10 @@ namespace confighttp { print_req(request); - std::string header = read_file(WEB_DIR "header.html"); - std::string content = read_file(WEB_DIR "config.html"); + std::string content = file_handler::read_file(WEB_DIR "config.html"); SimpleWeb::CaseInsensitiveMultimap headers; headers.emplace("Content-Type", "text/html; charset=utf-8"); - response->write(header + content, headers); + response->write(content, headers); } void @@ -227,11 +224,10 @@ namespace confighttp { print_req(request); - std::string header = read_file(WEB_DIR "header.html"); - std::string content = read_file(WEB_DIR "password.html"); + std::string content = file_handler::read_file(WEB_DIR "password.html"); SimpleWeb::CaseInsensitiveMultimap headers; headers.emplace("Content-Type", "text/html; charset=utf-8"); - response->write(header + content, headers); + response->write(content, headers); } void @@ -241,11 +237,10 @@ namespace confighttp { send_redirect(response, request, "/"); return; } - std::string header = read_file(WEB_DIR "header-no-nav.html"); - std::string content = read_file(WEB_DIR "welcome.html"); + std::string content = file_handler::read_file(WEB_DIR "welcome.html"); SimpleWeb::CaseInsensitiveMultimap headers; headers.emplace("Content-Type", "text/html; charset=utf-8"); - response->write(header + content, headers); + response->write(content, headers); } void @@ -254,11 +249,10 @@ namespace confighttp { print_req(request); - std::string header = read_file(WEB_DIR "header.html"); - std::string content = read_file(WEB_DIR "troubleshooting.html"); + std::string content = file_handler::read_file(WEB_DIR "troubleshooting.html"); SimpleWeb::CaseInsensitiveMultimap headers; headers.emplace("Content-Type", "text/html; charset=utf-8"); - response->write(header + content, headers); + response->write(content, headers); } void @@ -267,7 +261,7 @@ namespace confighttp { // todo - use mime_types map print_req(request); - std::ifstream in(WEB_DIR "images/favicon.ico", std::ios::binary); + std::ifstream in(WEB_DIR "images/sunshine.ico", std::ios::binary); SimpleWeb::CaseInsensitiveMultimap headers; headers.emplace("Content-Type", "image/x-icon"); response->write(SimpleWeb::StatusCode::success_ok, in, headers); @@ -295,14 +289,14 @@ namespace confighttp { getNodeModules(resp_https_t response, req_https_t request) { print_req(request); fs::path webDirPath(WEB_DIR); - fs::path nodeModulesPath(webDirPath / "node_modules"); + fs::path nodeModulesPath(webDirPath / "assets"); // .relative_path is needed to shed any leading slash that might exist in the request path auto filePath = fs::weakly_canonical(webDirPath / fs::path(request->path).relative_path()); - // Don't do anything if file does not exist or is outside the node_modules directory + // Don't do anything if file does not exist or is outside the assets directory if (!isChildPath(filePath, nodeModulesPath)) { - BOOST_LOG(warning) << "Someone requested a path " << filePath << " that is outside the node_modules folder"; + BOOST_LOG(warning) << "Someone requested a path " << filePath << " that is outside the assets folder"; response->write(SimpleWeb::StatusCode::client_error_bad_request, "Bad Request"); } else if (!fs::exists(filePath)) { @@ -331,7 +325,7 @@ namespace confighttp { print_req(request); - std::string content = read_file(config::stream.file_apps.c_str()); + std::string content = file_handler::read_file(config::stream.file_apps.c_str()); SimpleWeb::CaseInsensitiveMultimap headers; headers.emplace("Content-Type", "application/json"); response->write(content, headers); @@ -343,7 +337,7 @@ namespace confighttp { print_req(request); - std::string content = read_file(config::sunshine.log_file.c_str()); + std::string content = file_handler::read_file(config::sunshine.log_file.c_str()); SimpleWeb::CaseInsensitiveMultimap headers; headers.emplace("Content-Type", "text/plain"); response->write(SimpleWeb::StatusCode::success_ok, content, headers); @@ -368,7 +362,7 @@ namespace confighttp { pt::ptree inputTree, fileTree; - BOOST_LOG(fatal) << config::stream.file_apps; + BOOST_LOG(info) << config::stream.file_apps; try { // TODO: Input Validation pt::read_json(ss, inputTree); @@ -549,13 +543,31 @@ namespace confighttp { outputTree.put("platform", SUNSHINE_PLATFORM); outputTree.put("version", PROJECT_VER); - auto vars = config::parse_config(read_file(config::sunshine.config_file.c_str())); + auto vars = config::parse_config(file_handler::read_file(config::sunshine.config_file.c_str())); for (auto &[name, value] : vars) { outputTree.put(std::move(name), std::move(value)); } } + void + getLocale(resp_https_t response, req_https_t request) { + // we need to return the locale whether authenticated or not + + print_req(request); + + pt::ptree outputTree; + auto g = util::fail_guard([&]() { + std::ostringstream data; + + pt::write_json(data, outputTree); + response->write(data.str()); + }); + + outputTree.put("status", "true"); + outputTree.put("locale", config::sunshine.locale); + } + void saveConfig(resp_https_t response, req_https_t request) { if (!authenticate(response, request)) return; @@ -582,7 +594,7 @@ namespace confighttp { configStream << kv.first << " = " << value << std::endl; } - write_file(config::sunshine.config_file.c_str(), configStream.str()); + file_handler::write_file(config::sunshine.config_file.c_str(), configStream.str()); } catch (std::exception &e) { BOOST_LOG(warning) << "SaveConfig: "sv << e.what(); @@ -681,7 +693,8 @@ namespace confighttp { // TODO: Input Validation pt::read_json(ss, inputTree); std::string pin = inputTree.get("pin"); - outputTree.put("status", nvhttp::pin(pin)); + std::string name = inputTree.get("name"); + outputTree.put("status", nvhttp::pin(pin, name)); } catch (std::exception &e) { BOOST_LOG(warning) << "SavePin: "sv << e.what(); @@ -705,6 +718,60 @@ namespace confighttp { response->write(data.str()); }); nvhttp::erase_all_clients(); + proc::proc.terminate(); + outputTree.put("status", true); + } + + void + unpair(resp_https_t response, req_https_t request) { + if (!authenticate(response, request)) return; + + print_req(request); + + std::stringstream ss; + ss << request->content.rdbuf(); + + pt::ptree inputTree, outputTree; + + auto g = util::fail_guard([&]() { + std::ostringstream data; + pt::write_json(data, outputTree); + response->write(data.str()); + }); + + try { + // TODO: Input Validation + pt::read_json(ss, inputTree); + std::string uuid = inputTree.get("uuid"); + outputTree.put("status", nvhttp::unpair_client(uuid)); + } + catch (std::exception &e) { + BOOST_LOG(warning) << "Unpair: "sv << e.what(); + outputTree.put("status", false); + outputTree.put("error", e.what()); + return; + } + } + + void + listClients(resp_https_t response, req_https_t request) { + if (!authenticate(response, request)) return; + + print_req(request); + + pt::ptree named_certs = nvhttp::get_all_clients(); + + pt::ptree outputTree; + + outputTree.put("status", false); + + auto g = util::fail_guard([&]() { + std::ostringstream data; + pt::write_json(data, outputTree); + response->write(data.str()); + }); + + outputTree.add_child("named_certs", named_certs); outputTree.put("status", true); } @@ -730,35 +797,39 @@ namespace confighttp { start() { auto shutdown_event = mail::man->event(mail::shutdown); - auto port_https = map_port(PORT_HTTPS); + auto port_https = net::map_port(PORT_HTTPS); + auto address_family = net::af_from_enum_string(config::sunshine.address_family); https_server_t server { config::nvhttp.cert, config::nvhttp.pkey }; server.default_resource["GET"] = not_found; server.resource["^/$"]["GET"] = getIndexPage; - server.resource["^/pin$"]["GET"] = getPinPage; - server.resource["^/apps$"]["GET"] = getAppsPage; - server.resource["^/clients$"]["GET"] = getClientsPage; - server.resource["^/config$"]["GET"] = getConfigPage; - server.resource["^/password$"]["GET"] = getPasswordPage; - server.resource["^/welcome$"]["GET"] = getWelcomePage; - server.resource["^/troubleshooting$"]["GET"] = getTroubleshootingPage; + server.resource["^/pin/?$"]["GET"] = getPinPage; + server.resource["^/apps/?$"]["GET"] = getAppsPage; + server.resource["^/clients/?$"]["GET"] = getClientsPage; + server.resource["^/config/?$"]["GET"] = getConfigPage; + server.resource["^/password/?$"]["GET"] = getPasswordPage; + server.resource["^/welcome/?$"]["GET"] = getWelcomePage; + server.resource["^/troubleshooting/?$"]["GET"] = getTroubleshootingPage; server.resource["^/api/pin$"]["POST"] = savePin; server.resource["^/api/apps$"]["GET"] = getApps; server.resource["^/api/logs$"]["GET"] = getLogs; server.resource["^/api/apps$"]["POST"] = saveApp; server.resource["^/api/config$"]["GET"] = getConfig; server.resource["^/api/config$"]["POST"] = saveConfig; + server.resource["^/api/configLocale$"]["GET"] = getLocale; server.resource["^/api/restart$"]["POST"] = restart; server.resource["^/api/password$"]["POST"] = savePassword; server.resource["^/api/apps/([0-9]+)$"]["DELETE"] = deleteApp; - server.resource["^/api/clients/unpair$"]["POST"] = unpairAll; + server.resource["^/api/clients/unpair-all$"]["POST"] = unpairAll; + server.resource["^/api/clients/list$"]["GET"] = listClients; + server.resource["^/api/clients/unpair$"]["POST"] = unpair; server.resource["^/api/apps/close$"]["POST"] = closeApp; server.resource["^/api/covers/upload$"]["POST"] = uploadCover; - server.resource["^/images/favicon.ico$"]["GET"] = getFaviconImage; + server.resource["^/images/sunshine.ico$"]["GET"] = getFaviconImage; server.resource["^/images/logo-sunshine-45.png$"]["GET"] = getSunshineLogoImage; - server.resource["^/node_modules\\/.+$"]["GET"] = getNodeModules; + server.resource["^/assets\\/.+$"]["GET"] = getNodeModules; server.config.reuse_address = true; - server.config.address = "0.0.0.0"s; + server.config.address = net::af_to_any_address_string(address_family); server.config.port = port_https; auto accept_and_run = [&](auto *server) { diff --git a/src/crypto.cpp b/src/crypto.cpp index 5dec0f8dd57..9a5ef5a474e 100644 --- a/src/crypto.cpp +++ b/src/crypto.cpp @@ -17,6 +17,10 @@ namespace crypto { X509_STORE_add_cert(x509_store.get(), cert.get()); _certs.emplace_back(std::make_pair(std::move(cert), std::move(x509_store))); } + void + cert_chain_t::clear() { + _certs.clear(); + } static int openssl_verify_cb(int ok, X509_STORE_CTX *ctx) { @@ -152,10 +156,11 @@ namespace crypto { auto cipher = tagged_cipher.substr(tag_size); auto tag = tagged_cipher.substr(0, tag_size); - plaintext.resize((cipher.size() + 15) / 16 * 16); + plaintext.resize(round_to_pkcs7_padded(cipher.size())); + + int update_outlen, final_outlen; - int size; - if (EVP_DecryptUpdate(decrypt_ctx.get(), plaintext.data(), &size, (const std::uint8_t *) cipher.data(), cipher.size()) != 1) { + if (EVP_DecryptUpdate(decrypt_ctx.get(), plaintext.data(), &update_outlen, (const std::uint8_t *) cipher.data(), cipher.size()) != 1) { return -1; } @@ -163,12 +168,11 @@ namespace crypto { return -1; } - int len = size; - if (EVP_DecryptFinal_ex(decrypt_ctx.get(), plaintext.data() + size, &len) != 1) { + if (EVP_DecryptFinal_ex(decrypt_ctx.get(), plaintext.data() + update_outlen, &final_outlen) != 1) { return -1; } - plaintext.resize(size + len); + plaintext.resize(update_outlen + final_outlen); return 0; } @@ -187,16 +191,15 @@ namespace crypto { auto tag = tagged_cipher; auto cipher = tag + tag_size; - int len; - int size = round_to_pkcs7_padded(plaintext.size()); + int update_outlen, final_outlen; // Encrypt into the caller's buffer - if (EVP_EncryptUpdate(encrypt_ctx.get(), cipher, &size, (const std::uint8_t *) plaintext.data(), plaintext.size()) != 1) { + if (EVP_EncryptUpdate(encrypt_ctx.get(), cipher, &update_outlen, (const std::uint8_t *) plaintext.data(), plaintext.size()) != 1) { return -1; } // GCM encryption won't ever fill ciphertext here but we have to call it anyway - if (EVP_EncryptFinal_ex(encrypt_ctx.get(), cipher + size, &len) != 1) { + if (EVP_EncryptFinal_ex(encrypt_ctx.get(), cipher + update_outlen, &final_outlen) != 1) { return -1; } @@ -204,13 +207,11 @@ namespace crypto { return -1; } - return len + size; + return update_outlen + final_outlen; } int ecb_t::decrypt(const std::string_view &cipher, std::vector &plaintext) { - int len; - auto fg = util::fail_guard([this]() { EVP_CIPHER_CTX_reset(decrypt_ctx.get()); }); @@ -221,19 +222,19 @@ namespace crypto { } EVP_CIPHER_CTX_set_padding(decrypt_ctx.get(), padding); + plaintext.resize(round_to_pkcs7_padded(cipher.size())); + + int update_outlen, final_outlen; - plaintext.resize((cipher.size() + 15) / 16 * 16); - auto size = (int) plaintext.size(); - // Decrypt into the caller's buffer, leaving room for the auth tag to be prepended - if (EVP_DecryptUpdate(decrypt_ctx.get(), plaintext.data(), &size, (const std::uint8_t *) cipher.data(), cipher.size()) != 1) { + if (EVP_DecryptUpdate(decrypt_ctx.get(), plaintext.data(), &update_outlen, (const std::uint8_t *) cipher.data(), cipher.size()) != 1) { return -1; } - if (EVP_DecryptFinal_ex(decrypt_ctx.get(), plaintext.data(), &len) != 1) { + if (EVP_DecryptFinal_ex(decrypt_ctx.get(), plaintext.data() + update_outlen, &final_outlen) != 1) { return -1; } - plaintext.resize(len + size); + plaintext.resize(update_outlen + final_outlen); return 0; } @@ -249,22 +250,20 @@ namespace crypto { } EVP_CIPHER_CTX_set_padding(encrypt_ctx.get(), padding); + cipher.resize(round_to_pkcs7_padded(plaintext.size())); - int len; - - cipher.resize((plaintext.size() + 15) / 16 * 16); - auto size = (int) cipher.size(); + int update_outlen, final_outlen; // Encrypt into the caller's buffer - if (EVP_EncryptUpdate(encrypt_ctx.get(), cipher.data(), &size, (const std::uint8_t *) plaintext.data(), plaintext.size()) != 1) { + if (EVP_EncryptUpdate(encrypt_ctx.get(), cipher.data(), &update_outlen, (const std::uint8_t *) plaintext.data(), plaintext.size()) != 1) { return -1; } - if (EVP_EncryptFinal_ex(encrypt_ctx.get(), cipher.data() + size, &len) != 1) { + if (EVP_EncryptFinal_ex(encrypt_ctx.get(), cipher.data() + update_outlen, &final_outlen) != 1) { return -1; } - cipher.resize(len + size); + cipher.resize(update_outlen + final_outlen); return 0; } @@ -280,20 +279,18 @@ namespace crypto { return false; } - int len; - - int size = plaintext.size(); // round_to_pkcs7_padded(plaintext.size()); + int update_outlen, final_outlen; // Encrypt into the caller's buffer - if (EVP_EncryptUpdate(encrypt_ctx.get(), cipher, &size, (const std::uint8_t *) plaintext.data(), plaintext.size()) != 1) { + if (EVP_EncryptUpdate(encrypt_ctx.get(), cipher, &update_outlen, (const std::uint8_t *) plaintext.data(), plaintext.size()) != 1) { return -1; } - if (EVP_EncryptFinal_ex(encrypt_ctx.get(), cipher + size, &len) != 1) { + if (EVP_EncryptFinal_ex(encrypt_ctx.get(), cipher + update_outlen, &final_outlen) != 1) { return -1; } - return size + len; + return update_outlen + final_outlen; } ecb_t::ecb_t(const aes_t &key, bool padding): @@ -309,7 +306,7 @@ namespace crypto { aes_t gen_aes_key(const std::array &salt, const std::string_view &pin) { - aes_t key; + aes_t key(16); std::string salt_pin; salt_pin.reserve(salt.size() + pin.size()); @@ -409,11 +406,12 @@ namespace crypto { return {}; } - std::size_t slen = digest_size; - - std::vector digest; - digest.resize(slen); + std::size_t slen; + if (EVP_DigestSignFinal(ctx.get(), nullptr, &slen) != 1) { + return {}; + } + std::vector digest(slen); if (EVP_DigestSignFinal(ctx.get(), digest.data(), &slen) != 1) { return {}; } diff --git a/src/crypto.h b/src/crypto.h index d8d0a35a607..410d3c802a5 100644 --- a/src/crypto.h +++ b/src/crypto.h @@ -17,14 +17,13 @@ namespace crypto { std::string x509; std::string pkey; }; - constexpr std::size_t digest_size = 256; void md_ctx_destroy(EVP_MD_CTX *); using sha256_t = std::array; - using aes_t = std::array; + using aes_t = std::vector; using x509_t = util::safe_ptr; using x509_store_t = util::safe_ptr; using x509_store_ctx_t = util::safe_ptr; @@ -74,6 +73,9 @@ namespace crypto { void add(x509_t &&cert); + void + clear(); + const char * verify(x509_t::element_type *cert); diff --git a/src/entry_handler.cpp b/src/entry_handler.cpp new file mode 100644 index 00000000000..8d17b7d270a --- /dev/null +++ b/src/entry_handler.cpp @@ -0,0 +1,396 @@ +/** + * @file entry_handler.cpp + * @brief Entry point related functions. + */ + +// standard includes +#include +#include +#include + +// local includes +#include "config.h" +#include "confighttp.h" +#include "entry_handler.h" +#include "globals.h" +#include "httpcommon.h" +#include "logging.h" +#include "network.h" +#include "platform/common.h" +#include "version.h" + +extern "C" { +#ifdef _WIN32 + #include +#endif +} + +using namespace std::literals; + +/** + * @brief Launch the Web UI. + * + * EXAMPLES: + * ```cpp + * launch_ui(); + * ``` + */ +void +launch_ui() { + std::string url = "https://localhost:" + std::to_string(net::map_port(confighttp::PORT_HTTPS)); + platf::open_url(url); +} + +/** + * @brief Launch the Web UI at a specific endpoint. + * + * EXAMPLES: + * ```cpp + * launch_ui_with_path("/pin"); + * ``` + */ +void +launch_ui_with_path(std::string path) { + std::string url = "https://localhost:" + std::to_string(net::map_port(confighttp::PORT_HTTPS)) + path; + platf::open_url(url); +} + +namespace args { + /** + * @brief Reset the user credentials. + * + * @param name The name of the program. + * @param argc The number of arguments. + * @param argv The arguments. + * + * EXAMPLES: + * ```cpp + * creds("sunshine", 2, {"new_username", "new_password"}); + * ``` + */ + int + creds(const char *name, int argc, char *argv[]) { + if (argc < 2 || argv[0] == "help"sv || argv[1] == "help"sv) { + help(name, argc, argv); + } + + http::save_user_creds(config::sunshine.credentials_file, argv[0], argv[1]); + + return 0; + } + + /** + * @brief Print help to stdout, then exit. + * @param name The name of the program. + * @param argc The number of arguments. (Unused) + * @param argv The arguments. (Unused) + * + * EXAMPLES: + * ```cpp + * help("sunshine", 0, nullptr); + * ``` + */ + int + help(const char *name, int argc, char *argv[]) { + logging::print_help(name); + return 0; + } + + /** + * @brief Print the version to stdout, then exit. + * @param name The name of the program. (Unused) + * @param argc The number of arguments. (Unused) + * @param argv The arguments. (Unused) + * + * EXAMPLES: + * ```cpp + * version("sunshine", 0, nullptr); + * ``` + */ + int + version(const char *name, int argc, char *argv[]) { + // version was already logged at startup + return 0; + } + +#ifdef _WIN32 + /** + * @brief Restore global NVIDIA control panel settings. + * + * If Sunshine was improperly terminated, this function restores + * the global NVIDIA control panel settings to the undo file left + * by Sunshine. This function is typically called by the uninstaller. + * + * @param name The name of the program. (Unused) + * @param argc The number of arguments. (Unused) + * @param argv The arguments. (Unused) + * + * EXAMPLES: + * ```cpp + * restore_nvprefs_undo("sunshine", 0, nullptr); + * ``` + */ + int + restore_nvprefs_undo(const char *name, int argc, char *argv[]) { + if (nvprefs_instance.load()) { + nvprefs_instance.restore_from_and_delete_undo_file_if_exists(); + nvprefs_instance.unload(); + } + return 0; + } +#endif +} // namespace args + +namespace lifetime { + char **argv; + std::atomic_int desired_exit_code; + + /** + * @brief Terminates Sunshine gracefully with the provided exit code. + * @param exit_code The exit code to return from main(). + * @param async Specifies whether our termination will be non-blocking. + */ + void + exit_sunshine(int exit_code, bool async) { + // Store the exit code of the first exit_sunshine() call + int zero = 0; + desired_exit_code.compare_exchange_strong(zero, exit_code); + + // Raise SIGINT to start termination + std::raise(SIGINT); + + // Termination will happen asynchronously, but the caller may + // have wanted synchronous behavior. + while (!async) { + std::this_thread::sleep_for(1s); + } + } + + /** + * @brief Breaks into the debugger or terminates Sunshine if no debugger is attached. + */ + void + debug_trap() { +#ifdef _WIN32 + DebugBreak(); +#else + std::raise(SIGTRAP); +#endif + } + + /** + * @brief Gets the argv array passed to main(). + */ + char ** + get_argv() { + return argv; + } +} // namespace lifetime + +#ifdef _WIN32 +/** + * @brief Check if NVIDIA's GameStream software is running. + * @return `true` if GameStream is enabled, `false` otherwise. + */ +bool +is_gamestream_enabled() { + DWORD enabled; + DWORD size = sizeof(enabled); + return RegGetValueW( + HKEY_LOCAL_MACHINE, + L"SOFTWARE\\NVIDIA Corporation\\NvStream", + L"EnableStreaming", + RRF_RT_REG_DWORD, + nullptr, + &enabled, + &size) == ERROR_SUCCESS && + enabled != 0; +} + +namespace service_ctrl { + class service_controller { + public: + /** + * @brief Constructor for service_controller class. + * @param service_desired_access SERVICE_* desired access flags. + */ + service_controller(DWORD service_desired_access) { + scm_handle = OpenSCManagerA(nullptr, nullptr, SC_MANAGER_CONNECT); + if (!scm_handle) { + auto winerr = GetLastError(); + BOOST_LOG(error) << "OpenSCManager() failed: "sv << winerr; + return; + } + + service_handle = OpenServiceA(scm_handle, "SunshineService", service_desired_access); + if (!service_handle) { + auto winerr = GetLastError(); + BOOST_LOG(error) << "OpenService() failed: "sv << winerr; + return; + } + } + + ~service_controller() { + if (service_handle) { + CloseServiceHandle(service_handle); + } + + if (scm_handle) { + CloseServiceHandle(scm_handle); + } + } + + /** + * @brief Asynchronously starts the Sunshine service. + */ + bool + start_service() { + if (!service_handle) { + return false; + } + + if (!StartServiceA(service_handle, 0, nullptr)) { + auto winerr = GetLastError(); + if (winerr != ERROR_SERVICE_ALREADY_RUNNING) { + BOOST_LOG(error) << "StartService() failed: "sv << winerr; + return false; + } + } + + return true; + } + + /** + * @brief Query the service status. + * @param status The SERVICE_STATUS struct to populate. + */ + bool + query_service_status(SERVICE_STATUS &status) { + if (!service_handle) { + return false; + } + + if (!QueryServiceStatus(service_handle, &status)) { + auto winerr = GetLastError(); + BOOST_LOG(error) << "QueryServiceStatus() failed: "sv << winerr; + return false; + } + + return true; + } + + private: + SC_HANDLE scm_handle = NULL; + SC_HANDLE service_handle = NULL; + }; + + /** + * @brief Check if the service is running. + * + * EXAMPLES: + * ```cpp + * is_service_running(); + * ``` + */ + bool + is_service_running() { + service_controller sc { SERVICE_QUERY_STATUS }; + + SERVICE_STATUS status; + if (!sc.query_service_status(status)) { + return false; + } + + return status.dwCurrentState == SERVICE_RUNNING; + } + + /** + * @brief Start the service and wait for startup to complete. + * + * EXAMPLES: + * ```cpp + * start_service(); + * ``` + */ + bool + start_service() { + service_controller sc { SERVICE_QUERY_STATUS | SERVICE_START }; + + std::cout << "Starting Sunshine..."sv; + + // This operation is asynchronous, so we must wait for it to complete + if (!sc.start_service()) { + return false; + } + + SERVICE_STATUS status; + do { + Sleep(1000); + std::cout << '.'; + } while (sc.query_service_status(status) && status.dwCurrentState == SERVICE_START_PENDING); + + if (status.dwCurrentState != SERVICE_RUNNING) { + BOOST_LOG(error) << SERVICE_NAME " failed to start: "sv << status.dwWin32ExitCode; + return false; + } + + std::cout << std::endl; + return true; + } + + /** + * @brief Wait for the UI to be ready after Sunshine startup. + * + * EXAMPLES: + * ```cpp + * wait_for_ui_ready(); + * ``` + */ + bool + wait_for_ui_ready() { + std::cout << "Waiting for Web UI to be ready..."; + + // Wait up to 30 seconds for the web UI to start + for (int i = 0; i < 30; i++) { + PMIB_TCPTABLE tcp_table = nullptr; + ULONG table_size = 0; + ULONG err; + + auto fg = util::fail_guard([&tcp_table]() { + free(tcp_table); + }); + + do { + // Query all open TCP sockets to look for our web UI port + err = GetTcpTable(tcp_table, &table_size, false); + if (err == ERROR_INSUFFICIENT_BUFFER) { + free(tcp_table); + tcp_table = (PMIB_TCPTABLE) malloc(table_size); + } + } while (err == ERROR_INSUFFICIENT_BUFFER); + + if (err != NO_ERROR) { + BOOST_LOG(error) << "Failed to query TCP table: "sv << err; + return false; + } + + uint16_t port_nbo = htons(net::map_port(confighttp::PORT_HTTPS)); + for (DWORD i = 0; i < tcp_table->dwNumEntries; i++) { + auto &entry = tcp_table->table[i]; + + // Look for our port in the listening state + if (entry.dwLocalPort == port_nbo && entry.dwState == MIB_TCP_STATE_LISTEN) { + std::cout << std::endl; + return true; + } + } + + Sleep(1000); + std::cout << '.'; + } + + std::cout << "timed out"sv << std::endl; + return false; + } +} // namespace service_ctrl +#endif diff --git a/src/entry_handler.h b/src/entry_handler.h new file mode 100644 index 00000000000..bdab361cf0c --- /dev/null +++ b/src/entry_handler.h @@ -0,0 +1,62 @@ +/** + * @file entry_handler.h + * @brief Header file for entry point functions. + */ +#pragma once + +// standard includes +#include +#include + +// local includes +#include "thread_pool.h" +#include "thread_safe.h" + +// functions +void +launch_ui(); +void +launch_ui_with_path(std::string path); + +#ifdef _WIN32 +// windows only functions +bool +is_gamestream_enabled(); +#endif + +namespace args { + int + creds(const char *name, int argc, char *argv[]); + int + help(const char *name, int argc, char *argv[]); + int + version(const char *name, int argc, char *argv[]); +#ifdef _WIN32 + int + restore_nvprefs_undo(const char *name, int argc, char *argv[]); +#endif +} // namespace args + +namespace lifetime { + extern char **argv; + extern std::atomic_int desired_exit_code; + void + exit_sunshine(int exit_code, bool async); + void + debug_trap(); + char ** + get_argv(); +} // namespace lifetime + +#ifdef _WIN32 +namespace service_ctrl { + bool + is_service_running(); + + bool + start_service(); + + bool + wait_for_ui_ready(); +} // namespace service_ctrl +#endif diff --git a/src/file_handler.cpp b/src/file_handler.cpp new file mode 100644 index 00000000000..1c7f98e9b3b --- /dev/null +++ b/src/file_handler.cpp @@ -0,0 +1,60 @@ +/** + * @file file_handler.cpp + * @brief File handling functions. + */ + +// standard includes +#include +#include + +// local includes +#include "file_handler.h" +#include "logging.h" + +namespace file_handler { + + /** + * @brief Read a file to string. + * @param path The path of the file. + * @return `std::string` : The contents of the file. + * + * EXAMPLES: + * ```cpp + * std::string contents = read_file("path/to/file"); + * ``` + */ + std::string + read_file(const char *path) { + if (!std::filesystem::exists(path)) { + BOOST_LOG(debug) << "Missing file: " << path; + return {}; + } + + std::ifstream in(path); + return std::string { (std::istreambuf_iterator(in)), std::istreambuf_iterator() }; + } + + /** + * @brief Writes a file. + * @param path The path of the file. + * @param contents The contents to write. + * @return `int` : `0` on success, `-1` on failure. + * + * EXAMPLES: + * ```cpp + * int write_status = write_file("path/to/file", "file contents"); + * ``` + */ + int + write_file(const char *path, const std::string_view &contents) { + std::ofstream out(path); + + if (!out.is_open()) { + return -1; + } + + out << contents; + + return 0; + } +} // namespace file_handler diff --git a/src/file_handler.h b/src/file_handler.h new file mode 100644 index 00000000000..aa2387f8469 --- /dev/null +++ b/src/file_handler.h @@ -0,0 +1,14 @@ +/** + * @file file_handler.h + * @brief Header file for file handling functions. + */ +#pragma once + +#include + +namespace file_handler { + std::string + read_file(const char *path); + int + write_file(const char *path, const std::string_view &contents); +} // namespace file_handler diff --git a/src/globals.cpp b/src/globals.cpp new file mode 100644 index 00000000000..ae6c7544360 --- /dev/null +++ b/src/globals.cpp @@ -0,0 +1,27 @@ +/** + * @file globals.cpp + * @brief Implementation for globally accessible variables and functions. + */ +#include "globals.h" + +/** + * @brief A process-wide communication mechanism. + */ +safe::mail_t mail::man; + +/** + * @brief A thread pool for processing tasks. + */ +thread_pool_util::ThreadPool task_pool; + +/** + * @brief A boolean flag to indicate whether the cursor should be displayed. + */ +bool display_cursor = true; + +#ifdef _WIN32 +/** + * @brief A global singleton used for NVIDIA control panel modifications. + */ +nvprefs::nvprefs_interface nvprefs_instance; +#endif diff --git a/src/globals.h b/src/globals.h new file mode 100644 index 00000000000..a137bc9c4d5 --- /dev/null +++ b/src/globals.h @@ -0,0 +1,42 @@ +/** + * @file globals.h + * @brief Header for globally accessible variables and functions. + */ +#pragma once + +#include "entry_handler.h" +#include "thread_pool.h" + +extern thread_pool_util::ThreadPool task_pool; +extern bool display_cursor; + +#ifdef _WIN32 + // Declare global singleton used for NVIDIA control panel modifications + #include "platform/windows/nvprefs/nvprefs_interface.h" +extern nvprefs::nvprefs_interface nvprefs_instance; +#endif + +namespace mail { +#define MAIL(x) \ + constexpr auto x = std::string_view { \ + #x \ + } + + extern safe::mail_t man; + + // Global mail + MAIL(shutdown); + MAIL(broadcast_shutdown); + MAIL(video_packets); + MAIL(audio_packets); + MAIL(switch_display); + + // Local mail + MAIL(touch_port); + MAIL(idr); + MAIL(invalidate_ref_frames); + MAIL(gamepad_feedback); + MAIL(hdr); +#undef MAIL + +} // namespace mail diff --git a/src/httpcommon.cpp b/src/httpcommon.cpp index 2cfbfe0dd42..123e7e9393c 100644 --- a/src/httpcommon.cpp +++ b/src/httpcommon.cpp @@ -7,6 +7,7 @@ #include "process.h" #include +#include #include #include @@ -21,8 +22,9 @@ #include "config.h" #include "crypto.h" +#include "file_handler.h" #include "httpcommon.h" -#include "main.h" +#include "logging.h" #include "network.h" #include "nvhttp.h" #include "platform/common.h" @@ -41,13 +43,11 @@ namespace http { user_creds_exist(const std::string &file); std::string unique_id; - net::net_e origin_pin_allowed; net::net_e origin_web_ui_allowed; int init() { bool clean_slate = config::sunshine.flags[config::flag::FRESH_STATE]; - origin_pin_allowed = net::from_enum_string(config::nvhttp.origin_pin_allowed); origin_web_ui_allowed = net::from_enum_string(config::nvhttp.origin_web_ui_allowed); if (clean_slate) { @@ -162,12 +162,12 @@ namespace http { return -1; } - if (write_file(pkey.c_str(), creds.pkey)) { + if (file_handler::write_file(pkey.c_str(), creds.pkey)) { BOOST_LOG(error) << "Couldn't open ["sv << config::nvhttp.pkey << ']'; return -1; } - if (write_file(cert.c_str(), creds.x509)) { + if (file_handler::write_file(cert.c_str(), creds.x509)) { BOOST_LOG(error) << "Couldn't open ["sv << config::nvhttp.cert << ']'; return -1; } diff --git a/src/httpcommon.h b/src/httpcommon.h index 02d42d265fa..9dc8f9b2d11 100644 --- a/src/httpcommon.h +++ b/src/httpcommon.h @@ -30,7 +30,6 @@ namespace http { url_get_host(const std::string &url); extern std::string unique_id; - extern net::net_e origin_pin_allowed; extern net::net_e origin_web_ui_allowed; } // namespace http diff --git a/src/input.cpp b/src/input.cpp index a9a38e2d91c..2e26d5b00a8 100644 --- a/src/input.cpp +++ b/src/input.cpp @@ -10,17 +10,26 @@ extern "C" { } #include +#include +#include +#include +#include #include #include "config.h" +#include "globals.h" #include "input.h" -#include "main.h" +#include "logging.h" #include "platform/common.h" #include "thread_pool.h" #include "utility.h" -#include -#include +#include + +// Win32 WHEEL_DELTA constant +#ifndef WHEEL_DELTA + #define WHEEL_DELTA 120 +#endif using namespace std::literals; namespace input { @@ -78,6 +87,28 @@ namespace input { return kpid & 0xFF; } + /** + * @brief Converts a little-endian netfloat to a native endianness float. + * @param f Netfloat value. + * @return Float value. + */ + float + from_netfloat(netfloat f) { + return boost::endian::endian_load(f); + } + + /** + * @brief Converts a little-endian netfloat to a native endianness float and clamps it. + * @param f Netfloat value. + * @param min The minimium value for clamping. + * @param max The maximum value for clamping. + * @return Clamped float value. + */ + float + from_clamped_netfloat(netfloat f, float min, float max) { + return std::clamp(from_netfloat(f), min, max); + } + static task_pool_util::TaskPool::task_id_t key_press_repeat_id {}; static std::unordered_map key_press {}; static std::array mouse_press {}; @@ -128,27 +159,35 @@ namespace input { input_t( safe::mail_raw_t::event_t touch_port_event, - platf::rumble_queue_t rumble_queue): + platf::feedback_queue_t feedback_queue): shortcutFlags {}, - active_gamepad_state {}, gamepads(MAX_GAMEPADS), + client_context { platf::allocate_client_input_context(platf_input) }, touch_port_event { std::move(touch_port_event) }, - rumble_queue { std::move(rumble_queue) }, + feedback_queue { std::move(feedback_queue) }, mouse_left_button_timeout {}, - touch_port { { 0, 0, 0, 0 }, 0, 0, 1.0f } {} + touch_port { { 0, 0, 0, 0 }, 0, 0, 1.0f }, + accumulated_vscroll_delta {}, + accumulated_hscroll_delta {} {} // Keep track of alt+ctrl+shift key combo int shortcutFlags; - std::uint16_t active_gamepad_state; std::vector gamepads; + std::unique_ptr client_context; safe::mail_raw_t::event_t touch_port_event; - platf::rumble_queue_t rumble_queue; + platf::feedback_queue_t feedback_queue; + + std::list> input_queue; + std::mutex input_queue_lock; thread_pool_util::ThreadPool::task_id_t mouse_left_button_timeout; input::touch_port_t touch_port; + + int32_t accumulated_vscroll_delta; + int32_t accumulated_hscroll_delta; }; /** @@ -251,7 +290,7 @@ namespace input { << "--begin controller packet--"sv << std::endl << "controllerNumber ["sv << packet->controllerNumber << ']' << std::endl << "activeGamepadMask ["sv << util::hex(packet->activeGamepadMask).to_string_view() << ']' << std::endl - << "buttonFlags ["sv << util::hex(packet->buttonFlags).to_string_view() << ']' << std::endl + << "buttonFlags ["sv << util::hex((uint32_t) packet->buttonFlags | (packet->buttonFlags2 << 16)).to_string_view() << ']' << std::endl << "leftTrigger ["sv << util::hex(packet->leftTrigger).to_string_view() << ']' << std::endl << "rightTrigger ["sv << util::hex(packet->rightTrigger).to_string_view() << ']' << std::endl << "leftStickX ["sv << packet->leftStickX << ']' << std::endl @@ -261,6 +300,108 @@ namespace input { << "--end controller packet--"sv; } + /** + * @brief Prints a touch packet. + * @param packet The touch packet. + */ + void + print(PSS_TOUCH_PACKET packet) { + BOOST_LOG(debug) + << "--begin touch packet--"sv << std::endl + << "eventType ["sv << util::hex(packet->eventType).to_string_view() << ']' << std::endl + << "pointerId ["sv << util::hex(packet->pointerId).to_string_view() << ']' << std::endl + << "x ["sv << from_netfloat(packet->x) << ']' << std::endl + << "y ["sv << from_netfloat(packet->y) << ']' << std::endl + << "pressureOrDistance ["sv << from_netfloat(packet->pressureOrDistance) << ']' << std::endl + << "contactAreaMajor ["sv << from_netfloat(packet->contactAreaMajor) << ']' << std::endl + << "contactAreaMinor ["sv << from_netfloat(packet->contactAreaMinor) << ']' << std::endl + << "rotation ["sv << (uint32_t) packet->rotation << ']' << std::endl + << "--end touch packet--"sv; + } + + /** + * @brief Prints a pen packet. + * @param packet The pen packet. + */ + void + print(PSS_PEN_PACKET packet) { + BOOST_LOG(debug) + << "--begin pen packet--"sv << std::endl + << "eventType ["sv << util::hex(packet->eventType).to_string_view() << ']' << std::endl + << "toolType ["sv << util::hex(packet->toolType).to_string_view() << ']' << std::endl + << "penButtons ["sv << util::hex(packet->penButtons).to_string_view() << ']' << std::endl + << "x ["sv << from_netfloat(packet->x) << ']' << std::endl + << "y ["sv << from_netfloat(packet->y) << ']' << std::endl + << "pressureOrDistance ["sv << from_netfloat(packet->pressureOrDistance) << ']' << std::endl + << "contactAreaMajor ["sv << from_netfloat(packet->contactAreaMajor) << ']' << std::endl + << "contactAreaMinor ["sv << from_netfloat(packet->contactAreaMinor) << ']' << std::endl + << "rotation ["sv << (uint32_t) packet->rotation << ']' << std::endl + << "tilt ["sv << (uint32_t) packet->tilt << ']' << std::endl + << "--end pen packet--"sv; + } + + /** + * @brief Prints a controller arrival packet. + * @param packet The controller arrival packet. + */ + void + print(PSS_CONTROLLER_ARRIVAL_PACKET packet) { + BOOST_LOG(debug) + << "--begin controller arrival packet--"sv << std::endl + << "controllerNumber ["sv << (uint32_t) packet->controllerNumber << ']' << std::endl + << "type ["sv << util::hex(packet->type).to_string_view() << ']' << std::endl + << "capabilities ["sv << util::hex(packet->capabilities).to_string_view() << ']' << std::endl + << "supportedButtonFlags ["sv << util::hex(packet->supportedButtonFlags).to_string_view() << ']' << std::endl + << "--end controller arrival packet--"sv; + } + + /** + * @brief Prints a controller touch packet. + * @param packet The controller touch packet. + */ + void + print(PSS_CONTROLLER_TOUCH_PACKET packet) { + BOOST_LOG(debug) + << "--begin controller touch packet--"sv << std::endl + << "controllerNumber ["sv << (uint32_t) packet->controllerNumber << ']' << std::endl + << "eventType ["sv << util::hex(packet->eventType).to_string_view() << ']' << std::endl + << "pointerId ["sv << util::hex(packet->pointerId).to_string_view() << ']' << std::endl + << "x ["sv << from_netfloat(packet->x) << ']' << std::endl + << "y ["sv << from_netfloat(packet->y) << ']' << std::endl + << "pressure ["sv << from_netfloat(packet->pressure) << ']' << std::endl + << "--end controller touch packet--"sv; + } + + /** + * @brief Prints a controller motion packet. + * @param packet The controller motion packet. + */ + void + print(PSS_CONTROLLER_MOTION_PACKET packet) { + BOOST_LOG(verbose) + << "--begin controller motion packet--"sv << std::endl + << "controllerNumber ["sv << util::hex(packet->controllerNumber).to_string_view() << ']' << std::endl + << "motionType ["sv << util::hex(packet->motionType).to_string_view() << ']' << std::endl + << "x ["sv << from_netfloat(packet->x) << ']' << std::endl + << "y ["sv << from_netfloat(packet->y) << ']' << std::endl + << "z ["sv << from_netfloat(packet->z) << ']' << std::endl + << "--end controller motion packet--"sv; + } + + /** + * @brief Prints a controller battery packet. + * @param packet The controller battery packet. + */ + void + print(PSS_CONTROLLER_BATTERY_PACKET packet) { + BOOST_LOG(verbose) + << "--begin controller battery packet--"sv << std::endl + << "controllerNumber ["sv << util::hex(packet->controllerNumber).to_string_view() << ']' << std::endl + << "batteryState ["sv << util::hex(packet->batteryState).to_string_view() << ']' << std::endl + << "batteryPercentage ["sv << util::hex(packet->batteryPercentage).to_string_view() << ']' << std::endl + << "--end controller battery packet--"sv; + } + void print(void *payload) { auto header = (PNV_INPUT_HEADER) payload; @@ -292,6 +433,24 @@ namespace input { case MULTI_CONTROLLER_MAGIC_GEN5: print((PNV_MULTI_CONTROLLER_PACKET) payload); break; + case SS_TOUCH_MAGIC: + print((PSS_TOUCH_PACKET) payload); + break; + case SS_PEN_MAGIC: + print((PSS_PEN_PACKET) payload); + break; + case SS_CONTROLLER_ARRIVAL_MAGIC: + print((PSS_CONTROLLER_ARRIVAL_PACKET) payload); + break; + case SS_CONTROLLER_TOUCH_MAGIC: + print((PSS_CONTROLLER_TOUCH_PACKET) payload); + break; + case SS_CONTROLLER_MOTION_MAGIC: + print((PSS_CONTROLLER_MOTION_PACKET) payload); + break; + case SS_CONTROLLER_BATTERY_MAGIC: + print((PSS_CONTROLLER_BATTERY_PACKET) payload); + break; } } @@ -305,6 +464,82 @@ namespace input { platf::move_mouse(platf_input, util::endian::big(packet->deltaX), util::endian::big(packet->deltaY)); } + /** + * @brief Converts client coordinates on the specified surface into screen coordinates. + * @param input The input context. + * @param val The cartesian coordinate pair to convert. + * @param size The size of the client's surface containing the value. + * @return The host-relative coordinate pair if a touchport is available. + */ + std::optional> + client_to_touchport(std::shared_ptr &input, const std::pair &val, const std::pair &size) { + auto &touch_port_event = input->touch_port_event; + auto &touch_port = input->touch_port; + if (touch_port_event->peek()) { + touch_port = *touch_port_event->pop(); + } + if (!touch_port) { + BOOST_LOG(verbose) << "Ignoring early absolute input without a touch port"sv; + return std::nullopt; + } + + auto scalarX = touch_port.width / size.first; + auto scalarY = touch_port.height / size.second; + + float x = std::clamp(val.first, 0.0f, size.first) * scalarX; + float y = std::clamp(val.second, 0.0f, size.second) * scalarY; + + auto offsetX = touch_port.client_offsetX; + auto offsetY = touch_port.client_offsetY; + + x = std::clamp(x, offsetX, (size.first * scalarX) - offsetX); + y = std::clamp(y, offsetY, (size.second * scalarY) - offsetY); + + return std::pair { (x - offsetX) * touch_port.scalar_inv, (y - offsetY) * touch_port.scalar_inv }; + } + + /** + * @brief Multiplies a polar coordinate pair by a cartesian scaling factor. + * @param r The radial coordinate. + * @param angle The angular coordinate (radians). + * @param scalar The scalar cartesian coordinate pair. + * @return The scaled radial coordinate. + */ + float + multiply_polar_by_cartesian_scalar(float r, float angle, const std::pair &scalar) { + // Convert polar to cartesian coordinates + float x = r * std::cos(angle); + float y = r * std::sin(angle); + + // Scale the values + x *= scalar.first; + y *= scalar.second; + + // Convert the result back to a polar radial coordinate + return std::sqrt(std::pow(x, 2) + std::pow(y, 2)); + } + + /** + * @brief Scales the ellipse axes according to the provided size. + * @param val The major and minor axis pair. + * @param rotation The rotation value from the touch/pen event. + * @param scalar The scalar cartesian coordinate pair. + * @return The major and minor axis pair. + */ + std::pair + scale_client_contact_area(const std::pair &val, uint16_t rotation, const std::pair &scalar) { + // If the rotation is unknown, we'll just scale both axes equally by using + // a 45 degree angle for our scaling calculations + float angle = rotation == LI_ROT_UNKNOWN ? (M_PI / 4) : (rotation * (M_PI / 180)); + + // If we have a major but not a minor axis, treat the touch as circular + float major = val.first; + float minor = val.second != 0.0f ? val.second : val.first; + + // The minor axis is perpendicular to major axis so the angle must be rotated by 90 degrees + return { multiply_polar_by_cartesian_scalar(major, angle, scalar), multiply_polar_by_cartesian_scalar(minor, angle + (M_PI / 2), scalar) }; + } + void passthrough(std::shared_ptr &input, PNV_ABS_MOUSE_MOVE_PACKET packet) { if (!config::input.mouse) { @@ -315,12 +550,6 @@ namespace input { input->mouse_left_button_timeout = ENABLE_LEFT_BUTTON_DELAY; } - auto &touch_port_event = input->touch_port_event; - auto &touch_port = input->touch_port; - if (touch_port_event->peek()) { - touch_port = *touch_port_event->pop(); - } - float x = util::endian::big(packet->x); float y = util::endian::big(packet->y); @@ -335,24 +564,18 @@ namespace input { auto width = (float) util::endian::big(packet->width); auto height = (float) util::endian::big(packet->height); - auto scalarX = touch_port.width / width; - auto scalarY = touch_port.height / height; - - x *= scalarX; - y *= scalarY; - - auto offsetX = touch_port.client_offsetX; - auto offsetY = touch_port.client_offsetY; - - std::clamp(x, offsetX, width - offsetX); - std::clamp(y, offsetY, height - offsetY); + auto tpcoords = client_to_touchport(input, { x, y }, { width, height }); + if (!tpcoords) { + return; + } + auto &touch_port = input->touch_port; platf::touch_port_t abs_port { touch_port.offset_x, touch_port.offset_y, touch_port.env_width, touch_port.env_height }; - platf::abs_mouse(platf_input, abs_port, (x - offsetX) * touch_port.scalar_inv, (y - offsetY) * touch_port.scalar_inv); + platf::abs_mouse(platf_input, abs_port, tpcoords->first, tpcoords->second); } void @@ -585,22 +808,54 @@ namespace input { update_shortcutFlags(&input->shortcutFlags, map_keycode(keyCode), release); } + /** + * @brief Called to pass a vertical scroll message the platform backend. + * @param input The input context pointer. + * @param packet The scroll packet. + */ void - passthrough(PNV_SCROLL_PACKET packet) { + passthrough(std::shared_ptr &input, PNV_SCROLL_PACKET packet) { if (!config::input.mouse) { return; } - platf::scroll(platf_input, util::endian::big(packet->scrollAmt1)); + if (config::input.high_resolution_scrolling) { + platf::scroll(platf_input, util::endian::big(packet->scrollAmt1)); + } + else { + input->accumulated_vscroll_delta += util::endian::big(packet->scrollAmt1); + auto full_ticks = input->accumulated_vscroll_delta / WHEEL_DELTA; + if (full_ticks) { + // Send any full ticks that have accumulated and store the rest + platf::scroll(platf_input, full_ticks * WHEEL_DELTA); + input->accumulated_vscroll_delta -= full_ticks * WHEEL_DELTA; + } + } } + /** + * @brief Called to pass a horizontal scroll message the platform backend. + * @param input The input context pointer. + * @param packet The scroll packet. + */ void - passthrough(PSS_HSCROLL_PACKET packet) { + passthrough(std::shared_ptr &input, PSS_HSCROLL_PACKET packet) { if (!config::input.mouse) { return; } - platf::hscroll(platf_input, util::endian::big(packet->scrollAmount)); + if (config::input.high_resolution_scrolling) { + platf::hscroll(platf_input, util::endian::big(packet->scrollAmount)); + } + else { + input->accumulated_hscroll_delta += util::endian::big(packet->scrollAmount); + auto full_ticks = input->accumulated_hscroll_delta / WHEEL_DELTA; + if (full_ticks) { + // Send any full ticks that have accumulated and store the rest + platf::hscroll(platf_input, full_ticks * WHEEL_DELTA); + input->accumulated_hscroll_delta -= full_ticks * WHEEL_DELTA; + } + } } void @@ -613,83 +868,308 @@ namespace input { platf::unicode(platf_input, packet->text, size); } - int - updateGamepads(std::vector &gamepads, std::int16_t old_state, std::int16_t new_state, const platf::rumble_queue_t &rumble_queue) { - auto xorGamepadMask = old_state ^ new_state; - if (!xorGamepadMask) { - return 0; + /** + * @brief Called to pass a controller arrival message to the platform backend. + * @param input The input context pointer. + * @param packet The controller arrival packet. + */ + void + passthrough(std::shared_ptr &input, PSS_CONTROLLER_ARRIVAL_PACKET packet) { + if (!config::input.controller) { + return; } - for (int x = 0; x < sizeof(std::int16_t) * 8; ++x) { - if ((xorGamepadMask >> x) & 1) { - auto &gamepad = gamepads[x]; + if (packet->controllerNumber < 0 || packet->controllerNumber >= input->gamepads.size()) { + BOOST_LOG(warning) << "ControllerNumber out of range ["sv << packet->controllerNumber << ']'; + return; + } - if ((old_state >> x) & 1) { - if (gamepad.id < 0) { - return -1; - } + if (input->gamepads[packet->controllerNumber].id >= 0) { + BOOST_LOG(warning) << "ControllerNumber already allocated ["sv << packet->controllerNumber << ']'; + return; + } - free_gamepad(platf_input, gamepad.id); - gamepad.id = -1; - } - else { - auto id = alloc_id(gamepadMask); + platf::gamepad_arrival_t arrival { + packet->type, + util::endian::little(packet->capabilities), + util::endian::little(packet->supportedButtonFlags), + }; - if (id < 0) { - // Out of gamepads - return -1; - } + auto id = alloc_id(gamepadMask); + if (id < 0) { + return; + } - if (platf::alloc_gamepad(platf_input, id, rumble_queue)) { - free_id(gamepadMask, id); - // allocating a gamepad failed: solution: ignore gamepads - // The implementations of platf::alloc_gamepad already has logging - return -1; - } + // Allocate a new gamepad + if (platf::alloc_gamepad(platf_input, { id, packet->controllerNumber }, arrival, input->feedback_queue)) { + free_id(gamepadMask, id); + return; + } - gamepad.id = id; - } - } + input->gamepads[packet->controllerNumber].id = id; + } + + /** + * @brief Called to pass a touch message to the platform backend. + * @param input The input context pointer. + * @param packet The touch packet. + */ + void + passthrough(std::shared_ptr &input, PSS_TOUCH_PACKET packet) { + if (!config::input.mouse) { + return; } - return 0; + // Convert the client normalized coordinates to touchport coordinates + auto coords = client_to_touchport(input, + { from_clamped_netfloat(packet->x, 0.0f, 1.0f) * 65535.f, + from_clamped_netfloat(packet->y, 0.0f, 1.0f) * 65535.f }, + { 65535.f, 65535.f }); + if (!coords) { + return; + } + + auto &touch_port = input->touch_port; + platf::touch_port_t abs_port { + touch_port.offset_x, touch_port.offset_y, + touch_port.env_width, touch_port.env_height + }; + + // Renormalize the coordinates + coords->first /= abs_port.width; + coords->second /= abs_port.height; + + // Normalize rotation value to 0-359 degree range + auto rotation = util::endian::little(packet->rotation); + if (rotation != LI_ROT_UNKNOWN) { + rotation %= 360; + } + + // Normalize the contact area based on the touchport + auto contact_area = scale_client_contact_area( + { from_clamped_netfloat(packet->contactAreaMajor, 0.0f, 1.0f) * 65535.f, + from_clamped_netfloat(packet->contactAreaMinor, 0.0f, 1.0f) * 65535.f }, + rotation, + { abs_port.width / 65535.f, abs_port.height / 65535.f }); + + platf::touch_input_t touch { + packet->eventType, + rotation, + util::endian::little(packet->pointerId), + coords->first, + coords->second, + from_clamped_netfloat(packet->pressureOrDistance, 0.0f, 1.0f), + contact_area.first, + contact_area.second, + }; + + platf::touch(input->client_context.get(), abs_port, touch); } + /** + * @brief Called to pass a pen message to the platform backend. + * @param input The input context pointer. + * @param packet The pen packet. + */ void - passthrough(std::shared_ptr &input, PNV_MULTI_CONTROLLER_PACKET packet) { + passthrough(std::shared_ptr &input, PSS_PEN_PACKET packet) { + if (!config::input.mouse) { + return; + } + + // Convert the client normalized coordinates to touchport coordinates + auto coords = client_to_touchport(input, + { from_clamped_netfloat(packet->x, 0.0f, 1.0f) * 65535.f, + from_clamped_netfloat(packet->y, 0.0f, 1.0f) * 65535.f }, + { 65535.f, 65535.f }); + if (!coords) { + return; + } + + auto &touch_port = input->touch_port; + platf::touch_port_t abs_port { + touch_port.offset_x, touch_port.offset_y, + touch_port.env_width, touch_port.env_height + }; + + // Renormalize the coordinates + coords->first /= abs_port.width; + coords->second /= abs_port.height; + + // Normalize rotation value to 0-359 degree range + auto rotation = util::endian::little(packet->rotation); + if (rotation != LI_ROT_UNKNOWN) { + rotation %= 360; + } + + // Normalize the contact area based on the touchport + auto contact_area = scale_client_contact_area( + { from_clamped_netfloat(packet->contactAreaMajor, 0.0f, 1.0f) * 65535.f, + from_clamped_netfloat(packet->contactAreaMinor, 0.0f, 1.0f) * 65535.f }, + rotation, + { abs_port.width / 65535.f, abs_port.height / 65535.f }); + + platf::pen_input_t pen { + packet->eventType, + packet->toolType, + packet->penButtons, + packet->tilt, + rotation, + coords->first, + coords->second, + from_clamped_netfloat(packet->pressureOrDistance, 0.0f, 1.0f), + contact_area.first, + contact_area.second, + }; + + platf::pen(input->client_context.get(), abs_port, pen); + } + + /** + * @brief Called to pass a controller touch message to the platform backend. + * @param input The input context pointer. + * @param packet The controller touch packet. + */ + void + passthrough(std::shared_ptr &input, PSS_CONTROLLER_TOUCH_PACKET packet) { if (!config::input.controller) { return; } - if (updateGamepads(input->gamepads, input->active_gamepad_state, packet->activeGamepadMask, input->rumble_queue)) { + if (packet->controllerNumber < 0 || packet->controllerNumber >= input->gamepads.size()) { + BOOST_LOG(warning) << "ControllerNumber out of range ["sv << packet->controllerNumber << ']'; return; } - input->active_gamepad_state = packet->activeGamepadMask; + auto &gamepad = input->gamepads[packet->controllerNumber]; + if (gamepad.id < 0) { + BOOST_LOG(warning) << "ControllerNumber ["sv << packet->controllerNumber << "] not allocated"sv; + return; + } + + platf::gamepad_touch_t touch { + { gamepad.id, packet->controllerNumber }, + packet->eventType, + util::endian::little(packet->pointerId), + from_clamped_netfloat(packet->x, 0.0f, 1.0f), + from_clamped_netfloat(packet->y, 0.0f, 1.0f), + from_clamped_netfloat(packet->pressure, 0.0f, 1.0f), + }; + + platf::gamepad_touch(platf_input, touch); + } + + /** + * @brief Called to pass a controller motion message to the platform backend. + * @param input The input context pointer. + * @param packet The controller motion packet. + */ + void + passthrough(std::shared_ptr &input, PSS_CONTROLLER_MOTION_PACKET packet) { + if (!config::input.controller) { + return; + } if (packet->controllerNumber < 0 || packet->controllerNumber >= input->gamepads.size()) { BOOST_LOG(warning) << "ControllerNumber out of range ["sv << packet->controllerNumber << ']'; + return; + } + auto &gamepad = input->gamepads[packet->controllerNumber]; + if (gamepad.id < 0) { + BOOST_LOG(warning) << "ControllerNumber ["sv << packet->controllerNumber << "] not allocated"sv; return; } - if (!((input->active_gamepad_state >> packet->controllerNumber) & 1)) { + platf::gamepad_motion_t motion { + { gamepad.id, packet->controllerNumber }, + packet->motionType, + from_netfloat(packet->x), + from_netfloat(packet->y), + from_netfloat(packet->z), + }; + + platf::gamepad_motion(platf_input, motion); + } + + /** + * @brief Called to pass a controller battery message to the platform backend. + * @param input The input context pointer. + * @param packet The controller battery packet. + */ + void + passthrough(std::shared_ptr &input, PSS_CONTROLLER_BATTERY_PACKET packet) { + if (!config::input.controller) { + return; + } + + if (packet->controllerNumber < 0 || packet->controllerNumber >= input->gamepads.size()) { + BOOST_LOG(warning) << "ControllerNumber out of range ["sv << packet->controllerNumber << ']'; + return; + } + + auto &gamepad = input->gamepads[packet->controllerNumber]; + if (gamepad.id < 0) { BOOST_LOG(warning) << "ControllerNumber ["sv << packet->controllerNumber << "] not allocated"sv; + return; + } + + platf::gamepad_battery_t battery { + { gamepad.id, packet->controllerNumber }, + packet->batteryState, + packet->batteryPercentage + }; + + platf::gamepad_battery(platf_input, battery); + } + + void + passthrough(std::shared_ptr &input, PNV_MULTI_CONTROLLER_PACKET packet) { + if (!config::input.controller) { + return; + } + + if (packet->controllerNumber < 0 || packet->controllerNumber >= input->gamepads.size()) { + BOOST_LOG(warning) << "ControllerNumber out of range ["sv << packet->controllerNumber << ']'; return; } auto &gamepad = input->gamepads[packet->controllerNumber]; + // If this is an event for a new gamepad, create the gamepad now. Ideally, the client would + // send a controller arrival instead of this but it's still supported for legacy clients. + if ((packet->activeGamepadMask & (1 << packet->controllerNumber)) && gamepad.id < 0) { + auto id = alloc_id(gamepadMask); + if (id < 0) { + return; + } + + if (platf::alloc_gamepad(platf_input, { id, (uint8_t) packet->controllerNumber }, {}, input->feedback_queue)) { + free_id(gamepadMask, id); + return; + } + + gamepad.id = id; + } + else if (!(packet->activeGamepadMask & (1 << packet->controllerNumber)) && gamepad.id >= 0) { + // If this is the final event for a gamepad being removed, free the gamepad and return. + free_gamepad(platf_input, gamepad.id); + gamepad.id = -1; + return; + } + // If this gamepad has not been initialized, ignore it. // This could happen when platf::alloc_gamepad fails if (gamepad.id < 0) { + BOOST_LOG(warning) << "ControllerNumber ["sv << packet->controllerNumber << "] not allocated"sv; return; } std::uint16_t bf = packet->buttonFlags; + std::uint32_t bf2 = packet->buttonFlags2; platf::gamepad_state_t gamepad_state { - bf, + bf | (bf2 << 16), packet->leftTrigger, packet->rightTrigger, packet->leftStickX, @@ -738,7 +1218,7 @@ namespace input { platf::gamepad(platf_input, gamepad.id, state); // Sleep for a short time to allow the input to be detected - boost::this_thread::sleep_for(boost::chrono::milliseconds(100)); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); // Release Home button state.buttonFlags &= ~platf::HOME; @@ -761,12 +1241,350 @@ namespace input { gamepad.gamepad_state = gamepad_state; } + enum class batch_result_e { + batched, // This entry was batched with the source entry + not_batchable, // Not eligible to batch but continue attempts to batch + terminate_batch, // Stop trying to batch with this entry + }; + + /** + * @brief Batch two relative mouse messages. + * @param dest The original packet to batch into. + * @param src A later packet to attempt to batch. + * @return `batch_result_e` : The status of the batching operation. + */ + batch_result_e + batch(PNV_REL_MOUSE_MOVE_PACKET dest, PNV_REL_MOUSE_MOVE_PACKET src) { + short deltaX, deltaY; + + // Batching is safe as long as the result doesn't overflow a 16-bit integer + if (!__builtin_add_overflow(util::endian::big(dest->deltaX), util::endian::big(src->deltaX), &deltaX)) { + return batch_result_e::terminate_batch; + } + if (!__builtin_add_overflow(util::endian::big(dest->deltaY), util::endian::big(src->deltaY), &deltaY)) { + return batch_result_e::terminate_batch; + } + + // Take the sum of deltas + dest->deltaX = util::endian::big(deltaX); + dest->deltaY = util::endian::big(deltaY); + return batch_result_e::batched; + } + + /** + * @brief Batch two absolute mouse messages. + * @param dest The original packet to batch into. + * @param src A later packet to attempt to batch. + * @return `batch_result_e` : The status of the batching operation. + */ + batch_result_e + batch(PNV_ABS_MOUSE_MOVE_PACKET dest, PNV_ABS_MOUSE_MOVE_PACKET src) { + // Batching must only happen if the reference width and height don't change + if (dest->width != src->width || dest->height != src->height) { + return batch_result_e::terminate_batch; + } + + // Take the latest absolute position + *dest = *src; + return batch_result_e::batched; + } + + /** + * @brief Batch two vertical scroll messages. + * @param dest The original packet to batch into. + * @param src A later packet to attempt to batch. + * @return `batch_result_e` : The status of the batching operation. + */ + batch_result_e + batch(PNV_SCROLL_PACKET dest, PNV_SCROLL_PACKET src) { + short scrollAmt; + + // Batching is safe as long as the result doesn't overflow a 16-bit integer + if (!__builtin_add_overflow(util::endian::big(dest->scrollAmt1), util::endian::big(src->scrollAmt1), &scrollAmt)) { + return batch_result_e::terminate_batch; + } + + // Take the sum of delta + dest->scrollAmt1 = util::endian::big(scrollAmt); + dest->scrollAmt2 = util::endian::big(scrollAmt); + return batch_result_e::batched; + } + + /** + * @brief Batch two horizontal scroll messages. + * @param dest The original packet to batch into. + * @param src A later packet to attempt to batch. + * @return `batch_result_e` : The status of the batching operation. + */ + batch_result_e + batch(PSS_HSCROLL_PACKET dest, PSS_HSCROLL_PACKET src) { + short scrollAmt; + + // Batching is safe as long as the result doesn't overflow a 16-bit integer + if (!__builtin_add_overflow(util::endian::big(dest->scrollAmount), util::endian::big(src->scrollAmount), &scrollAmt)) { + return batch_result_e::terminate_batch; + } + + // Take the sum of delta + dest->scrollAmount = util::endian::big(scrollAmt); + return batch_result_e::batched; + } + + /** + * @brief Batch two controller state messages. + * @param dest The original packet to batch into. + * @param src A later packet to attempt to batch. + * @return `batch_result_e` : The status of the batching operation. + */ + batch_result_e + batch(PNV_MULTI_CONTROLLER_PACKET dest, PNV_MULTI_CONTROLLER_PACKET src) { + // Do not allow batching if the active controllers change + if (dest->activeGamepadMask != src->activeGamepadMask) { + return batch_result_e::terminate_batch; + } + + // We can only batch entries for the same controller, but allow batching attempts to continue + // in case we have more packets for this controller later in the queue. + if (dest->controllerNumber != src->controllerNumber) { + return batch_result_e::not_batchable; + } + + // Do not allow batching if the button state changes on this controller + if (dest->buttonFlags != src->buttonFlags || dest->buttonFlags2 != src->buttonFlags2) { + return batch_result_e::terminate_batch; + } + + // Take the latest state + *dest = *src; + return batch_result_e::batched; + } + + /** + * @brief Batch two touch messages. + * @param dest The original packet to batch into. + * @param src A later packet to attempt to batch. + * @return `batch_result_e` : The status of the batching operation. + */ + batch_result_e + batch(PSS_TOUCH_PACKET dest, PSS_TOUCH_PACKET src) { + // Only batch hover or move events + if (dest->eventType != LI_TOUCH_EVENT_MOVE && + dest->eventType != LI_TOUCH_EVENT_HOVER) { + return batch_result_e::terminate_batch; + } + + // Don't batch beyond state changing events + if (src->eventType != LI_TOUCH_EVENT_MOVE && + src->eventType != LI_TOUCH_EVENT_HOVER) { + return batch_result_e::terminate_batch; + } + + // Batched events must be the same pointer ID + if (dest->pointerId != src->pointerId) { + return batch_result_e::not_batchable; + } + + // The pointer must be in the same state + if (dest->eventType != src->eventType) { + return batch_result_e::terminate_batch; + } + + // Take the latest state + *dest = *src; + return batch_result_e::batched; + } + + /** + * @brief Batch two pen messages. + * @param dest The original packet to batch into. + * @param src A later packet to attempt to batch. + * @return `batch_result_e` : The status of the batching operation. + */ + batch_result_e + batch(PSS_PEN_PACKET dest, PSS_PEN_PACKET src) { + // Only batch hover or move events + if (dest->eventType != LI_TOUCH_EVENT_MOVE && + dest->eventType != LI_TOUCH_EVENT_HOVER) { + return batch_result_e::terminate_batch; + } + + // Batched events must be the same type + if (dest->eventType != src->eventType) { + return batch_result_e::terminate_batch; + } + + // Do not allow batching if the button state changes + if (dest->penButtons != src->penButtons) { + return batch_result_e::terminate_batch; + } + + // Do not batch beyond tool changes + if (dest->toolType != src->toolType) { + return batch_result_e::terminate_batch; + } + + // Take the latest state + *dest = *src; + return batch_result_e::batched; + } + + /** + * @brief Batch two controller touch messages. + * @param dest The original packet to batch into. + * @param src A later packet to attempt to batch. + * @return `batch_result_e` : The status of the batching operation. + */ + batch_result_e + batch(PSS_CONTROLLER_TOUCH_PACKET dest, PSS_CONTROLLER_TOUCH_PACKET src) { + // Only batch hover or move events + if (dest->eventType != LI_TOUCH_EVENT_MOVE && + dest->eventType != LI_TOUCH_EVENT_HOVER) { + return batch_result_e::terminate_batch; + } + + // We can only batch entries for the same controller, but allow batching attempts to continue + // in case we have more packets for this controller later in the queue. + if (dest->controllerNumber != src->controllerNumber) { + return batch_result_e::not_batchable; + } + + // Don't batch beyond state changing events + if (src->eventType != LI_TOUCH_EVENT_MOVE && + src->eventType != LI_TOUCH_EVENT_HOVER) { + return batch_result_e::terminate_batch; + } + + // Batched events must be the same pointer ID + if (dest->pointerId != src->pointerId) { + return batch_result_e::not_batchable; + } + + // The pointer must be in the same state + if (dest->eventType != src->eventType) { + return batch_result_e::terminate_batch; + } + + // Take the latest state + *dest = *src; + return batch_result_e::batched; + } + + /** + * @brief Batch two controller motion messages. + * @param dest The original packet to batch into. + * @param src A later packet to attempt to batch. + * @return `batch_result_e` : The status of the batching operation. + */ + batch_result_e + batch(PSS_CONTROLLER_MOTION_PACKET dest, PSS_CONTROLLER_MOTION_PACKET src) { + // We can only batch entries for the same controller, but allow batching attempts to continue + // in case we have more packets for this controller later in the queue. + if (dest->controllerNumber != src->controllerNumber) { + return batch_result_e::not_batchable; + } + + // Batched events must be the same sensor + if (dest->motionType != src->motionType) { + return batch_result_e::not_batchable; + } + + // Take the latest state + *dest = *src; + return batch_result_e::batched; + } + + /** + * @brief Batch two input messages. + * @param dest The original packet to batch into. + * @param src A later packet to attempt to batch. + * @return `batch_result_e` : The status of the batching operation. + */ + batch_result_e + batch(PNV_INPUT_HEADER dest, PNV_INPUT_HEADER src) { + // We can only batch if the packet types are the same + if (dest->magic != src->magic) { + return batch_result_e::terminate_batch; + } + + // We can only batch certain message types + switch (util::endian::little(dest->magic)) { + case MOUSE_MOVE_REL_MAGIC_GEN5: + return batch((PNV_REL_MOUSE_MOVE_PACKET) dest, (PNV_REL_MOUSE_MOVE_PACKET) src); + case MOUSE_MOVE_ABS_MAGIC: + return batch((PNV_ABS_MOUSE_MOVE_PACKET) dest, (PNV_ABS_MOUSE_MOVE_PACKET) src); + case SCROLL_MAGIC_GEN5: + return batch((PNV_SCROLL_PACKET) dest, (PNV_SCROLL_PACKET) src); + case SS_HSCROLL_MAGIC: + return batch((PSS_HSCROLL_PACKET) dest, (PSS_HSCROLL_PACKET) src); + case MULTI_CONTROLLER_MAGIC_GEN5: + return batch((PNV_MULTI_CONTROLLER_PACKET) dest, (PNV_MULTI_CONTROLLER_PACKET) src); + case SS_TOUCH_MAGIC: + return batch((PSS_TOUCH_PACKET) dest, (PSS_TOUCH_PACKET) src); + case SS_PEN_MAGIC: + return batch((PSS_PEN_PACKET) dest, (PSS_PEN_PACKET) src); + case SS_CONTROLLER_TOUCH_MAGIC: + return batch((PSS_CONTROLLER_TOUCH_PACKET) dest, (PSS_CONTROLLER_TOUCH_PACKET) src); + case SS_CONTROLLER_MOTION_MAGIC: + return batch((PSS_CONTROLLER_MOTION_PACKET) dest, (PSS_CONTROLLER_MOTION_PACKET) src); + default: + // Not a batchable message type + return batch_result_e::terminate_batch; + } + } + + /** + * @brief Called on a thread pool thread to process an input message. + * @param input The input context pointer. + */ void - passthrough_helper(std::shared_ptr input, std::vector &&input_data) { - void *payload = input_data.data(); - auto header = (PNV_INPUT_HEADER) payload; + passthrough_next_message(std::shared_ptr input) { + // 'entry' backs the 'payload' pointer, so they must remain in scope together + std::vector entry; + PNV_INPUT_HEADER payload; + + // Lock the input queue while batching, but release it before sending + // the input to the OS. This avoids potentially lengthy lock contention + // in the control stream thread while input is being processed by the OS. + { + std::lock_guard lg(input->input_queue_lock); + + // If all entries have already been processed, nothing to do + if (input->input_queue.empty()) { + return; + } - switch (util::endian::little(header->magic)) { + // Pop off the first entry, which we will send + entry = input->input_queue.front(); + payload = (PNV_INPUT_HEADER) entry.data(); + input->input_queue.pop_front(); + + // Try to batch with remaining items on the queue + auto i = input->input_queue.begin(); + while (i != input->input_queue.end()) { + auto batchable_entry = *i; + auto batchable_payload = (PNV_INPUT_HEADER) batchable_entry.data(); + + auto batch_result = batch(payload, batchable_payload); + if (batch_result == batch_result_e::terminate_batch) { + // Stop batching + break; + } + else if (batch_result == batch_result_e::batched) { + // Erase this entry since it was batched + i = input->input_queue.erase(i); + } + else { + // We couldn't batch this entry, but try to batch later entries. + i++; + } + } + } + + // Print the final input packet + input::print((void *) payload); + + // Send the batched input to the OS + switch (util::endian::little(payload->magic)) { case MOUSE_MOVE_REL_MAGIC_GEN5: passthrough(input, (PNV_REL_MOUSE_MOVE_PACKET) payload); break; @@ -778,10 +1596,10 @@ namespace input { passthrough(input, (PNV_MOUSE_BUTTON_PACKET) payload); break; case SCROLL_MAGIC_GEN5: - passthrough((PNV_SCROLL_PACKET) payload); + passthrough(input, (PNV_SCROLL_PACKET) payload); break; case SS_HSCROLL_MAGIC: - passthrough((PSS_HSCROLL_PACKET) payload); + passthrough(input, (PSS_HSCROLL_PACKET) payload); break; case KEY_DOWN_EVENT_MAGIC: case KEY_UP_EVENT_MAGIC: @@ -793,12 +1611,39 @@ namespace input { case MULTI_CONTROLLER_MAGIC_GEN5: passthrough(input, (PNV_MULTI_CONTROLLER_PACKET) payload); break; + case SS_TOUCH_MAGIC: + passthrough(input, (PSS_TOUCH_PACKET) payload); + break; + case SS_PEN_MAGIC: + passthrough(input, (PSS_PEN_PACKET) payload); + break; + case SS_CONTROLLER_ARRIVAL_MAGIC: + passthrough(input, (PSS_CONTROLLER_ARRIVAL_PACKET) payload); + break; + case SS_CONTROLLER_TOUCH_MAGIC: + passthrough(input, (PSS_CONTROLLER_TOUCH_PACKET) payload); + break; + case SS_CONTROLLER_MOTION_MAGIC: + passthrough(input, (PSS_CONTROLLER_MOTION_PACKET) payload); + break; + case SS_CONTROLLER_BATTERY_MAGIC: + passthrough(input, (PSS_CONTROLLER_BATTERY_PACKET) payload); + break; } } + /** + * @brief Called on the control stream thread to queue an input message. + * @param input The input context pointer. + * @param input_data The input message. + */ void passthrough(std::shared_ptr &input, std::vector &&input_data) { - task_pool.push(passthrough_helper, input, move_by_copy_util::cmove(input_data)); + { + std::lock_guard lg(input->input_queue_lock); + input->input_queue.push_back(std::move(input_data)); + } + task_pool.push(passthrough_next_message, input); } void @@ -816,6 +1661,10 @@ namespace input { } for (auto &kp : key_press) { + if (!kp.second) { + // already released + continue; + } platf::keyboard(platf_input, vk_from_kpid(kp.first) & 0x00FF, true, flags_from_kpid(kp.first)); key_press[kp.first] = false; } @@ -840,7 +1689,7 @@ namespace input { alloc(safe::mail_t mail) { auto input = std::make_shared( mail->event(mail::touch_port), - mail->queue(mail::rumble)); + mail->queue(mail::gamepad_feedback)); // Workaround to ensure new frames will be captured when a client connects task_pool.pushDelayed([]() { diff --git a/src/input.h b/src/input.h index 095c20ee717..33a9ee42741 100644 --- a/src/input.h +++ b/src/input.h @@ -32,5 +32,13 @@ namespace input { float client_offsetX, client_offsetY; float scalar_inv; + + explicit + operator bool() const { + return width != 0 && height != 0 && env_width != 0 && env_height != 0; + } }; + + std::pair + scale_client_contact_area(const std::pair &val, uint16_t rotation, const std::pair &scalar); } // namespace input diff --git a/src/logging.cpp b/src/logging.cpp new file mode 100644 index 00000000000..e03bcbf5134 --- /dev/null +++ b/src/logging.cpp @@ -0,0 +1,216 @@ +/** + * @file src/logging.cpp + * @brief Logging implementation file for the Sunshine application. + */ + +// standard includes +#include +#include + +// lib includes +#include +#include +#include +#include +#include +#include + +// local includes +#include "logging.h" + +extern "C" { +#include +} + +using namespace std::literals; + +namespace bl = boost::log; + +boost::shared_ptr> sink; + +bl::sources::severity_logger verbose(0); // Dominating output +bl::sources::severity_logger debug(1); // Follow what is happening +bl::sources::severity_logger info(2); // Should be informed about +bl::sources::severity_logger warning(3); // Strange events +bl::sources::severity_logger error(4); // Recoverable errors +bl::sources::severity_logger fatal(5); // Unrecoverable errors + +BOOST_LOG_ATTRIBUTE_KEYWORD(severity, "Severity", int) + +namespace logging { + /** + * @brief A destructor that restores the initial state. + */ + deinit_t::~deinit_t() { + deinit(); + } + + /** + * @brief Deinitialize the logging system. + * + * EXAMPLES: + * ```cpp + * deinit(); + * ``` + */ + void + deinit() { + log_flush(); + bl::core::get()->remove_sink(sink); + sink.reset(); + } + + /** + * @brief Initialize the logging system. + * @param min_log_level The minimum log level to output. + * @param log_file The log file to write to. + * @returns A deinit_t object that will deinitialize the logging system when it goes out of scope. + * + * EXAMPLES: + * ```cpp + * log_init(2, "sunshine.log"); + * ``` + */ + [[nodiscard]] std::unique_ptr + init(int min_log_level, const std::string &log_file) { + if (sink) { + // Deinitialize the logging system before reinitializing it. This can probably only ever be hit in tests. + deinit(); + } + + setup_av_logging(min_log_level); + + sink = boost::make_shared(); + + boost::shared_ptr stream { &std::cout, boost::null_deleter() }; + sink->locked_backend()->add_stream(stream); + sink->locked_backend()->add_stream(boost::make_shared(log_file)); + sink->set_filter(severity >= min_log_level); + + sink->set_formatter([](const bl::record_view &view, bl::formatting_ostream &os) { + constexpr const char *message = "Message"; + constexpr const char *severity = "Severity"; + constexpr int DATE_BUFFER_SIZE = 21 + 2 + 1; // Full string plus ": \0" + + auto log_level = view.attribute_values()[severity].extract().get(); + + std::string_view log_type; + switch (log_level) { + case 0: + log_type = "Verbose: "sv; + break; + case 1: + log_type = "Debug: "sv; + break; + case 2: + log_type = "Info: "sv; + break; + case 3: + log_type = "Warning: "sv; + break; + case 4: + log_type = "Error: "sv; + break; + case 5: + log_type = "Fatal: "sv; + break; + }; + + char _date[DATE_BUFFER_SIZE]; + std::time_t t = std::time(nullptr); + strftime(_date, DATE_BUFFER_SIZE, "[%Y:%m:%d:%H:%M:%S]: ", std::localtime(&t)); + + os << _date << log_type << view.attribute_values()[message].extract(); + }); + + // Flush after each log record to ensure log file contents on disk isn't stale. + // This is particularly important when running from a Windows service. + sink->locked_backend()->auto_flush(true); + + bl::core::get()->add_sink(sink); + return std::make_unique(); + } + + /** + * @brief Setup AV logging. + * @param min_log_level The log level. + */ + void + setup_av_logging(int min_log_level) { + if (min_log_level >= 1) { + av_log_set_level(AV_LOG_QUIET); + } + else { + av_log_set_level(AV_LOG_DEBUG); + } + av_log_set_callback([](void *ptr, int level, const char *fmt, va_list vl) { + static int print_prefix = 1; + char buffer[1024]; + + av_log_format_line(ptr, level, fmt, vl, buffer, sizeof(buffer), &print_prefix); + if (level <= AV_LOG_ERROR) { + // We print AV_LOG_FATAL at the error level. FFmpeg prints things as fatal that + // are expected in some cases, such as lack of codec support or similar things. + BOOST_LOG(error) << buffer; + } + else if (level <= AV_LOG_WARNING) { + BOOST_LOG(warning) << buffer; + } + else if (level <= AV_LOG_INFO) { + BOOST_LOG(info) << buffer; + } + else if (level <= AV_LOG_VERBOSE) { + // AV_LOG_VERBOSE is less verbose than AV_LOG_DEBUG + BOOST_LOG(debug) << buffer; + } + else { + BOOST_LOG(verbose) << buffer; + } + }); + } + + /** + * @brief Flush the log. + * + * EXAMPLES: + * ```cpp + * log_flush(); + * ``` + */ + void + log_flush() { + if (sink) { + sink->flush(); + } + } + + /** + * @brief Print help to stdout. + * @param name The name of the program. + * + * EXAMPLES: + * ```cpp + * print_help("sunshine"); + * ``` + */ + void + print_help(const char *name) { + std::cout + << "Usage: "sv << name << " [options] [/path/to/configuration_file] [--cmd]"sv << std::endl + << " Any configurable option can be overwritten with: \"name=value\""sv << std::endl + << std::endl + << " Note: The configuration will be created if it doesn't exist."sv << std::endl + << std::endl + << " --help | print help"sv << std::endl + << " --creds username password | set user credentials for the Web manager"sv << std::endl + << " --version | print the version of sunshine"sv << std::endl + << std::endl + << " flags"sv << std::endl + << " -0 | Read PIN from stdin"sv << std::endl + << " -1 | Do not load previously saved state and do retain any state after shutdown"sv << std::endl + << " | Effectively starting as if for the first time without overwriting any pairings with your devices"sv << std::endl + << " -2 | Force replacement of headers in video stream"sv << std::endl + << " -p | Enable/Disable UPnP"sv << std::endl + << std::endl; + } +} // namespace logging diff --git a/src/logging.h b/src/logging.h new file mode 100644 index 00000000000..24f9d169082 --- /dev/null +++ b/src/logging.h @@ -0,0 +1,38 @@ +/** + * @file src/logging.h + * @brief Logging header file for the Sunshine application. + */ + +// macros +#pragma once + +// lib includes +#include +#include + +using text_sink = boost::log::sinks::asynchronous_sink; + +extern boost::log::sources::severity_logger verbose; +extern boost::log::sources::severity_logger debug; +extern boost::log::sources::severity_logger info; +extern boost::log::sources::severity_logger warning; +extern boost::log::sources::severity_logger error; +extern boost::log::sources::severity_logger fatal; + +namespace logging { + class deinit_t { + public: + ~deinit_t(); + }; + + void + deinit(); + [[nodiscard]] std::unique_ptr + init(int min_log_level, const std::string &log_file); + void + setup_av_logging(int min_log_level); + void + log_flush(); + void + print_help(const char *name); +} // namespace logging diff --git a/src/main.cpp b/src/main.cpp index b347cc0ae5a..6231e503ef3 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -4,383 +4,30 @@ */ // standard includes +#include #include -#include #include #include -#include - -// lib includes -#include -#include -#include -#include -#include // local includes -#include "config.h" #include "confighttp.h" +#include "entry_handler.h" +#include "globals.h" #include "httpcommon.h" +#include "logging.h" #include "main.h" #include "nvhttp.h" -#include "platform/common.h" #include "process.h" -#include "rtsp.h" #include "system_tray.h" -#include "thread_pool.h" #include "upnp.h" #include "version.h" #include "video.h" extern "C" { -#include #include - -#ifdef _WIN32 - #include -#endif } -safe::mail_t mail::man; - using namespace std::literals; -namespace bl = boost::log; - -thread_pool_util::ThreadPool task_pool; -bl::sources::severity_logger verbose(0); // Dominating output -bl::sources::severity_logger debug(1); // Follow what is happening -bl::sources::severity_logger info(2); // Should be informed about -bl::sources::severity_logger warning(3); // Strange events -bl::sources::severity_logger error(4); // Recoverable errors -bl::sources::severity_logger fatal(5); // Unrecoverable errors - -bool display_cursor = true; - -using text_sink = bl::sinks::asynchronous_sink; -boost::shared_ptr sink; - -struct NoDelete { - void - operator()(void *) {} -}; - -BOOST_LOG_ATTRIBUTE_KEYWORD(severity, "Severity", int) - -/** - * @brief Print help to stdout. - * @param name The name of the program. - * - * EXAMPLES: - * ```cpp - * print_help("sunshine"); - * ``` - */ -void -print_help(const char *name) { - std::cout - << "Usage: "sv << name << " [options] [/path/to/configuration_file] [--cmd]"sv << std::endl - << " Any configurable option can be overwritten with: \"name=value\""sv << std::endl - << std::endl - << " Note: The configuration will be created if it doesn't exist."sv << std::endl - << std::endl - << " --help | print help"sv << std::endl - << " --creds username password | set user credentials for the Web manager"sv << std::endl - << " --version | print the version of sunshine"sv << std::endl - << std::endl - << " flags"sv << std::endl - << " -0 | Read PIN from stdin"sv << std::endl - << " -1 | Do not load previously saved state and do retain any state after shutdown"sv << std::endl - << " | Effectively starting as if for the first time without overwriting any pairings with your devices"sv << std::endl - << " -2 | Force replacement of headers in video stream"sv << std::endl - << " -p | Enable/Disable UPnP"sv << std::endl - << std::endl; -} - -namespace help { - int - entry(const char *name, int argc, char *argv[]) { - print_help(name); - return 0; - } -} // namespace help - -namespace version { - int - entry(const char *name, int argc, char *argv[]) { - std::cout << PROJECT_NAME << " version: v" << PROJECT_VER << std::endl; - return 0; - } -} // namespace version - -namespace lifetime { - static char **argv; - static std::atomic_int desired_exit_code; - - /** - * @brief Terminates Sunshine gracefully with the provided exit code. - * @param exit_code The exit code to return from main(). - * @param async Specifies whether our termination will be non-blocking. - */ - void - exit_sunshine(int exit_code, bool async) { - // Store the exit code of the first exit_sunshine() call - int zero = 0; - desired_exit_code.compare_exchange_strong(zero, exit_code); - - // Raise SIGINT to start termination - std::raise(SIGINT); - - // Termination will happen asynchronously, but the caller may - // have wanted synchronous behavior. - while (!async) { - std::this_thread::sleep_for(1s); - } - } - - /** - * @brief Gets the argv array passed to main(). - */ - char ** - get_argv() { - return argv; - } -} // namespace lifetime - -#ifdef _WIN32 -namespace service_ctrl { - class service_controller { - public: - /** - * @brief Constructor for service_controller class. - * @param service_desired_access SERVICE_* desired access flags. - */ - service_controller(DWORD service_desired_access) { - scm_handle = OpenSCManagerA(nullptr, nullptr, SC_MANAGER_CONNECT); - if (!scm_handle) { - auto winerr = GetLastError(); - BOOST_LOG(error) << "OpenSCManager() failed: "sv << winerr; - return; - } - - service_handle = OpenServiceA(scm_handle, "SunshineService", service_desired_access); - if (!service_handle) { - auto winerr = GetLastError(); - BOOST_LOG(error) << "OpenService() failed: "sv << winerr; - return; - } - } - - ~service_controller() { - if (service_handle) { - CloseServiceHandle(service_handle); - } - - if (scm_handle) { - CloseServiceHandle(scm_handle); - } - } - - /** - * @brief Asynchronously starts the Sunshine service. - */ - bool - start_service() { - if (!service_handle) { - return false; - } - - if (!StartServiceA(service_handle, 0, nullptr)) { - auto winerr = GetLastError(); - if (winerr != ERROR_SERVICE_ALREADY_RUNNING) { - BOOST_LOG(error) << "StartService() failed: "sv << winerr; - return false; - } - } - - return true; - } - - /** - * @brief Query the service status. - * @param status The SERVICE_STATUS struct to populate. - */ - bool - query_service_status(SERVICE_STATUS &status) { - if (!service_handle) { - return false; - } - - if (!QueryServiceStatus(service_handle, &status)) { - auto winerr = GetLastError(); - BOOST_LOG(error) << "QueryServiceStatus() failed: "sv << winerr; - return false; - } - - return true; - } - - private: - SC_HANDLE scm_handle = NULL; - SC_HANDLE service_handle = NULL; - }; - - /** - * @brief Check if the service is running. - * - * EXAMPLES: - * ```cpp - * is_service_running(); - * ``` - */ - bool - is_service_running() { - service_controller sc { SERVICE_QUERY_STATUS }; - - SERVICE_STATUS status; - if (!sc.query_service_status(status)) { - return false; - } - - return status.dwCurrentState == SERVICE_RUNNING; - } - - /** - * @brief Start the service and wait for startup to complete. - * - * EXAMPLES: - * ```cpp - * start_service(); - * ``` - */ - bool - start_service() { - service_controller sc { SERVICE_QUERY_STATUS | SERVICE_START }; - - std::cout << "Starting Sunshine..."sv; - - // This operation is asynchronous, so we must wait for it to complete - if (!sc.start_service()) { - return false; - } - - SERVICE_STATUS status; - do { - Sleep(1000); - std::cout << '.'; - } while (sc.query_service_status(status) && status.dwCurrentState == SERVICE_START_PENDING); - - if (status.dwCurrentState != SERVICE_RUNNING) { - BOOST_LOG(error) << SERVICE_NAME " failed to start: "sv << status.dwWin32ExitCode; - return false; - } - - std::cout << std::endl; - return true; - } - - /** - * @brief Wait for the UI to be ready after Sunshine startup. - * - * EXAMPLES: - * ```cpp - * wait_for_ui_ready(); - * ``` - */ - bool - wait_for_ui_ready() { - std::cout << "Waiting for Web UI to be ready..."; - - // Wait up to 30 seconds for the web UI to start - for (int i = 0; i < 30; i++) { - PMIB_TCPTABLE tcp_table = nullptr; - ULONG table_size = 0; - ULONG err; - - auto fg = util::fail_guard([&tcp_table]() { - free(tcp_table); - }); - - do { - // Query all open TCP sockets to look for our web UI port - err = GetTcpTable(tcp_table, &table_size, false); - if (err == ERROR_INSUFFICIENT_BUFFER) { - free(tcp_table); - tcp_table = (PMIB_TCPTABLE) malloc(table_size); - } - } while (err == ERROR_INSUFFICIENT_BUFFER); - - if (err != NO_ERROR) { - BOOST_LOG(error) << "Failed to query TCP table: "sv << err; - return false; - } - - uint16_t port_nbo = htons(map_port(confighttp::PORT_HTTPS)); - for (DWORD i = 0; i < tcp_table->dwNumEntries; i++) { - auto &entry = tcp_table->table[i]; - - // Look for our port in the listening state - if (entry.dwLocalPort == port_nbo && entry.dwState == MIB_TCP_STATE_LISTEN) { - std::cout << std::endl; - return true; - } - } - - Sleep(1000); - std::cout << '.'; - } - - std::cout << "timed out"sv << std::endl; - return false; - } -} // namespace service_ctrl - -/** - * @brief Checks if NVIDIA's GameStream software is running. - * @return `true` if GameStream is enabled. - */ -bool -is_gamestream_enabled() { - DWORD enabled; - DWORD size = sizeof(enabled); - return RegGetValueW( - HKEY_LOCAL_MACHINE, - L"SOFTWARE\\NVIDIA Corporation\\NvStream", - L"EnableStreaming", - RRF_RT_REG_DWORD, - nullptr, - &enabled, - &size) == ERROR_SUCCESS && - enabled != 0; -} - -#endif - -/** - * @brief Launch the Web UI. - * - * EXAMPLES: - * ```cpp - * launch_ui(); - * ``` - */ -void -launch_ui() { - std::string url = "https://localhost:" + std::to_string(map_port(confighttp::PORT_HTTPS)); - platf::open_url(url); -} - -/** - * @brief Flush the log. - * - * EXAMPLES: - * ```cpp - * log_flush(); - * ``` - */ -void -log_flush() { - sink->flush(); -} std::map> signal_handlers; void @@ -396,30 +43,25 @@ on_signal(int sig, FN &&fn) { std::signal(sig, on_signal_forwarder); } -namespace gen_creds { - int - entry(const char *name, int argc, char *argv[]) { - if (argc < 2 || argv[0] == "help"sv || argv[1] == "help"sv) { - print_help(name); - return 0; - } - - http::save_user_creds(config::sunshine.credentials_file, argv[0], argv[1]); - - return 0; - } -} // namespace gen_creds - std::map> cmd_to_func { - { "creds"sv, gen_creds::entry }, - { "help"sv, help::entry }, - { "version"sv, version::entry } + { "creds"sv, args::creds }, + { "help"sv, args::help }, + { "version"sv, args::version }, +#ifdef _WIN32 + { "restore-nvprefs-undo"sv, args::restore_nvprefs_undo }, +#endif }; #ifdef _WIN32 LRESULT CALLBACK SessionMonitorWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { switch (uMsg) { + case WM_CLOSE: + DestroyWindow(hwnd); + return 0; + case WM_DESTROY: + PostQuitMessage(0); + return 0; case WM_ENDSESSION: { // Terminate ourselves with a blocking exit call std::cout << "Received WM_ENDSESSION"sv << std::endl; @@ -449,16 +91,78 @@ main(int argc, char *argv[]) { task_pool_util::TaskPool::task_id_t force_shutdown = nullptr; #ifdef _WIN32 + setlocale(LC_ALL, "C"); +#endif + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + // Use UTF-8 conversion for the default C++ locale (used by boost::log) + std::locale::global(std::locale(std::locale(), new std::codecvt_utf8)); +#pragma GCC diagnostic pop + + mail::man = std::make_shared(); + + if (config::parse(argc, argv)) { + return 0; + } + + auto log_deinit_guard = logging::init(config::sunshine.min_log_level, config::sunshine.log_file); + if (!log_deinit_guard) { + BOOST_LOG(error) << "Logging failed to initialize"sv; + } + + // logging can begin at this point + // if anything is logged prior to this point, it will appear in stdout, but not in the log viewer in the UI + // the version should be printed to the log before anything else + BOOST_LOG(info) << PROJECT_NAME << " version: " << PROJECT_VER; + + if (!config::sunshine.cmd.name.empty()) { + auto fn = cmd_to_func.find(config::sunshine.cmd.name); + if (fn == std::end(cmd_to_func)) { + BOOST_LOG(fatal) << "Unknown command: "sv << config::sunshine.cmd.name; + + BOOST_LOG(info) << "Possible commands:"sv; + for (auto &[key, _] : cmd_to_func) { + BOOST_LOG(info) << '\t' << key; + } + + return 7; + } + + return fn->second(argv[0], config::sunshine.cmd.argc, config::sunshine.cmd.argv); + } + +#ifdef WIN32 + // Modify relevant NVIDIA control panel settings if the system has corresponding gpu + if (nvprefs_instance.load()) { + // Restore global settings to the undo file left by improper termination of sunshine.exe + nvprefs_instance.restore_from_and_delete_undo_file_if_exists(); + // Modify application settings for sunshine.exe + nvprefs_instance.modify_application_profile(); + // Modify global settings, undo file is produced in the process to restore after improper termination + nvprefs_instance.modify_global_profile(); + // Unload dynamic library to survive driver re-installation + nvprefs_instance.unload(); + } + // Wait as long as possible to terminate Sunshine.exe during logoff/shutdown SetProcessShutdownParameters(0x100, SHUTDOWN_NORETRY); // We must create a hidden window to receive shutdown notifications since we load gdi32.dll - std::thread window_thread([]() { + std::promise session_monitor_hwnd_promise; + auto session_monitor_hwnd_future = session_monitor_hwnd_promise.get_future(); + std::promise session_monitor_join_thread_promise; + auto session_monitor_join_thread_future = session_monitor_join_thread_promise.get_future(); + + std::thread session_monitor_thread([&]() { + session_monitor_join_thread_promise.set_value_at_thread_exit(); + WNDCLASSA wnd_class {}; wnd_class.lpszClassName = "SunshineSessionMonitorClass"; wnd_class.lpfnWndProc = SessionMonitorWindowProc; if (!RegisterClassA(&wnd_class)) { - std::cout << "Failed to register session monitor window class"sv << std::endl; + session_monitor_hwnd_promise.set_value(NULL); + BOOST_LOG(error) << "Failed to register session monitor window class"sv << std::endl; return; } @@ -475,8 +179,11 @@ main(int argc, char *argv[]) { nullptr, nullptr, nullptr); + + session_monitor_hwnd_promise.set_value(wnd); + if (!wnd) { - std::cout << "Failed to create session monitor window"sv << std::endl; + BOOST_LOG(error) << "Failed to create session monitor window"sv << std::endl; return; } @@ -489,86 +196,30 @@ main(int argc, char *argv[]) { DispatchMessage(&msg); } }); - window_thread.detach(); -#endif - mail::man = std::make_shared(); - - if (config::parse(argc, argv)) { - return 0; - } - - if (config::sunshine.min_log_level >= 1) { - av_log_set_level(AV_LOG_QUIET); - } - else { - av_log_set_level(AV_LOG_DEBUG); - } - - sink = boost::make_shared(); - - boost::shared_ptr stream { &std::cout, NoDelete {} }; - sink->locked_backend()->add_stream(stream); - sink->locked_backend()->add_stream(boost::make_shared(config::sunshine.log_file)); - sink->set_filter(severity >= config::sunshine.min_log_level); - - sink->set_formatter([message = "Message"s, severity = "Severity"s](const bl::record_view &view, bl::formatting_ostream &os) { - constexpr int DATE_BUFFER_SIZE = 21 + 2 + 1; // Full string plus ": \0" - - auto log_level = view.attribute_values()[severity].extract().get(); - - std::string_view log_type; - switch (log_level) { - case 0: - log_type = "Verbose: "sv; - break; - case 1: - log_type = "Debug: "sv; - break; - case 2: - log_type = "Info: "sv; - break; - case 3: - log_type = "Warning: "sv; - break; - case 4: - log_type = "Error: "sv; - break; - case 5: - log_type = "Fatal: "sv; - break; - }; + auto session_monitor_join_thread_guard = util::fail_guard([&]() { + if (session_monitor_hwnd_future.wait_for(1s) == std::future_status::ready) { + if (HWND session_monitor_hwnd = session_monitor_hwnd_future.get()) { + PostMessage(session_monitor_hwnd, WM_CLOSE, 0, 0); + } - char _date[DATE_BUFFER_SIZE]; - std::time_t t = std::time(nullptr); - strftime(_date, DATE_BUFFER_SIZE, "[%Y:%m:%d:%H:%M:%S]: ", std::localtime(&t)); + if (session_monitor_join_thread_future.wait_for(1s) == std::future_status::ready) { + session_monitor_thread.join(); + return; + } + else { + BOOST_LOG(warning) << "session_monitor_join_thread_future reached timeout"; + } + } + else { + BOOST_LOG(warning) << "session_monitor_hwnd_future reached timeout"; + } - os << _date << log_type << view.attribute_values()[message].extract(); + session_monitor_thread.detach(); }); - // Flush after each log record to ensure log file contents on disk isn't stale. - // This is particularly important when running from a Windows service. - sink->locked_backend()->auto_flush(true); - - bl::core::get()->add_sink(sink); - auto fg = util::fail_guard(log_flush); - - if (!config::sunshine.cmd.name.empty()) { - auto fn = cmd_to_func.find(config::sunshine.cmd.name); - if (fn == std::end(cmd_to_func)) { - BOOST_LOG(fatal) << "Unknown command: "sv << config::sunshine.cmd.name; - - BOOST_LOG(info) << "Possible commands:"sv; - for (auto &[key, _] : cmd_to_func) { - BOOST_LOG(info) << '\t' << key; - } - - return 7; - } +#endif - return fn->second(argv[0], config::sunshine.cmd.argc, config::sunshine.cmd.argv); - } - BOOST_LOG(info) << PROJECT_NAME << " version: " << PROJECT_VER << std::endl; task_pool.start(1); #if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 @@ -583,8 +234,8 @@ main(int argc, char *argv[]) { auto task = []() { BOOST_LOG(fatal) << "10 seconds passed, yet Sunshine's still running: Forcing shutdown"sv; - log_flush(); - std::abort(); + logging::log_flush(); + lifetime::debug_trap(); }; force_shutdown = task_pool.pushDelayed(task, 10s).task_id; @@ -596,8 +247,8 @@ main(int argc, char *argv[]) { auto task = []() { BOOST_LOG(fatal) << "10 seconds passed, yet Sunshine's still running: Forcing shutdown"sv; - log_flush(); - std::abort(); + logging::log_flush(); + lifetime::debug_trap(); }; force_shutdown = task_pool.pushDelayed(task, 10s).task_id; @@ -609,8 +260,8 @@ main(int argc, char *argv[]) { // If any of the following fail, we log an error and continue event though sunshine will not function correctly. // This allows access to the UI to fix configuration problems or view the logs. - auto deinit_guard = platf::init(); - if (!deinit_guard) { + auto platf_deinit_guard = platf::init(); + if (!platf_deinit_guard) { BOOST_LOG(error) << "Platform failed to initialize"sv; } @@ -675,76 +326,13 @@ main(int argc, char *argv[]) { system_tray::end_tray(); #endif - return lifetime::desired_exit_code; -} - -/** - * @brief Read a file to string. - * @param path The path of the file. - * @return `std::string` : The contents of the file. - * - * EXAMPLES: - * ```cpp - * std::string contents = read_file("path/to/file"); - * ``` - */ -std::string -read_file(const char *path) { - if (!std::filesystem::exists(path)) { - BOOST_LOG(debug) << "Missing file: " << path; - return {}; +#ifdef WIN32 + // Restore global NVIDIA control panel settings + if (nvprefs_instance.owning_undo_file() && nvprefs_instance.load()) { + nvprefs_instance.restore_global_profile(); + nvprefs_instance.unload(); } +#endif - std::ifstream in(path); - - std::string input; - std::string base64_cert; - - while (!in.eof()) { - std::getline(in, input); - base64_cert += input + '\n'; - } - - return base64_cert; -} - -/** - * @brief Writes a file. - * @param path The path of the file. - * @param contents The contents to write. - * @return `int` : `0` on success, `-1` on failure. - * - * EXAMPLES: - * ```cpp - * int write_status = write_file("path/to/file", "file contents"); - * ``` - */ -int -write_file(const char *path, const std::string_view &contents) { - std::ofstream out(path); - - if (!out.is_open()) { - return -1; - } - - out << contents; - - return 0; -} - -/** - * @brief Map a specified port based on the base port. - * @param port The port to map as a difference from the base port. - * @return `std:uint16_t` : The mapped port number. - * - * EXAMPLES: - * ```cpp - * std::uint16_t mapped_port = map_port(1); - * ``` - */ -std::uint16_t -map_port(int port) { - // TODO: Ensure port is in the range of 21-65535 - // TODO: Ensure port is not already in use by another application - return (std::uint16_t)((int) config::sunshine.port + port); + return lifetime::desired_exit_code; } diff --git a/src/main.h b/src/main.h index e98206f3838..f34ca6cda66 100644 --- a/src/main.h +++ b/src/main.h @@ -6,84 +6,6 @@ // macros #pragma once -// standard includes -#include -#include - -// lib includes -#include - -// local includes -#include "thread_pool.h" -#include "thread_safe.h" - -extern thread_pool_util::ThreadPool task_pool; -extern bool display_cursor; - -extern boost::log::sources::severity_logger verbose; -extern boost::log::sources::severity_logger debug; -extern boost::log::sources::severity_logger info; -extern boost::log::sources::severity_logger warning; -extern boost::log::sources::severity_logger error; -extern boost::log::sources::severity_logger fatal; - // functions int main(int argc, char *argv[]); -void -log_flush(); -void -print_help(const char *name); -std::string -read_file(const char *path); -int -write_file(const char *path, const std::string_view &contents); -std::uint16_t -map_port(int port); -void -launch_ui(); - -// namespaces -namespace mail { -#define MAIL(x) \ - constexpr auto x = std::string_view { \ - #x \ - } - - extern safe::mail_t man; - - // Global mail - MAIL(shutdown); - MAIL(broadcast_shutdown); - MAIL(video_packets); - MAIL(audio_packets); - MAIL(switch_display); - - // Local mail - MAIL(touch_port); - MAIL(idr); - MAIL(rumble); - MAIL(hdr); -#undef MAIL - -} // namespace mail - -namespace lifetime { - void - exit_sunshine(int exit_code, bool async); - char ** - get_argv(); -} // namespace lifetime - -#ifdef _WIN32 -namespace service_ctrl { - bool - is_service_running(); - - bool - start_service(); - - bool - wait_for_ui_ready(); -} // namespace service_ctrl -#endif diff --git a/src/network.cpp b/src/network.cpp index f65bdfaae89..2784afebc39 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -3,58 +3,35 @@ * @brief todo */ #include "network.h" +#include "config.h" +#include "logging.h" #include "utility.h" #include +#include using namespace std::literals; -namespace net { - // In the format "xxx.xxx.xxx.xxx/x" - std::pair - ip_block(const std::string_view &ip); - std::vector> pc_ips { - ip_block("127.0.0.1/32"sv) +namespace ip = boost::asio::ip; + +namespace net { + std::vector pc_ips_v4 { + ip::make_network_v4("127.0.0.0/8"sv), }; - std::vector> lan_ips { - ip_block("192.168.0.0/16"sv), - ip_block("172.16.0.0/12"sv), - ip_block("10.0.0.0/8"sv), - ip_block("100.64.0.0/10"sv) + std::vector lan_ips_v4 { + ip::make_network_v4("192.168.0.0/16"sv), + ip::make_network_v4("172.16.0.0/12"sv), + ip::make_network_v4("10.0.0.0/8"sv), + ip::make_network_v4("100.64.0.0/10"sv), + ip::make_network_v4("169.254.0.0/16"sv), }; - std::uint32_t - ip(const std::string_view &ip_str) { - auto begin = std::begin(ip_str); - auto end = std::end(ip_str); - auto temp_end = std::find(begin, end, '.'); - - std::uint32_t ip = 0; - auto shift = 24; - while (temp_end != end) { - ip += (util::from_chars(begin, temp_end) << shift); - shift -= 8; - - begin = temp_end + 1; - temp_end = std::find(begin, end, '.'); - } - - ip += util::from_chars(begin, end); - - return ip; - } - - // In the format "xxx.xxx.xxx.xxx/x" - std::pair - ip_block(const std::string_view &ip_str) { - auto begin = std::begin(ip_str); - auto end = std::find(begin, std::end(ip_str), '/'); - - auto addr = ip({ begin, (std::size_t)(end - begin) }); - - auto bits = 32 - util::from_chars(end + 1, std::end(ip_str)); - - return { addr, addr + ((1 << bits) - 1) }; - } + std::vector pc_ips_v6 { + ip::make_network_v6("::1/128"sv), + }; + std::vector lan_ips_v6 { + ip::make_network_v6("fc00::/7"sv), + ip::make_network_v6("fe80::/64"sv), + }; net_e from_enum_string(const std::string_view &view) { @@ -67,19 +44,35 @@ namespace net { return PC; } + net_e from_address(const std::string_view &view) { - auto addr = ip(view); + auto addr = normalize_address(ip::make_address(view)); - for (auto [ip_low, ip_high] : pc_ips) { - if (addr >= ip_low && addr <= ip_high) { - return PC; + if (addr.is_v6()) { + for (auto &range : pc_ips_v6) { + if (range.hosts().find(addr.to_v6()) != range.hosts().end()) { + return PC; + } + } + + for (auto &range : lan_ips_v6) { + if (range.hosts().find(addr.to_v6()) != range.hosts().end()) { + return LAN; + } } } + else { + for (auto &range : pc_ips_v4) { + if (range.hosts().find(addr.to_v4()) != range.hosts().end()) { + return PC; + } + } - for (auto [ip_low, ip_high] : lan_ips) { - if (addr >= ip_low && addr <= ip_high) { - return LAN; + for (auto &range : lan_ips_v4) { + if (range.hosts().find(addr.to_v4()) != range.hosts().end()) { + return LAN; + } } } @@ -101,12 +94,124 @@ namespace net { return "wan"sv; } + /** + * @brief Returns the `af_e` enum value for the `address_family` config option value. + * @param view The config option value. + * @return The `af_e` enum value. + */ + af_e + af_from_enum_string(const std::string_view &view) { + if (view == "ipv4") { + return IPV4; + } + if (view == "both") { + return BOTH; + } + + // avoid warning + return BOTH; + } + + /** + * @brief Returns the wildcard binding address for a given address family. + * @param af Address family. + * @return Normalized address. + */ + std::string_view + af_to_any_address_string(af_e af) { + switch (af) { + case IPV4: + return "0.0.0.0"sv; + case BOTH: + return "::"sv; + } + + // avoid warning + return "::"sv; + } + + /** + * @brief Converts an address to a normalized form. + * @details Normalization converts IPv4-mapped IPv6 addresses into IPv4 addresses. + * @param address The address to normalize. + * @return Normalized address. + */ + boost::asio::ip::address + normalize_address(boost::asio::ip::address address) { + // Convert IPv6-mapped IPv4 addresses into regular IPv4 addresses + if (address.is_v6()) { + auto v6 = address.to_v6(); + if (v6.is_v4_mapped()) { + return boost::asio::ip::make_address_v4(boost::asio::ip::v4_mapped, v6); + } + } + + return address; + } + + /** + * @brief Returns the given address in normalized string form. + * @details Normalization converts IPv4-mapped IPv6 addresses into IPv4 addresses. + * @param address The address to normalize. + * @return Normalized address in string form. + */ + std::string + addr_to_normalized_string(boost::asio::ip::address address) { + return normalize_address(address).to_string(); + } + + /** + * @brief Returns the given address in a normalized form for in the host portion of a URL. + * @details Normalization converts IPv4-mapped IPv6 addresses into IPv4 addresses. + * @param address The address to normalize and escape. + * @return Normalized address in URL-escaped string. + */ + std::string + addr_to_url_escaped_string(boost::asio::ip::address address) { + address = normalize_address(address); + if (address.is_v6()) { + std::stringstream ss; + ss << '[' << address.to_string() << ']'; + return ss.str(); + } + else { + return address.to_string(); + } + } + + /** + * @brief Returns the encryption mode for the given remote endpoint address. + * @param address The address used to look up the desired encryption mode. + * @return The WAN or LAN encryption mode, based on the provided address. + */ + int + encryption_mode_for_address(boost::asio::ip::address address) { + auto nettype = net::from_address(address.to_string()); + if (nettype == net::net_e::PC || nettype == net::net_e::LAN) { + return config::stream.lan_encryption_mode; + } + else { + return config::stream.wan_encryption_mode; + } + } + host_t - host_create(ENetAddress &addr, std::size_t peers, std::uint16_t port) { - enet_address_set_host(&addr, "0.0.0.0"); + host_create(af_e af, ENetAddress &addr, std::size_t peers, std::uint16_t port) { + static std::once_flag enet_init_flag; + std::call_once(enet_init_flag, []() { + enet_initialize(); + }); + + auto any_addr = net::af_to_any_address_string(af); + enet_address_set_host(&addr, any_addr.data()); enet_address_set_port(&addr, port); - return host_t { enet_host_create(AF_INET, &addr, peers, 1, 0, 0) }; + auto host = host_t { enet_host_create(af == IPV4 ? AF_INET : AF_INET6, &addr, peers, 0, 0, 0) }; + + // Enable opportunistic QoS tagging (automatically disables if the network appears to drop tagged packets) + enet_socket_set_option(host->socket, ENET_SOCKOPT_QOS, 1); + + return host; } void @@ -121,4 +226,29 @@ namespace net { enet_host_destroy(host); } + + /** + * @brief Map a specified port based on the base port. + * @param port The port to map as a difference from the base port. + * @return `std:uint16_t` : The mapped port number. + * + * EXAMPLES: + * ```cpp + * std::uint16_t mapped_port = net::map_port(1); + * ``` + */ + std::uint16_t + map_port(int port) { + // calculate the port from the config port + auto mapped_port = (std::uint16_t)((int) config::sunshine.port + port); + + // Ensure port is in the range of 1024-65535 + if (mapped_port < 1024 || mapped_port > 65535) { + BOOST_LOG(warning) << "Port out of range: "sv << mapped_port; + } + + // TODO: Ensure port is not already in use by another application + + return mapped_port; + } } // namespace net diff --git a/src/network.h b/src/network.h index e1ca36c7531..5fe842e7c8b 100644 --- a/src/network.h +++ b/src/network.h @@ -5,6 +5,9 @@ #pragma once #include +#include + +#include #include @@ -14,6 +17,9 @@ namespace net { void free_host(ENetHost *host); + std::uint16_t + map_port(int port); + using host_t = util::safe_ptr; using peer_t = ENetPeer *; using packet_t = util::safe_ptr; @@ -24,6 +30,11 @@ namespace net { WAN }; + enum af_e : int { + IPV4, + BOTH + }; + net_e from_enum_string(const std::string_view &view); std::string_view @@ -33,5 +44,56 @@ namespace net { from_address(const std::string_view &view); host_t - host_create(ENetAddress &addr, std::size_t peers, std::uint16_t port); + host_create(af_e af, ENetAddress &addr, std::size_t peers, std::uint16_t port); + + /** + * @brief Returns the `af_e` enum value for the `address_family` config option value. + * @param view The config option value. + * @return The `af_e` enum value. + */ + af_e + af_from_enum_string(const std::string_view &view); + + /** + * @brief Returns the wildcard binding address for a given address family. + * @param af Address family. + * @return Normalized address. + */ + std::string_view + af_to_any_address_string(af_e af); + + /** + * @brief Converts an address to a normalized form. + * @details Normalization converts IPv4-mapped IPv6 addresses into IPv4 addresses. + * @param address The address to normalize. + * @return Normalized address. + */ + boost::asio::ip::address + normalize_address(boost::asio::ip::address address); + + /** + * @brief Returns the given address in normalized string form. + * @details Normalization converts IPv4-mapped IPv6 addresses into IPv4 addresses. + * @param address The address to normalize. + * @return Normalized address in string form. + */ + std::string + addr_to_normalized_string(boost::asio::ip::address address); + + /** + * @brief Returns the given address in a normalized form for in the host portion of a URL. + * @details Normalization converts IPv4-mapped IPv6 addresses into IPv4 addresses. + * @param address The address to normalize and escape. + * @return Normalized address in URL-escaped string. + */ + std::string + addr_to_url_escaped_string(boost::asio::ip::address address); + + /** + * @brief Returns the encryption mode for the given remote endpoint address. + * @param address The address used to look up the desired encryption mode. + * @return The WAN or LAN encryption mode, based on the provided address. + */ + int + encryption_mode_for_address(boost::asio::ip::address address); } // namespace net diff --git a/src/nvenc/nvenc_base.cpp b/src/nvenc/nvenc_base.cpp new file mode 100644 index 00000000000..b9eba5a04df --- /dev/null +++ b/src/nvenc/nvenc_base.cpp @@ -0,0 +1,627 @@ +#include "nvenc_base.h" + +#include "src/config.h" +#include "src/logging.h" +#include "src/utility.h" + +#define MAKE_NVENC_VER(major, minor) ((major) | ((minor) << 24)) + +// Make sure we check backwards compatibility when bumping the Video Codec SDK version +// Things to look out for: +// - NV_ENC_*_VER definitions where the value inside NVENCAPI_STRUCT_VERSION() was increased +// - Incompatible struct changes in nvEncodeAPI.h (fields removed, semantics changed, etc.) +// - Test both old and new drivers with all supported codecs +#if NVENCAPI_VERSION != MAKE_NVENC_VER(12U, 0U) + #error Check and update NVENC code for backwards compatibility! +#endif + +namespace { + + GUID + quality_preset_guid_from_number(unsigned number) { + if (number > 7) number = 7; + + switch (number) { + case 1: + default: + return NV_ENC_PRESET_P1_GUID; + + case 2: + return NV_ENC_PRESET_P2_GUID; + + case 3: + return NV_ENC_PRESET_P3_GUID; + + case 4: + return NV_ENC_PRESET_P4_GUID; + + case 5: + return NV_ENC_PRESET_P5_GUID; + + case 6: + return NV_ENC_PRESET_P6_GUID; + + case 7: + return NV_ENC_PRESET_P7_GUID; + } + }; + + bool + equal_guids(const GUID &guid1, const GUID &guid2) { + return std::memcmp(&guid1, &guid2, sizeof(GUID)) == 0; + } + + auto + quality_preset_string_from_guid(const GUID &guid) { + if (equal_guids(guid, NV_ENC_PRESET_P1_GUID)) { + return "P1"; + } + if (equal_guids(guid, NV_ENC_PRESET_P2_GUID)) { + return "P2"; + } + if (equal_guids(guid, NV_ENC_PRESET_P3_GUID)) { + return "P3"; + } + if (equal_guids(guid, NV_ENC_PRESET_P4_GUID)) { + return "P4"; + } + if (equal_guids(guid, NV_ENC_PRESET_P5_GUID)) { + return "P5"; + } + if (equal_guids(guid, NV_ENC_PRESET_P6_GUID)) { + return "P6"; + } + if (equal_guids(guid, NV_ENC_PRESET_P7_GUID)) { + return "P7"; + } + return "Unknown"; + } + +} // namespace + +namespace nvenc { + + nvenc_base::nvenc_base(NV_ENC_DEVICE_TYPE device_type, void *device): + device_type(device_type), + device(device) { + } + + nvenc_base::~nvenc_base() { + // Use destroy_encoder() instead + } + + bool + nvenc_base::create_encoder(const nvenc_config &config, const video::config_t &client_config, const nvenc_colorspace_t &colorspace, NV_ENC_BUFFER_FORMAT buffer_format) { + // Pick the minimum NvEncode API version required to support the specified codec + // to maximize driver compatibility. AV1 was introduced in SDK v12.0. + minimum_api_version = (client_config.videoFormat <= 1) ? MAKE_NVENC_VER(11U, 0U) : MAKE_NVENC_VER(12U, 0U); + + if (!nvenc && !init_library()) return false; + + if (encoder) destroy_encoder(); + auto fail_guard = util::fail_guard([this] { destroy_encoder(); }); + + encoder_params.width = client_config.width; + encoder_params.height = client_config.height; + encoder_params.buffer_format = buffer_format; + encoder_params.rfi = true; + + NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS session_params = { min_struct_version(NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS_VER) }; + session_params.device = device; + session_params.deviceType = device_type; + session_params.apiVersion = minimum_api_version; + if (nvenc_failed(nvenc->nvEncOpenEncodeSessionEx(&session_params, &encoder))) { + BOOST_LOG(error) << "NvEncOpenEncodeSessionEx failed: " << last_error_string; + return false; + } + + uint32_t encode_guid_count = 0; + if (nvenc_failed(nvenc->nvEncGetEncodeGUIDCount(encoder, &encode_guid_count))) { + BOOST_LOG(error) << "NvEncGetEncodeGUIDCount failed: " << last_error_string; + return false; + }; + + std::vector encode_guids(encode_guid_count); + if (nvenc_failed(nvenc->nvEncGetEncodeGUIDs(encoder, encode_guids.data(), encode_guids.size(), &encode_guid_count))) { + BOOST_LOG(error) << "NvEncGetEncodeGUIDs failed: " << last_error_string; + return false; + } + + NV_ENC_INITIALIZE_PARAMS init_params = { min_struct_version(NV_ENC_INITIALIZE_PARAMS_VER) }; + + switch (client_config.videoFormat) { + case 0: + // H.264 + init_params.encodeGUID = NV_ENC_CODEC_H264_GUID; + break; + + case 1: + // HEVC + init_params.encodeGUID = NV_ENC_CODEC_HEVC_GUID; + break; + + case 2: + // AV1 + init_params.encodeGUID = NV_ENC_CODEC_AV1_GUID; + break; + + default: + BOOST_LOG(error) << "NvEnc: unknown video format " << client_config.videoFormat; + return false; + } + + { + auto search_predicate = [&](const GUID &guid) { + return equal_guids(init_params.encodeGUID, guid); + }; + if (std::find_if(encode_guids.begin(), encode_guids.end(), search_predicate) == encode_guids.end()) { + BOOST_LOG(error) << "NvEnc: encoding format is not supported by the gpu"; + return false; + } + } + + auto get_encoder_cap = [&](NV_ENC_CAPS cap) { + NV_ENC_CAPS_PARAM param = { min_struct_version(NV_ENC_CAPS_PARAM_VER), cap }; + int value = 0; + nvenc->nvEncGetEncodeCaps(encoder, init_params.encodeGUID, ¶m, &value); + return value; + }; + + auto buffer_is_10bit = [&]() { + return buffer_format == NV_ENC_BUFFER_FORMAT_YUV420_10BIT || buffer_format == NV_ENC_BUFFER_FORMAT_YUV444_10BIT; + }; + + auto buffer_is_yuv444 = [&]() { + return buffer_format == NV_ENC_BUFFER_FORMAT_YUV444 || buffer_format == NV_ENC_BUFFER_FORMAT_YUV444_10BIT; + }; + + { + auto supported_width = get_encoder_cap(NV_ENC_CAPS_WIDTH_MAX); + auto supported_height = get_encoder_cap(NV_ENC_CAPS_HEIGHT_MAX); + if (encoder_params.width > supported_width || encoder_params.height > supported_height) { + BOOST_LOG(error) << "NvEnc: gpu max encode resolution " << supported_width << "x" << supported_height << ", requested " << encoder_params.width << "x" << encoder_params.height; + return false; + } + } + + if (buffer_is_10bit() && !get_encoder_cap(NV_ENC_CAPS_SUPPORT_10BIT_ENCODE)) { + BOOST_LOG(error) << "NvEnc: gpu doesn't support 10-bit encode"; + return false; + } + + if (buffer_is_yuv444() && !get_encoder_cap(NV_ENC_CAPS_SUPPORT_YUV444_ENCODE)) { + BOOST_LOG(error) << "NvEnc: gpu doesn't support YUV444 encode"; + return false; + } + + if (async_event_handle && !get_encoder_cap(NV_ENC_CAPS_ASYNC_ENCODE_SUPPORT)) { + BOOST_LOG(warning) << "NvEnc: gpu doesn't support async encode"; + async_event_handle = nullptr; + } + + encoder_params.rfi = get_encoder_cap(NV_ENC_CAPS_SUPPORT_REF_PIC_INVALIDATION); + + init_params.presetGUID = quality_preset_guid_from_number(config.quality_preset); + init_params.tuningInfo = NV_ENC_TUNING_INFO_ULTRA_LOW_LATENCY; + init_params.enablePTD = 1; + init_params.enableEncodeAsync = async_event_handle ? 1 : 0; + init_params.enableWeightedPrediction = config.weighted_prediction && get_encoder_cap(NV_ENC_CAPS_SUPPORT_WEIGHTED_PREDICTION); + + init_params.encodeWidth = encoder_params.width; + init_params.darWidth = encoder_params.width; + init_params.encodeHeight = encoder_params.height; + init_params.darHeight = encoder_params.height; + init_params.frameRateNum = client_config.framerate; + init_params.frameRateDen = 1; + + NV_ENC_PRESET_CONFIG preset_config = { min_struct_version(NV_ENC_PRESET_CONFIG_VER), { min_struct_version(NV_ENC_CONFIG_VER, 7, 8) } }; + if (nvenc_failed(nvenc->nvEncGetEncodePresetConfigEx(encoder, init_params.encodeGUID, init_params.presetGUID, init_params.tuningInfo, &preset_config))) { + BOOST_LOG(error) << "NvEncGetEncodePresetConfigEx failed: " << last_error_string; + return false; + } + + NV_ENC_CONFIG enc_config = preset_config.presetCfg; + enc_config.profileGUID = NV_ENC_CODEC_PROFILE_AUTOSELECT_GUID; + enc_config.gopLength = NVENC_INFINITE_GOPLENGTH; + enc_config.frameIntervalP = 1; + enc_config.rcParams.enableAQ = config.adaptive_quantization; + enc_config.rcParams.rateControlMode = NV_ENC_PARAMS_RC_CBR; + enc_config.rcParams.zeroReorderDelay = 1; + enc_config.rcParams.enableLookahead = 0; + enc_config.rcParams.lowDelayKeyFrameScale = 1; + enc_config.rcParams.multiPass = config.two_pass == nvenc_two_pass::quarter_resolution ? NV_ENC_TWO_PASS_QUARTER_RESOLUTION : + config.two_pass == nvenc_two_pass::full_resolution ? NV_ENC_TWO_PASS_FULL_RESOLUTION : + NV_ENC_MULTI_PASS_DISABLED; + + enc_config.rcParams.enableAQ = config.adaptive_quantization; + enc_config.rcParams.averageBitRate = client_config.bitrate * 1000; + + if (get_encoder_cap(NV_ENC_CAPS_SUPPORT_CUSTOM_VBV_BUF_SIZE)) { + enc_config.rcParams.vbvBufferSize = client_config.bitrate * 1000 / client_config.framerate; + if (config.vbv_percentage_increase > 0) { + enc_config.rcParams.vbvBufferSize += enc_config.rcParams.vbvBufferSize * config.vbv_percentage_increase / 100; + } + } + + auto set_h264_hevc_common_format_config = [&](auto &format_config) { + format_config.repeatSPSPPS = 1; + format_config.idrPeriod = NVENC_INFINITE_GOPLENGTH; + format_config.sliceMode = 3; + format_config.sliceModeData = client_config.slicesPerFrame; + if (buffer_is_yuv444()) { + format_config.chromaFormatIDC = 3; + } + format_config.enableFillerDataInsertion = config.insert_filler_data; + }; + + auto set_ref_frames = [&](uint32_t &ref_frames_option, NV_ENC_NUM_REF_FRAMES &L0_option, uint32_t ref_frames_default) { + if (client_config.numRefFrames > 0) { + ref_frames_option = client_config.numRefFrames; + } + else { + ref_frames_option = ref_frames_default; + } + if (ref_frames_option > 0 && !get_encoder_cap(NV_ENC_CAPS_SUPPORT_MULTIPLE_REF_FRAMES)) { + ref_frames_option = 1; + encoder_params.rfi = false; + } + encoder_params.ref_frames_in_dpb = ref_frames_option; + // This limits ref frames any frame can use to 1, but allows larger buffer size for fallback if some frames are invalidated through rfi + L0_option = NV_ENC_NUM_REF_FRAMES_1; + }; + + auto set_minqp_if_enabled = [&](int value) { + if (config.enable_min_qp) { + enc_config.rcParams.enableMinQP = 1; + enc_config.rcParams.minQP.qpInterP = value; + enc_config.rcParams.minQP.qpIntra = value; + } + }; + + auto fill_h264_hevc_vui = [&colorspace](auto &vui_config) { + vui_config.videoSignalTypePresentFlag = 1; + vui_config.videoFormat = NV_ENC_VUI_VIDEO_FORMAT_UNSPECIFIED; + vui_config.videoFullRangeFlag = colorspace.full_range; + vui_config.colourDescriptionPresentFlag = 1; + vui_config.colourPrimaries = colorspace.primaries; + vui_config.transferCharacteristics = colorspace.tranfer_function; + vui_config.colourMatrix = colorspace.matrix; + vui_config.chromaSampleLocationFlag = 1; + vui_config.chromaSampleLocationTop = 0; + vui_config.chromaSampleLocationBot = 0; + }; + + switch (client_config.videoFormat) { + case 0: { + // H.264 + enc_config.profileGUID = buffer_is_yuv444() ? NV_ENC_H264_PROFILE_HIGH_444_GUID : NV_ENC_H264_PROFILE_HIGH_GUID; + auto &format_config = enc_config.encodeCodecConfig.h264Config; + set_h264_hevc_common_format_config(format_config); + if (config.h264_cavlc || !get_encoder_cap(NV_ENC_CAPS_SUPPORT_CABAC)) { + format_config.entropyCodingMode = NV_ENC_H264_ENTROPY_CODING_MODE_CAVLC; + } + else { + format_config.entropyCodingMode = NV_ENC_H264_ENTROPY_CODING_MODE_CABAC; + } + set_ref_frames(format_config.maxNumRefFrames, format_config.numRefL0, 5); + set_minqp_if_enabled(config.min_qp_h264); + fill_h264_hevc_vui(format_config.h264VUIParameters); + break; + } + + case 1: { + // HEVC + auto &format_config = enc_config.encodeCodecConfig.hevcConfig; + set_h264_hevc_common_format_config(format_config); + if (buffer_is_10bit()) { + format_config.pixelBitDepthMinus8 = 2; + } + set_ref_frames(format_config.maxNumRefFramesInDPB, format_config.numRefL0, 5); + set_minqp_if_enabled(config.min_qp_hevc); + fill_h264_hevc_vui(format_config.hevcVUIParameters); + break; + } + + case 2: { + // AV1 + auto &format_config = enc_config.encodeCodecConfig.av1Config; + format_config.repeatSeqHdr = 1; + format_config.idrPeriod = NVENC_INFINITE_GOPLENGTH; + format_config.chromaFormatIDC = 1; // YUV444 not supported by NVENC yet + format_config.enableBitstreamPadding = config.insert_filler_data; + if (buffer_is_10bit()) { + format_config.inputPixelBitDepthMinus8 = 2; + format_config.pixelBitDepthMinus8 = 2; + } + format_config.colorPrimaries = colorspace.primaries; + format_config.transferCharacteristics = colorspace.tranfer_function; + format_config.matrixCoefficients = colorspace.matrix; + format_config.colorRange = colorspace.full_range; + format_config.chromaSamplePosition = 1; + set_ref_frames(format_config.maxNumRefFramesInDPB, format_config.numFwdRefs, 8); + set_minqp_if_enabled(config.min_qp_av1); + + if (client_config.slicesPerFrame > 1) { + // NVENC only supports slice counts that are powers of two, so we'll pick powers of two + // with bias to rows due to hopefully more similar macroblocks with a row vs a column. + format_config.numTileRows = std::pow(2, std::ceil(std::log2(client_config.slicesPerFrame) / 2)); + format_config.numTileColumns = std::pow(2, std::floor(std::log2(client_config.slicesPerFrame) / 2)); + } + break; + } + } + + init_params.encodeConfig = &enc_config; + + if (nvenc_failed(nvenc->nvEncInitializeEncoder(encoder, &init_params))) { + BOOST_LOG(error) << "NvEncInitializeEncoder failed: " << last_error_string; + return false; + } + + if (async_event_handle) { + NV_ENC_EVENT_PARAMS event_params = { min_struct_version(NV_ENC_EVENT_PARAMS_VER) }; + event_params.completionEvent = async_event_handle; + if (nvenc_failed(nvenc->nvEncRegisterAsyncEvent(encoder, &event_params))) { + BOOST_LOG(error) << "NvEncRegisterAsyncEvent failed: " << last_error_string; + return false; + } + } + + NV_ENC_CREATE_BITSTREAM_BUFFER create_bitstream_buffer = { min_struct_version(NV_ENC_CREATE_BITSTREAM_BUFFER_VER) }; + if (nvenc_failed(nvenc->nvEncCreateBitstreamBuffer(encoder, &create_bitstream_buffer))) { + BOOST_LOG(error) << "NvEncCreateBitstreamBuffer failed: " << last_error_string; + return false; + } + output_bitstream = create_bitstream_buffer.bitstreamBuffer; + + if (!create_and_register_input_buffer()) { + return false; + } + + { + auto f = stat_trackers::one_digit_after_decimal(); + BOOST_LOG(debug) << "NvEnc: requested encoded frame size " << f % (client_config.bitrate / 8. / client_config.framerate) << " kB"; + } + + { + std::string extra; + if (init_params.enableEncodeAsync) extra += " async"; + if (buffer_is_10bit()) extra += " 10-bit"; + if (enc_config.rcParams.multiPass != NV_ENC_MULTI_PASS_DISABLED) extra += " two-pass"; + if (config.vbv_percentage_increase > 0 && get_encoder_cap(NV_ENC_CAPS_SUPPORT_CUSTOM_VBV_BUF_SIZE)) extra += " vbv+" + std::to_string(config.vbv_percentage_increase); + if (encoder_params.rfi) extra += " rfi"; + if (init_params.enableWeightedPrediction) extra += " weighted-prediction"; + if (enc_config.rcParams.enableAQ) extra += " spatial-aq"; + if (enc_config.rcParams.enableMinQP) extra += " qpmin=" + std::to_string(enc_config.rcParams.minQP.qpInterP); + if (config.insert_filler_data) extra += " filler-data"; + BOOST_LOG(info) << "NvEnc: created encoder " << quality_preset_string_from_guid(init_params.presetGUID) << extra; + } + + encoder_state = {}; + fail_guard.disable(); + return true; + } + + void + nvenc_base::destroy_encoder() { + if (output_bitstream) { + nvenc->nvEncDestroyBitstreamBuffer(encoder, output_bitstream); + output_bitstream = nullptr; + } + if (encoder && async_event_handle) { + NV_ENC_EVENT_PARAMS event_params = { min_struct_version(NV_ENC_EVENT_PARAMS_VER) }; + event_params.completionEvent = async_event_handle; + nvenc->nvEncUnregisterAsyncEvent(encoder, &event_params); + } + if (registered_input_buffer) { + nvenc->nvEncUnregisterResource(encoder, registered_input_buffer); + registered_input_buffer = nullptr; + } + if (encoder) { + nvenc->nvEncDestroyEncoder(encoder); + encoder = nullptr; + } + + encoder_state = {}; + encoder_params = {}; + } + + nvenc_encoded_frame + nvenc_base::encode_frame(uint64_t frame_index, bool force_idr) { + if (!encoder) { + return {}; + } + + assert(registered_input_buffer); + assert(output_bitstream); + + NV_ENC_MAP_INPUT_RESOURCE mapped_input_buffer = { min_struct_version(NV_ENC_MAP_INPUT_RESOURCE_VER) }; + mapped_input_buffer.registeredResource = registered_input_buffer; + + if (nvenc_failed(nvenc->nvEncMapInputResource(encoder, &mapped_input_buffer))) { + BOOST_LOG(error) << "NvEncMapInputResource failed: " << last_error_string; + return {}; + } + auto unmap_guard = util::fail_guard([&] { nvenc->nvEncUnmapInputResource(encoder, &mapped_input_buffer); }); + + NV_ENC_PIC_PARAMS pic_params = { min_struct_version(NV_ENC_PIC_PARAMS_VER, 4, 6) }; + pic_params.inputWidth = encoder_params.width; + pic_params.inputHeight = encoder_params.height; + pic_params.encodePicFlags = force_idr ? NV_ENC_PIC_FLAG_FORCEIDR : 0; + pic_params.inputTimeStamp = frame_index; + pic_params.pictureStruct = NV_ENC_PIC_STRUCT_FRAME; + pic_params.inputBuffer = mapped_input_buffer.mappedResource; + pic_params.bufferFmt = mapped_input_buffer.mappedBufferFmt; + pic_params.outputBitstream = output_bitstream; + pic_params.completionEvent = async_event_handle; + + if (nvenc_failed(nvenc->nvEncEncodePicture(encoder, &pic_params))) { + BOOST_LOG(error) << "NvEncEncodePicture failed: " << last_error_string; + return {}; + } + + NV_ENC_LOCK_BITSTREAM lock_bitstream = { min_struct_version(NV_ENC_LOCK_BITSTREAM_VER, 1, 2) }; + lock_bitstream.outputBitstream = output_bitstream; + lock_bitstream.doNotWait = 0; + + if (async_event_handle && !wait_for_async_event(100)) { + BOOST_LOG(error) << "NvEnc: frame " << frame_index << " encode wait timeout"; + return {}; + } + + if (nvenc_failed(nvenc->nvEncLockBitstream(encoder, &lock_bitstream))) { + BOOST_LOG(error) << "NvEncLockBitstream failed: " << last_error_string; + return {}; + } + + auto data_pointer = (uint8_t *) lock_bitstream.bitstreamBufferPtr; + nvenc_encoded_frame encoded_frame { + { data_pointer, data_pointer + lock_bitstream.bitstreamSizeInBytes }, + lock_bitstream.outputTimeStamp, + lock_bitstream.pictureType == NV_ENC_PIC_TYPE_IDR, + encoder_state.rfi_needs_confirmation, + }; + + if (encoder_state.rfi_needs_confirmation) { + // Invalidation request has been fulfilled, and video network packet will be marked as such + encoder_state.rfi_needs_confirmation = false; + } + + encoder_state.last_encoded_frame_index = frame_index; + + if (encoded_frame.idr) { + BOOST_LOG(debug) << "NvEnc: idr frame " << encoded_frame.frame_index; + } + + if (nvenc_failed(nvenc->nvEncUnlockBitstream(encoder, lock_bitstream.outputBitstream))) { + BOOST_LOG(error) << "NvEncUnlockBitstream failed: " << last_error_string; + } + + if (config::sunshine.min_log_level <= 1) { + // Print encoded frame size stats to debug log every 20 seconds + auto callback = [&](float stat_min, float stat_max, double stat_avg) { + auto f = stat_trackers::one_digit_after_decimal(); + BOOST_LOG(debug) << "NvEnc: encoded frame sizes (min max avg) " << f % stat_min << " " << f % stat_max << " " << f % stat_avg << " kB"; + }; + using namespace std::literals; + encoder_state.frame_size_tracker.collect_and_callback_on_interval(encoded_frame.data.size() / 1000., callback, 20s); + } + + return encoded_frame; + } + + bool + nvenc_base::invalidate_ref_frames(uint64_t first_frame, uint64_t last_frame) { + if (!encoder || !encoder_params.rfi) return false; + + if (first_frame >= encoder_state.last_rfi_range.first && + last_frame <= encoder_state.last_rfi_range.second) { + BOOST_LOG(debug) << "NvEnc: rfi request " << first_frame << "-" << last_frame << " already done"; + return true; + } + + encoder_state.rfi_needs_confirmation = true; + + if (last_frame < first_frame) { + BOOST_LOG(error) << "NvEnc: invaid rfi request " << first_frame << "-" << last_frame << ", generating IDR"; + return false; + } + + BOOST_LOG(debug) << "NvEnc: rfi request " << first_frame << "-" << last_frame << " expanding to last encoded frame " << encoder_state.last_encoded_frame_index; + last_frame = encoder_state.last_encoded_frame_index; + + encoder_state.last_rfi_range = { first_frame, last_frame }; + + if (last_frame - first_frame + 1 >= encoder_params.ref_frames_in_dpb) { + BOOST_LOG(debug) << "NvEnc: rfi request too large, generating IDR"; + return false; + } + + for (auto i = first_frame; i <= last_frame; i++) { + if (nvenc_failed(nvenc->nvEncInvalidateRefFrames(encoder, i))) { + BOOST_LOG(error) << "NvEncInvalidateRefFrames " << i << " failed: " << last_error_string; + return false; + } + } + + return true; + } + + bool + nvenc_base::nvenc_failed(NVENCSTATUS status) { + auto status_string = [](NVENCSTATUS status) -> std::string { + switch (status) { +#define nvenc_status_case(x) \ + case x: \ + return #x; + nvenc_status_case(NV_ENC_SUCCESS); + nvenc_status_case(NV_ENC_ERR_NO_ENCODE_DEVICE); + nvenc_status_case(NV_ENC_ERR_UNSUPPORTED_DEVICE); + nvenc_status_case(NV_ENC_ERR_INVALID_ENCODERDEVICE); + nvenc_status_case(NV_ENC_ERR_INVALID_DEVICE); + nvenc_status_case(NV_ENC_ERR_DEVICE_NOT_EXIST); + nvenc_status_case(NV_ENC_ERR_INVALID_PTR); + nvenc_status_case(NV_ENC_ERR_INVALID_EVENT); + nvenc_status_case(NV_ENC_ERR_INVALID_PARAM); + nvenc_status_case(NV_ENC_ERR_INVALID_CALL); + nvenc_status_case(NV_ENC_ERR_OUT_OF_MEMORY); + nvenc_status_case(NV_ENC_ERR_ENCODER_NOT_INITIALIZED); + nvenc_status_case(NV_ENC_ERR_UNSUPPORTED_PARAM); + nvenc_status_case(NV_ENC_ERR_LOCK_BUSY); + nvenc_status_case(NV_ENC_ERR_NOT_ENOUGH_BUFFER); + nvenc_status_case(NV_ENC_ERR_INVALID_VERSION); + nvenc_status_case(NV_ENC_ERR_MAP_FAILED); + nvenc_status_case(NV_ENC_ERR_NEED_MORE_INPUT); + nvenc_status_case(NV_ENC_ERR_ENCODER_BUSY); + nvenc_status_case(NV_ENC_ERR_EVENT_NOT_REGISTERD); + nvenc_status_case(NV_ENC_ERR_GENERIC); + nvenc_status_case(NV_ENC_ERR_INCOMPATIBLE_CLIENT_KEY); + nvenc_status_case(NV_ENC_ERR_UNIMPLEMENTED); + nvenc_status_case(NV_ENC_ERR_RESOURCE_REGISTER_FAILED); + nvenc_status_case(NV_ENC_ERR_RESOURCE_NOT_REGISTERED); + nvenc_status_case(NV_ENC_ERR_RESOURCE_NOT_MAPPED); + // Newer versions of sdk may add more constants, look for them the end of NVENCSTATUS enum +#undef nvenc_status_case + default: + return std::to_string(status); + } + }; + + last_error_string.clear(); + if (status != NV_ENC_SUCCESS) { + if (nvenc && encoder) { + last_error_string = nvenc->nvEncGetLastErrorString(encoder); + if (!last_error_string.empty()) last_error_string += " "; + } + last_error_string += status_string(status); + return true; + } + + return false; + } + + /** + * @brief This function returns the corresponding struct version for the minimum API required by the codec. + * @details Reducing the struct versions maximizes driver compatibility by avoiding needless API breaks. + * @param version The raw structure version from `NVENCAPI_STRUCT_VERSION()`. + * @param v11_struct_version Optionally specifies the struct version to use with v11 SDK major versions. + * @param v12_struct_version Optionally specifies the struct version to use with v12 SDK major versions. + * @return A suitable struct version for the active codec. + */ + uint32_t + nvenc_base::min_struct_version(uint32_t version, uint32_t v11_struct_version, uint32_t v12_struct_version) { + assert(minimum_api_version); + + // Mask off and replace the original NVENCAPI_VERSION + version &= ~NVENCAPI_VERSION; + version |= minimum_api_version; + + // If there's a struct version override, apply that too + if (v11_struct_version || v12_struct_version) { + version &= ~(0xFFu << 16); + version |= (((minimum_api_version & 0xFF) >= 12) ? v12_struct_version : v11_struct_version) << 16; + } + + return version; + } +} // namespace nvenc diff --git a/src/nvenc/nvenc_base.h b/src/nvenc/nvenc_base.h new file mode 100644 index 00000000000..2d012ef8da8 --- /dev/null +++ b/src/nvenc/nvenc_base.h @@ -0,0 +1,92 @@ +#pragma once + +#include "nvenc_colorspace.h" +#include "nvenc_config.h" +#include "nvenc_encoded_frame.h" + +#include "src/stat_trackers.h" +#include "src/video.h" + +#include + +namespace nvenc { + + class nvenc_base { + public: + nvenc_base(NV_ENC_DEVICE_TYPE device_type, void *device); + virtual ~nvenc_base(); + + nvenc_base(const nvenc_base &) = delete; + nvenc_base & + operator=(const nvenc_base &) = delete; + + bool + create_encoder(const nvenc_config &config, const video::config_t &client_config, const nvenc_colorspace_t &colorspace, NV_ENC_BUFFER_FORMAT buffer_format); + + void + destroy_encoder(); + + nvenc_encoded_frame + encode_frame(uint64_t frame_index, bool force_idr); + + bool + invalidate_ref_frames(uint64_t first_frame, uint64_t last_frame); + + protected: + virtual bool + init_library() = 0; + + virtual bool + create_and_register_input_buffer() = 0; + + virtual bool + wait_for_async_event(uint32_t timeout_ms) { return false; } + + bool + nvenc_failed(NVENCSTATUS status); + + /** + * @brief This function returns the corresponding struct version for the minimum API required by the codec. + * @details Reducing the struct versions maximizes driver compatibility by avoiding needless API breaks. + * @param version The raw structure version from `NVENCAPI_STRUCT_VERSION()`. + * @param v11_struct_version Optionally specifies the struct version to use with v11 SDK major versions. + * @param v12_struct_version Optionally specifies the struct version to use with v12 SDK major versions. + * @return A suitable struct version for the active codec. + */ + uint32_t + min_struct_version(uint32_t version, uint32_t v11_struct_version = 0, uint32_t v12_struct_version = 0); + + const NV_ENC_DEVICE_TYPE device_type; + void *const device; + + std::unique_ptr nvenc; + + void *encoder = nullptr; + + struct { + uint32_t width = 0; + uint32_t height = 0; + NV_ENC_BUFFER_FORMAT buffer_format = NV_ENC_BUFFER_FORMAT_UNDEFINED; + uint32_t ref_frames_in_dpb = 0; + bool rfi = false; + } encoder_params; + + // Derived classes set these variables + NV_ENC_REGISTERED_PTR registered_input_buffer = nullptr; + void *async_event_handle = nullptr; + + std::string last_error_string; + + private: + NV_ENC_OUTPUT_PTR output_bitstream = nullptr; + uint32_t minimum_api_version = 0; + + struct { + uint64_t last_encoded_frame_index = 0; + bool rfi_needs_confirmation = false; + std::pair last_rfi_range; + stat_trackers::min_max_avg_tracker frame_size_tracker; + } encoder_state; + }; + +} // namespace nvenc diff --git a/src/nvenc/nvenc_colorspace.h b/src/nvenc/nvenc_colorspace.h new file mode 100644 index 00000000000..9ebcb10f479 --- /dev/null +++ b/src/nvenc/nvenc_colorspace.h @@ -0,0 +1,12 @@ +#pragma once + +#include + +namespace nvenc { + struct nvenc_colorspace_t { + NV_ENC_VUI_COLOR_PRIMARIES primaries; + NV_ENC_VUI_TRANSFER_CHARACTERISTIC tranfer_function; + NV_ENC_VUI_MATRIX_COEFFS matrix; + bool full_range; + }; +} // namespace nvenc diff --git a/src/nvenc/nvenc_config.h b/src/nvenc/nvenc_config.h new file mode 100644 index 00000000000..c4aae12a86e --- /dev/null +++ b/src/nvenc/nvenc_config.h @@ -0,0 +1,51 @@ +#pragma once + +namespace nvenc { + + enum class nvenc_two_pass { + // Single pass, the fastest and no extra vram + disabled, + + // Larger motion vectors being caught, faster and uses less extra vram + quarter_resolution, + + // Better overall statistics, slower and uses more extra vram + full_resolution, + }; + + struct nvenc_config { + // Quality preset from 1 to 7, higher is slower + int quality_preset = 1; + + // Use optional preliminary pass for better motion vectors, bitrate distribution and stricter VBV(HRD), uses CUDA cores + nvenc_two_pass two_pass = nvenc_two_pass::quarter_resolution; + + // Percentage increase of VBV/HRD from the default single frame, allows low-latency variable bitrate + int vbv_percentage_increase = 0; + + // Improves fades compression, uses CUDA cores + bool weighted_prediction = false; + + // Allocate more bitrate to flat regions since they're visually more perceptible, uses CUDA cores + bool adaptive_quantization = false; + + // Don't use QP below certain value, limits peak image quality to save bitrate + bool enable_min_qp = false; + + // Min QP value for H.264 when enable_min_qp is selected + unsigned min_qp_h264 = 19; + + // Min QP value for HEVC when enable_min_qp is selected + unsigned min_qp_hevc = 23; + + // Min QP value for AV1 when enable_min_qp is selected + unsigned min_qp_av1 = 23; + + // Use CAVLC entropy coding in H.264 instead of CABAC, not relevant and here for historical reasons + bool h264_cavlc = false; + + // Add filler data to encoded frames to stay at target bitrate, mainly for testing + bool insert_filler_data = false; + }; + +} // namespace nvenc diff --git a/src/nvenc/nvenc_d3d11.cpp b/src/nvenc/nvenc_d3d11.cpp new file mode 100644 index 00000000000..cb33a1801af --- /dev/null +++ b/src/nvenc/nvenc_d3d11.cpp @@ -0,0 +1,106 @@ +#include "src/logging.h" + +#ifdef _WIN32 + #include "nvenc_d3d11.h" + + #include "nvenc_utils.h" + +namespace nvenc { + + nvenc_d3d11::nvenc_d3d11(ID3D11Device *d3d_device): + nvenc_base(NV_ENC_DEVICE_TYPE_DIRECTX, d3d_device), + d3d_device(d3d_device) { + } + + nvenc_d3d11::~nvenc_d3d11() { + if (encoder) destroy_encoder(); + + if (dll) { + FreeLibrary(dll); + dll = NULL; + } + } + + ID3D11Texture2D * + nvenc_d3d11::get_input_texture() { + return d3d_input_texture.GetInterfacePtr(); + } + + bool + nvenc_d3d11::init_library() { + if (dll) return true; + + #ifdef _WIN64 + auto dll_name = "nvEncodeAPI64.dll"; + #else + auto dll_name = "nvEncodeAPI.dll"; + #endif + + if ((dll = LoadLibraryEx(dll_name, NULL, LOAD_LIBRARY_SEARCH_SYSTEM32))) { + if (auto create_instance = (decltype(NvEncodeAPICreateInstance) *) GetProcAddress(dll, "NvEncodeAPICreateInstance")) { + auto new_nvenc = std::make_unique(); + new_nvenc->version = min_struct_version(NV_ENCODE_API_FUNCTION_LIST_VER); + if (nvenc_failed(create_instance(new_nvenc.get()))) { + BOOST_LOG(error) << "NvEncodeAPICreateInstance failed: " << last_error_string; + } + else { + nvenc = std::move(new_nvenc); + return true; + } + } + else { + BOOST_LOG(error) << "No NvEncodeAPICreateInstance in " << dll_name; + } + } + else { + BOOST_LOG(debug) << "Couldn't load NvEnc library " << dll_name; + } + + if (dll) { + FreeLibrary(dll); + dll = NULL; + } + + return false; + } + + bool + nvenc_d3d11::create_and_register_input_buffer() { + if (!d3d_input_texture) { + D3D11_TEXTURE2D_DESC desc = {}; + desc.Width = encoder_params.width; + desc.Height = encoder_params.height; + desc.MipLevels = 1; + desc.ArraySize = 1; + desc.Format = dxgi_format_from_nvenc_format(encoder_params.buffer_format); + desc.SampleDesc.Count = 1; + desc.Usage = D3D11_USAGE_DEFAULT; + desc.BindFlags = D3D11_BIND_RENDER_TARGET; + if (d3d_device->CreateTexture2D(&desc, nullptr, &d3d_input_texture) != S_OK) { + BOOST_LOG(error) << "NvEnc: couldn't create input texture"; + return false; + } + } + + if (!registered_input_buffer) { + NV_ENC_REGISTER_RESOURCE register_resource = { min_struct_version(NV_ENC_REGISTER_RESOURCE_VER, 3, 4) }; + register_resource.resourceType = NV_ENC_INPUT_RESOURCE_TYPE_DIRECTX; + register_resource.width = encoder_params.width; + register_resource.height = encoder_params.height; + register_resource.resourceToRegister = d3d_input_texture.GetInterfacePtr(); + register_resource.bufferFormat = encoder_params.buffer_format; + register_resource.bufferUsage = NV_ENC_INPUT_IMAGE; + + if (nvenc_failed(nvenc->nvEncRegisterResource(encoder, ®ister_resource))) { + BOOST_LOG(error) << "NvEncRegisterResource failed: " << last_error_string; + return false; + } + + registered_input_buffer = register_resource.registeredResource; + } + + return true; + } + +} // namespace nvenc +#endif diff --git a/src/nvenc/nvenc_d3d11.h b/src/nvenc/nvenc_d3d11.h new file mode 100644 index 00000000000..ef1b8d4c232 --- /dev/null +++ b/src/nvenc/nvenc_d3d11.h @@ -0,0 +1,35 @@ +#pragma once +#ifdef _WIN32 + + #include + #include + + #include "nvenc_base.h" + +namespace nvenc { + + _COM_SMARTPTR_TYPEDEF(ID3D11Device, IID_ID3D11Device); + _COM_SMARTPTR_TYPEDEF(ID3D11Texture2D, IID_ID3D11Texture2D); + + class nvenc_d3d11 final: public nvenc_base { + public: + nvenc_d3d11(ID3D11Device *d3d_device); + ~nvenc_d3d11(); + + ID3D11Texture2D * + get_input_texture(); + + private: + bool + init_library() override; + + bool + create_and_register_input_buffer() override; + + HMODULE dll = NULL; + const ID3D11DevicePtr d3d_device; + ID3D11Texture2DPtr d3d_input_texture; + }; + +} // namespace nvenc +#endif diff --git a/src/nvenc/nvenc_encoded_frame.h b/src/nvenc/nvenc_encoded_frame.h new file mode 100644 index 00000000000..f60ba3023e7 --- /dev/null +++ b/src/nvenc/nvenc_encoded_frame.h @@ -0,0 +1,13 @@ +#pragma once + +#include +#include + +namespace nvenc { + struct nvenc_encoded_frame { + std::vector data; + uint64_t frame_index = 0; + bool idr = false; + bool after_ref_frame_invalidation = false; + }; +} // namespace nvenc diff --git a/src/nvenc/nvenc_utils.cpp b/src/nvenc/nvenc_utils.cpp new file mode 100644 index 00000000000..1b8b7ec9f10 --- /dev/null +++ b/src/nvenc/nvenc_utils.cpp @@ -0,0 +1,78 @@ +#include + +#include "nvenc_utils.h" + +namespace nvenc { + +#ifdef _WIN32 + DXGI_FORMAT + dxgi_format_from_nvenc_format(NV_ENC_BUFFER_FORMAT format) { + switch (format) { + case NV_ENC_BUFFER_FORMAT_YUV420_10BIT: + return DXGI_FORMAT_P010; + + case NV_ENC_BUFFER_FORMAT_NV12: + return DXGI_FORMAT_NV12; + + default: + return DXGI_FORMAT_UNKNOWN; + } + } +#endif + + NV_ENC_BUFFER_FORMAT + nvenc_format_from_sunshine_format(platf::pix_fmt_e format) { + switch (format) { + case platf::pix_fmt_e::nv12: + return NV_ENC_BUFFER_FORMAT_NV12; + + case platf::pix_fmt_e::p010: + return NV_ENC_BUFFER_FORMAT_YUV420_10BIT; + + default: + return NV_ENC_BUFFER_FORMAT_UNDEFINED; + } + } + + nvenc_colorspace_t + nvenc_colorspace_from_sunshine_colorspace(const video::sunshine_colorspace_t &sunshine_colorspace) { + nvenc_colorspace_t colorspace; + + switch (sunshine_colorspace.colorspace) { + case video::colorspace_e::rec601: + // Rec. 601 + colorspace.primaries = NV_ENC_VUI_COLOR_PRIMARIES_SMPTE170M; + colorspace.tranfer_function = NV_ENC_VUI_TRANSFER_CHARACTERISTIC_SMPTE170M; + colorspace.matrix = NV_ENC_VUI_MATRIX_COEFFS_SMPTE170M; + break; + + case video::colorspace_e::rec709: + // Rec. 709 + colorspace.primaries = NV_ENC_VUI_COLOR_PRIMARIES_BT709; + colorspace.tranfer_function = NV_ENC_VUI_TRANSFER_CHARACTERISTIC_BT709; + colorspace.matrix = NV_ENC_VUI_MATRIX_COEFFS_BT709; + break; + + case video::colorspace_e::bt2020sdr: + // Rec. 2020 + colorspace.primaries = NV_ENC_VUI_COLOR_PRIMARIES_BT2020; + assert(sunshine_colorspace.bit_depth == 10); + colorspace.tranfer_function = NV_ENC_VUI_TRANSFER_CHARACTERISTIC_BT2020_10; + colorspace.matrix = NV_ENC_VUI_MATRIX_COEFFS_BT2020_NCL; + break; + + case video::colorspace_e::bt2020: + // Rec. 2020 with ST 2084 perceptual quantizer + colorspace.primaries = NV_ENC_VUI_COLOR_PRIMARIES_BT2020; + assert(sunshine_colorspace.bit_depth == 10); + colorspace.tranfer_function = NV_ENC_VUI_TRANSFER_CHARACTERISTIC_SMPTE2084; + colorspace.matrix = NV_ENC_VUI_MATRIX_COEFFS_BT2020_NCL; + break; + } + + colorspace.full_range = sunshine_colorspace.full_range; + + return colorspace; + } + +} // namespace nvenc diff --git a/src/nvenc/nvenc_utils.h b/src/nvenc/nvenc_utils.h new file mode 100644 index 00000000000..67af1037618 --- /dev/null +++ b/src/nvenc/nvenc_utils.h @@ -0,0 +1,27 @@ +#pragma once + +#ifdef _WIN32 + #include +#endif + +#include "nvenc_colorspace.h" + +#include "src/platform/common.h" +#include "src/video_colorspace.h" + +#include + +namespace nvenc { + +#ifdef _WIN32 + DXGI_FORMAT + dxgi_format_from_nvenc_format(NV_ENC_BUFFER_FORMAT format); +#endif + + NV_ENC_BUFFER_FORMAT + nvenc_format_from_sunshine_format(platf::pix_fmt_e format); + + nvenc_colorspace_t + nvenc_colorspace_from_sunshine_colorspace(const video::sunshine_colorspace_t &sunshine_colorspace); + +} // namespace nvenc diff --git a/src/nvhttp.cpp b/src/nvhttp.cpp index dfa0b9fef76..bd8434e5534 100644 --- a/src/nvhttp.cpp +++ b/src/nvhttp.cpp @@ -8,6 +8,7 @@ // standard includes #include +#include // lib includes #include @@ -17,17 +18,21 @@ #include #include #include +#include // local includes #include "config.h" #include "crypto.h" +#include "file_handler.h" +#include "globals.h" #include "httpcommon.h" -#include "main.h" +#include "logging.h" #include "network.h" #include "nvhttp.h" #include "platform/common.h" #include "process.h" #include "rtsp.h" +#include "system_tray.h" #include "utility.h" #include "uuid.h" #include "video.h" @@ -38,6 +43,8 @@ namespace nvhttp { namespace fs = std::filesystem; namespace pt = boost::property_tree; + crypto::cert_chain_t cert_chain; + class SunshineHttpsServer: public SimpleWeb::Server { public: SunshineHttpsServer(const std::string &certification_file, const std::string &private_key_file): @@ -110,9 +117,15 @@ namespace nvhttp { std::string pkey; } conf_intern; + struct named_cert_t { + std::string name; + std::string uuid; + std::string cert; + }; + struct client_t { - std::string uniqueID; std::vector certs; + std::vector named_devices; }; struct pair_session_t { @@ -138,7 +151,8 @@ namespace nvhttp { // uniqueID, session std::unordered_map map_id_sess; - std::unordered_map map_id_client; + client_t client_root; + std::atomic session_id_counter; using args_t = SimpleWeb::CaseInsensitiveMultimap; using resp_https_t = std::shared_ptr::Response>; @@ -152,9 +166,13 @@ namespace nvhttp { }; std::string - get_arg(const args_t &args, const char *name) { + get_arg(const args_t &args, const char *name, const char *default_value = nullptr) { auto it = args.find(name); if (it == std::end(args)) { + if (default_value != NULL) { + return std::string(default_value); + } + throw std::out_of_range(name); } return it->second; @@ -177,22 +195,18 @@ namespace nvhttp { root.erase("root"s); root.put("root.uniqueid", http::unique_id); - auto &nodes = root.add_child("root.devices", pt::ptree {}); - for (auto &[_, client] : map_id_client) { - pt::ptree node; - - node.put("uniqueid"s, client.uniqueID); - - pt::ptree cert_nodes; - for (auto &cert : client.certs) { - pt::ptree cert_node; - cert_node.put_value(cert); - cert_nodes.push_back(std::make_pair(""s, cert_node)); - } - node.add_child("certs"s, cert_nodes); - - nodes.push_back(std::make_pair(""s, node)); + client_t &client = client_root; + pt::ptree node; + + pt::ptree named_cert_nodes; + for (auto &named_cert : client.named_devices) { + pt::ptree named_cert_node; + named_cert_node.put("name"s, named_cert.name); + named_cert_node.put("cert"s, named_cert.cert); + named_cert_node.put("uuid"s, named_cert.uuid); + named_cert_nodes.push_back(std::make_pair(""s, named_cert_node)); } + root.add_child("root.named_devices"s, named_cert_nodes); try { pt::write_json(config::nvhttp.file_state, root); @@ -211,9 +225,9 @@ namespace nvhttp { return; } - pt::ptree root; + pt::ptree tree; try { - pt::read_json(config::nvhttp.file_state, root); + pt::read_json(config::nvhttp.file_state, tree); } catch (std::exception &e) { BOOST_LOG(error) << "Couldn't read "sv << config::nvhttp.file_state << ": "sv << e.what(); @@ -221,7 +235,7 @@ namespace nvhttp { return; } - auto unique_id_p = root.get_optional("root.uniqueid"); + auto unique_id_p = tree.get_optional("root.uniqueid"); if (!unique_id_p) { // This file doesn't contain moonlight credentials http::unique_id = uuid_util::uuid_t::generate().string(); @@ -229,30 +243,61 @@ namespace nvhttp { } http::unique_id = std::move(*unique_id_p); - auto device_nodes = root.get_child("root.devices"); - - for (auto &[_, device_node] : device_nodes) { - auto uniqID = device_node.get("uniqueid"); - auto &client = map_id_client.emplace(uniqID, client_t {}).first->second; - - client.uniqueID = uniqID; + auto root = tree.get_child("root"); + client_t client; + + // Import from old format + if (root.get_child_optional("devices")) { + auto device_nodes = root.get_child("devices"); + for (auto &[_, device_node] : device_nodes) { + auto uniqID = device_node.get("uniqueid"); + + if (device_node.count("certs")) { + for (auto &[_, el] : device_node.get_child("certs")) { + named_cert_t named_cert; + named_cert.name = ""s; + named_cert.cert = el.get_value(); + named_cert.uuid = uuid_util::uuid_t::generate().string(); + client.named_devices.emplace_back(named_cert); + client.certs.emplace_back(named_cert.cert); + } + } + } + } - for (auto &[_, el] : device_node.get_child("certs")) { - client.certs.emplace_back(el.get_value()); + if (root.count("named_devices")) { + for (auto &[_, el] : root.get_child("named_devices")) { + named_cert_t named_cert; + named_cert.name = el.get_child("name").get_value(); + named_cert.cert = el.get_child("cert").get_value(); + named_cert.uuid = el.get_child("uuid").get_value(); + client.named_devices.emplace_back(named_cert); + client.certs.emplace_back(named_cert.cert); } } + + // Empty certificate chain and import certs from file + cert_chain.clear(); + for (auto &cert : client.certs) { + cert_chain.add(crypto::x509(cert)); + } + for (auto &named_cert : client.named_devices) { + cert_chain.add(crypto::x509(named_cert.cert)); + } + + client_root = client; } void update_id_client(const std::string &uniqueID, std::string &&cert, op_e op) { switch (op) { case op_e::ADD: { - auto &client = map_id_client[uniqueID]; + client_t &client = client_root; client.certs.emplace_back(std::move(cert)); - client.uniqueID = uniqueID; } break; case op_e::REMOVE: - map_id_client.erase(uniqueID); + client_t client; + client_root = client; break; } @@ -261,18 +306,54 @@ namespace nvhttp { } } - rtsp_stream::launch_session_t + std::shared_ptr make_launch_session(bool host_audio, const args_t &args) { - rtsp_stream::launch_session_t launch_session; + auto launch_session = std::make_shared(); + + launch_session->id = ++session_id_counter; + + auto rikey = util::from_hex_vec(get_arg(args, "rikey"), true); + std::copy(rikey.cbegin(), rikey.cend(), std::back_inserter(launch_session->gcm_key)); + + launch_session->host_audio = host_audio; + std::stringstream mode = std::stringstream(get_arg(args, "mode", "0x0x0")); + // Split mode by the char "x", to populate width/height/fps + int x = 0; + std::string segment; + while (std::getline(mode, segment, 'x')) { + if (x == 0) launch_session->width = atoi(segment.c_str()); + if (x == 1) launch_session->height = atoi(segment.c_str()); + if (x == 2) launch_session->fps = atoi(segment.c_str()); + x++; + } + launch_session->unique_id = (get_arg(args, "uniqueid", "unknown")); + launch_session->appid = util::from_view(get_arg(args, "appid", "unknown")); + launch_session->enable_sops = util::from_view(get_arg(args, "sops", "0")); + launch_session->surround_info = util::from_view(get_arg(args, "surroundAudioInfo", "196610")); + launch_session->surround_params = (get_arg(args, "surroundParams", "")); + launch_session->gcmap = util::from_view(get_arg(args, "gcmap", "0")); + launch_session->enable_hdr = util::from_view(get_arg(args, "hdrMode", "0")); + + // Encrypted RTSP is enabled with client reported corever >= 1 + auto corever = util::from_view(get_arg(args, "corever", "0")); + if (corever >= 1) { + launch_session->rtsp_cipher = crypto::cipher::gcm_t { + launch_session->gcm_key, false + }; + launch_session->rtsp_iv_counter = 0; + } + launch_session->rtsp_url_scheme = launch_session->rtsp_cipher ? "rtspenc://"s : "rtsp://"s; + + // Generate the unique identifiers for this connection that we will send later during RTSP handshake + unsigned char raw_payload[8]; + RAND_bytes(raw_payload, sizeof(raw_payload)); + launch_session->av_ping_payload = util::hex_vec(raw_payload); + RAND_bytes((unsigned char *) &launch_session->control_connect_data, sizeof(launch_session->control_connect_data)); - launch_session.host_audio = host_audio; - launch_session.gcm_key = util::from_hex(get_arg(args, "rikey"), true); + launch_session->iv.resize(16); uint32_t prepend_iv = util::endian::big(util::from_view(get_arg(args, "rikeyid"))); auto prepend_iv_p = (uint8_t *) &prepend_iv; - - auto next = std::copy(prepend_iv_p, prepend_iv_p + sizeof(prepend_iv), std::begin(launch_session.iv)); - std::fill(next, std::end(launch_session.iv), 0); - + std::copy(prepend_iv_p, prepend_iv_p + sizeof(prepend_iv), std::begin(launch_session->iv)); return launch_session; } @@ -296,6 +377,7 @@ namespace nvhttp { tree.put("root.plaincert", util::hex_vec(conf_intern.servercert, true)); tree.put("root..status_code", 200); } + void serverchallengeresp(pair_session_t &sess, pt::ptree &tree, const args_t &args) { auto encrypted_response = util::from_hex_vec(get_arg(args, "serverchallengeresp"), true); @@ -358,11 +440,15 @@ namespace nvhttp { auto &client = sess.client; auto pairingsecret = util::from_hex_vec(get_arg(args, "clientpairingsecret"), true); + if (pairingsecret.size() <= 16) { + tree.put("root.paired", 0); + tree.put("root..status_code", 400); + tree.put("root..status_message", "Clientpairingsecret too short"); + return; + } std::string_view secret { pairingsecret.data(), 16 }; - std::string_view sign { pairingsecret.data() + secret.size(), crypto::digest_size }; - - assert((secret.size() + sign.size()) == pairingsecret.size()); + std::string_view sign { pairingsecret.data() + secret.size(), pairingsecret.size() - secret.size() }; auto x509 = crypto::x509(client.cert); auto x509_sign = crypto::signature(x509); @@ -486,7 +572,6 @@ namespace nvhttp { auto ptr = map_id_sess.emplace(sess.client.uniqueID, std::move(sess)).first; ptr->second.async_insert_pin.salt = std::move(get_arg(args, "salt")); - if (config::sunshine.flags[config::flag::PIN_STDIN]) { std::string pin; @@ -496,6 +581,9 @@ namespace nvhttp { getservercert(ptr->second, tree, pin); } else { +#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 + system_tray::update_tray_require_pin(); +#endif ptr->second.async_insert_pin.response = std::move(response); fg.disable(); @@ -525,23 +613,49 @@ namespace nvhttp { /** * @brief Compare the user supplied pin to the Moonlight pin. * @param pin The user supplied pin. + * @param name The user supplied name. * @return `true` if the pin is correct, `false` otherwise. * * EXAMPLES: * ```cpp - * bool pin_status = nvhttp::pin("1234"); + * bool pin_status = nvhttp::pin("1234", "laptop"); * ``` */ bool - pin(std::string pin) { + pin(std::string pin, std::string name) { pt::ptree tree; if (map_id_sess.empty()) { return false; } + // ensure pin is 4 digits + if (pin.size() != 4) { + tree.put("root.paired", 0); + tree.put("root..status_code", 400); + tree.put( + "root..status_message", "Pin must be 4 digits, " + std::to_string(pin.size()) + " provided"); + return false; + } + + // ensure all pin characters are numeric + if (!std::all_of(pin.begin(), pin.end(), ::isdigit)) { + tree.put("root.paired", 0); + tree.put("root..status_code", 400); + tree.put("root..status_message", "Pin must be numeric"); + return false; + } + auto &sess = std::begin(map_id_sess)->second; getservercert(sess, tree, pin); + // set up named cert + client_t &client = client_root; + named_cert_t named_cert; + named_cert.name = name; + named_cert.cert = sess.client.cert; + named_cert.uuid = uuid_util::uuid_t::generate().string(); + client.named_devices.emplace_back(named_cert); + // response to the request for pin std::ostringstream data; pt::write_xml(data, tree); @@ -563,32 +677,6 @@ namespace nvhttp { return true; } - template - void - pin(std::shared_ptr::Response> response, std::shared_ptr::Request> request) { - print_req(request); - - response->close_connection_after_response = true; - - auto address = request->remote_endpoint().address().to_string(); - auto ip_type = net::from_address(address); - if (ip_type > http::origin_pin_allowed) { - BOOST_LOG(info) << "/pin: ["sv << address << "] -- denied"sv; - - response->write(SimpleWeb::StatusCode::client_error_forbidden); - - return; - } - - bool pinResponse = pin(request->path_match[1]); - if (pinResponse) { - response->write(SimpleWeb::StatusCode::success_ok); - } - else { - response->write(SimpleWeb::StatusCode::client_error_im_a_teapot); - } - } - template void serverinfo(std::shared_ptr::Response> response, std::shared_ptr::Request> request) { @@ -600,9 +688,7 @@ namespace nvhttp { auto clientID = args.find("uniqueid"s); if (clientID != std::end(args)) { - if (auto it = map_id_client.find(clientID->second); it != std::end(map_id_client)) { - pair_status = 1; - } + pair_status = 1; } } @@ -616,22 +702,50 @@ namespace nvhttp { tree.put("root.appversion", VERSION); tree.put("root.GfeVersion", GFE_VERSION); tree.put("root.uniqueid", http::unique_id); - tree.put("root.HttpsPort", map_port(PORT_HTTPS)); - tree.put("root.ExternalPort", map_port(PORT_HTTP)); - tree.put("root.mac", platf::get_mac_address(local_endpoint.address().to_string())); + tree.put("root.HttpsPort", net::map_port(PORT_HTTPS)); + tree.put("root.ExternalPort", net::map_port(PORT_HTTP)); tree.put("root.MaxLumaPixelsHEVC", video::active_hevc_mode > 1 ? "1869449984" : "0"); - tree.put("root.LocalIP", local_endpoint.address().to_string()); - if (video::active_hevc_mode == 3) { - tree.put("root.ServerCodecModeSupport", "3843"); + // Only include the MAC address for requests sent from paired clients over HTTPS. + // For HTTP requests, use a placeholder MAC address that Moonlight knows to ignore. + if constexpr (std::is_same_v) { + tree.put("root.mac", platf::get_mac_address(net::addr_to_normalized_string(local_endpoint.address()))); } - else if (video::active_hevc_mode == 2) { - tree.put("root.ServerCodecModeSupport", "259"); + else { + tree.put("root.mac", "00:00:00:00:00:00"); + } + + // Moonlight clients track LAN IPv6 addresses separately from LocalIP which is expected to + // always be an IPv4 address. If we return that same IPv6 address here, it will clobber the + // stored LAN IPv4 address. To avoid this, we need to return an IPv4 address in this field + // when we get a request over IPv6. + // + // HACK: We should return the IPv4 address of local interface here, but we don't currently + // have that implemented. For now, we will emulate the behavior of GFE+GS-IPv6-Forwarder, + // which returns 127.0.0.1 as LocalIP for IPv6 connections. Moonlight clients with IPv6 + // support know to ignore this bogus address. + if (local_endpoint.address().is_v6() && !local_endpoint.address().to_v6().is_v4_mapped()) { + tree.put("root.LocalIP", "127.0.0.1"); } else { - tree.put("root.ServerCodecModeSupport", "3"); + tree.put("root.LocalIP", net::addr_to_normalized_string(local_endpoint.address())); } + uint32_t codec_mode_flags = SCM_H264; + if (video::active_hevc_mode >= 2) { + codec_mode_flags |= SCM_HEVC; + } + if (video::active_hevc_mode >= 3) { + codec_mode_flags |= SCM_HEVC_MAIN10; + } + if (video::active_av1_mode >= 2) { + codec_mode_flags |= SCM_AV1_MAIN8; + } + if (video::active_av1_mode >= 3) { + codec_mode_flags |= SCM_AV1_MAIN10; + } + tree.put("root.ServerCodecModeSupport", codec_mode_flags); + pt::ptree display_nodes; for (auto &resolution : config::nvhttp.resolutions) { auto pred = [](auto ch) { return ch == ' ' || ch == '\t' || ch == 'x'; }; @@ -669,6 +783,20 @@ namespace nvhttp { response->close_connection_after_response = true; } + pt::ptree + get_all_clients() { + pt::ptree named_cert_nodes; + client_t &client = client_root; + for (auto &named_cert : client.named_devices) { + pt::ptree named_cert_node; + named_cert_node.put("name"s, named_cert.name); + named_cert_node.put("uuid"s, named_cert.uuid); + named_cert_nodes.push_back(std::make_pair(""s, named_cert_node)); + } + + return named_cert_nodes; + } + void applist(resp_https_t response, req_https_t request) { print_req(request); @@ -757,8 +885,22 @@ namespace nvhttp { } } + host_audio = util::from_view(get_arg(args, "localAudioPlayMode")); + auto launch_session = make_launch_session(host_audio, args); + + auto encryption_mode = net::encryption_mode_for_address(request->remote_endpoint().address()); + if (!launch_session->rtsp_cipher && encryption_mode == config::ENCRYPTION_MODE_MANDATORY) { + BOOST_LOG(error) << "Rejecting client that cannot comply with mandatory encryption requirement"sv; + + tree.put("root..status_code", 403); + tree.put("root..status_message", "Encryption is mandatory for this host but unsupported by the client"); + tree.put("root.gamesession", 0); + + return; + } + if (appid > 0) { - auto err = proc::proc.execute(appid); + auto err = proc::proc.execute(appid, launch_session); if (err) { tree.put("root..status_code", err); tree.put("root..status_message", "Failed to start the specified application"); @@ -768,12 +910,13 @@ namespace nvhttp { } } - host_audio = util::from_view(get_arg(args, "localAudioPlayMode")); - rtsp_stream::launch_session_raise(make_launch_session(host_audio, args)); - tree.put("root..status_code", 200); - tree.put("root.sessionUrl0", "rtsp://"s + request->local_endpoint().address().to_string() + ':' + std::to_string(map_port(rtsp_stream::RTSP_SETUP_PORT))); + tree.put("root.sessionUrl0", launch_session->rtsp_url_scheme + + net::addr_to_url_escaped_string(request->local_endpoint().address()) + ':' + + std::to_string(net::map_port(rtsp_stream::RTSP_SETUP_PORT))); tree.put("root.gamesession", 1); + + rtsp_stream::launch_session_raise(launch_session); } void @@ -840,11 +983,26 @@ namespace nvhttp { } } - rtsp_stream::launch_session_raise(make_launch_session(host_audio, args)); + auto launch_session = make_launch_session(host_audio, args); + + auto encryption_mode = net::encryption_mode_for_address(request->remote_endpoint().address()); + if (!launch_session->rtsp_cipher && encryption_mode == config::ENCRYPTION_MODE_MANDATORY) { + BOOST_LOG(error) << "Rejecting client that cannot comply with mandatory encryption requirement"sv; + + tree.put("root..status_code", 403); + tree.put("root..status_message", "Encryption is mandatory for this host but unsupported by the client"); + tree.put("root.gamesession", 0); + + return; + } tree.put("root..status_code", 200); - tree.put("root.sessionUrl0", "rtsp://"s + request->local_endpoint().address().to_string() + ':' + std::to_string(map_port(rtsp_stream::RTSP_SETUP_PORT))); + tree.put("root.sessionUrl0", launch_session->rtsp_url_scheme + + net::addr_to_url_escaped_string(request->local_endpoint().address()) + ':' + + std::to_string(net::map_port(rtsp_stream::RTSP_SETUP_PORT))); tree.put("root.resume", 1); + + rtsp_stream::launch_session_raise(launch_session); } void @@ -904,8 +1062,9 @@ namespace nvhttp { start() { auto shutdown_event = mail::man->event(mail::shutdown); - auto port_http = map_port(PORT_HTTP); - auto port_https = map_port(PORT_HTTPS); + auto port_http = net::map_port(PORT_HTTP); + auto port_https = net::map_port(PORT_HTTPS); + auto address_family = net::af_from_enum_string(config::sunshine.address_family); bool clean_slate = config::sunshine.flags[config::flag::FRESH_STATE]; @@ -913,27 +1072,20 @@ namespace nvhttp { load_state(); } - conf_intern.pkey = read_file(config::nvhttp.pkey.c_str()); - conf_intern.servercert = read_file(config::nvhttp.cert.c_str()); - - crypto::cert_chain_t cert_chain; - for (auto &[_, client] : map_id_client) { - for (auto &cert : client.certs) { - cert_chain.add(crypto::x509(cert)); - } - } + conf_intern.pkey = file_handler::read_file(config::nvhttp.pkey.c_str()); + conf_intern.servercert = file_handler::read_file(config::nvhttp.cert.c_str()); auto add_cert = std::make_shared>(30); - // /resume doesn't always get the parameter "localAudioPlayMode" - // /launch will store it in host_audio + // resume doesn't always get the parameter "localAudioPlayMode" + // launch will store it in host_audio bool host_audio {}; https_server_t https_server { config::nvhttp.cert, config::nvhttp.pkey }; http_server_t http_server; // Verify certificates after establishing connection - https_server.verify = [&cert_chain, add_cert](SSL *ssl) { + https_server.verify = [add_cert](SSL *ssl) { crypto::x509_t x509 { SSL_get_peer_certificate(ssl) }; if (!x509) { BOOST_LOG(info) << "unknown -- denied"sv; @@ -993,21 +1145,19 @@ namespace nvhttp { https_server.resource["^/applist$"]["GET"] = applist; https_server.resource["^/appasset$"]["GET"] = appasset; https_server.resource["^/launch$"]["GET"] = [&host_audio](auto resp, auto req) { launch(host_audio, resp, req); }; - https_server.resource["^/pin/([0-9]+)$"]["GET"] = pin; https_server.resource["^/resume$"]["GET"] = [&host_audio](auto resp, auto req) { resume(host_audio, resp, req); }; https_server.resource["^/cancel$"]["GET"] = cancel; https_server.config.reuse_address = true; - https_server.config.address = "0.0.0.0"s; + https_server.config.address = net::af_to_any_address_string(address_family); https_server.config.port = port_https; http_server.default_resource["GET"] = not_found; http_server.resource["^/serverinfo$"]["GET"] = serverinfo; http_server.resource["^/pair$"]["GET"] = [&add_cert](auto resp, auto req) { pair(add_cert, resp, req); }; - http_server.resource["^/pin/([0-9]+)$"]["GET"] = pin; http_server.config.reuse_address = true; - http_server.config.address = "0.0.0.0"s; + http_server.config.address = net::af_to_any_address_string(address_family); http_server.config.port = port_http; auto accept_and_run = [&](auto *http_server) { @@ -1048,7 +1198,48 @@ namespace nvhttp { */ void erase_all_clients() { - map_id_client.clear(); + client_t client; + client_root = client; + cert_chain.clear(); + save_state(); + } + + /** + * @brief Remove single client. + * + * EXAMPLES: + * ```cpp + * nvhttp::unpair_client("4D7BB2DD-5704-A405-B41C-891A022932E1"); + * ``` + */ + int + unpair_client(std::string uuid) { + int removed = 0; + client_t &client = client_root; + for (auto it = client.named_devices.begin(); it != client.named_devices.end();) { + if ((*it).uuid == uuid) { + // Find matching cert and remove it + for (auto cert = client.certs.begin(); cert != client.certs.end();) { + if ((*cert) == (*it).cert) { + cert = client.certs.erase(cert); + removed++; + } + else { + ++cert; + } + } + + // And then remove the named cert + it = client.named_devices.erase(it); + removed++; + } + else { + ++it; + } + } + save_state(); + load_state(); + return removed; } } // namespace nvhttp diff --git a/src/nvhttp.h b/src/nvhttp.h index 3be24b3de06..6fdf202ac29 100644 --- a/src/nvhttp.h +++ b/src/nvhttp.h @@ -9,6 +9,9 @@ // standard includes #include +// lib includes +#include + // local includes #include "thread_safe.h" @@ -43,7 +46,11 @@ namespace nvhttp { void start(); bool - pin(std::string pin); + pin(std::string pin, std::string name); + int + unpair_client(std::string uniqueid); + boost::property_tree::ptree + get_all_clients(); void erase_all_clients(); } // namespace nvhttp diff --git a/src/platform/common.h b/src/platform/common.h index cdb5785e076..007f7ece61b 100644 --- a/src/platform/common.h +++ b/src/platform/common.h @@ -10,14 +10,19 @@ #include #include -#include "src/main.h" +#include "src/config.h" +#include "src/logging.h" +#include "src/stat_trackers.h" #include "src/thread_safe.h" #include "src/utility.h" +#include "src/video_colorspace.h" extern "C" { #include } +using namespace std::literals; + struct sockaddr; struct AVFrame; struct AVBufferRef; @@ -45,37 +50,105 @@ namespace boost { namespace video { struct config_t; } // namespace video +namespace nvenc { + class nvenc_base; +} namespace platf { - constexpr auto MAX_GAMEPADS = 32; - - constexpr std::uint16_t DPAD_UP = 0x0001; - constexpr std::uint16_t DPAD_DOWN = 0x0002; - constexpr std::uint16_t DPAD_LEFT = 0x0004; - constexpr std::uint16_t DPAD_RIGHT = 0x0008; - constexpr std::uint16_t START = 0x0010; - constexpr std::uint16_t BACK = 0x0020; - constexpr std::uint16_t LEFT_STICK = 0x0040; - constexpr std::uint16_t RIGHT_STICK = 0x0080; - constexpr std::uint16_t LEFT_BUTTON = 0x0100; - constexpr std::uint16_t RIGHT_BUTTON = 0x0200; - constexpr std::uint16_t HOME = 0x0400; - constexpr std::uint16_t A = 0x1000; - constexpr std::uint16_t B = 0x2000; - constexpr std::uint16_t X = 0x4000; - constexpr std::uint16_t Y = 0x8000; - - struct rumble_t { - KITTY_DEFAULT_CONSTR(rumble_t) - - rumble_t(std::uint16_t id, std::uint16_t lowfreq, std::uint16_t highfreq): - id { id }, lowfreq { lowfreq }, highfreq { highfreq } {} + // Limited by bits in activeGamepadMask + constexpr auto MAX_GAMEPADS = 16; + + constexpr std::uint32_t DPAD_UP = 0x0001; + constexpr std::uint32_t DPAD_DOWN = 0x0002; + constexpr std::uint32_t DPAD_LEFT = 0x0004; + constexpr std::uint32_t DPAD_RIGHT = 0x0008; + constexpr std::uint32_t START = 0x0010; + constexpr std::uint32_t BACK = 0x0020; + constexpr std::uint32_t LEFT_STICK = 0x0040; + constexpr std::uint32_t RIGHT_STICK = 0x0080; + constexpr std::uint32_t LEFT_BUTTON = 0x0100; + constexpr std::uint32_t RIGHT_BUTTON = 0x0200; + constexpr std::uint32_t HOME = 0x0400; + constexpr std::uint32_t A = 0x1000; + constexpr std::uint32_t B = 0x2000; + constexpr std::uint32_t X = 0x4000; + constexpr std::uint32_t Y = 0x8000; + constexpr std::uint32_t PADDLE1 = 0x010000; + constexpr std::uint32_t PADDLE2 = 0x020000; + constexpr std::uint32_t PADDLE3 = 0x040000; + constexpr std::uint32_t PADDLE4 = 0x080000; + constexpr std::uint32_t TOUCHPAD_BUTTON = 0x100000; + constexpr std::uint32_t MISC_BUTTON = 0x200000; + + enum class gamepad_feedback_e { + rumble, + rumble_triggers, + set_motion_event_state, + set_rgb_led, + }; + + struct gamepad_feedback_msg_t { + static gamepad_feedback_msg_t + make_rumble(std::uint16_t id, std::uint16_t lowfreq, std::uint16_t highfreq) { + gamepad_feedback_msg_t msg; + msg.type = gamepad_feedback_e::rumble; + msg.id = id; + msg.data.rumble = { lowfreq, highfreq }; + return msg; + } + + static gamepad_feedback_msg_t + make_rumble_triggers(std::uint16_t id, std::uint16_t left, std::uint16_t right) { + gamepad_feedback_msg_t msg; + msg.type = gamepad_feedback_e::rumble_triggers; + msg.id = id; + msg.data.rumble_triggers = { left, right }; + return msg; + } + static gamepad_feedback_msg_t + make_motion_event_state(std::uint16_t id, std::uint8_t motion_type, std::uint16_t report_rate) { + gamepad_feedback_msg_t msg; + msg.type = gamepad_feedback_e::set_motion_event_state; + msg.id = id; + msg.data.motion_event_state.motion_type = motion_type; + msg.data.motion_event_state.report_rate = report_rate; + return msg; + } + + static gamepad_feedback_msg_t + make_rgb_led(std::uint16_t id, std::uint8_t r, std::uint8_t g, std::uint8_t b) { + gamepad_feedback_msg_t msg; + msg.type = gamepad_feedback_e::set_rgb_led; + msg.id = id; + msg.data.rgb_led = { r, g, b }; + return msg; + } + + gamepad_feedback_e type; std::uint16_t id; - std::uint16_t lowfreq; - std::uint16_t highfreq; + union { + struct { + std::uint16_t lowfreq; + std::uint16_t highfreq; + } rumble; + struct { + std::uint16_t left_trigger; + std::uint16_t right_trigger; + } rumble_triggers; + struct { + std::uint16_t report_rate; + std::uint8_t motion_type; + } motion_event_state; + struct { + std::uint8_t r; + std::uint8_t g; + std::uint8_t b; + } rgb_led; + } data; }; - using rumble_queue_t = safe::mail_raw_t::queue_t; + + using feedback_queue_t = safe::mail_raw_t::queue_t; namespace speaker { enum speaker_e { @@ -118,6 +191,7 @@ namespace platf { vaapi, dxgi, cuda, + videotoolbox, unknown }; @@ -153,8 +227,16 @@ namespace platf { int width, height; }; + // These values must match Limelight-internal.h's SS_FF_* constants! + namespace platform_caps { + typedef uint32_t caps_t; + + constexpr caps_t pen_touch = 0x01; // Pen and touch events + constexpr caps_t controller_touch = 0x02; // Controller touch events + }; // namespace platform_caps + struct gamepad_state_t { - std::uint16_t buttonFlags; + std::uint32_t buttonFlags; std::uint8_t lt; std::uint8_t rt; std::int16_t lsX; @@ -163,6 +245,73 @@ namespace platf { std::int16_t rsY; }; + struct gamepad_id_t { + // The global index is used when looking up gamepads in the platform's + // gamepad array. It identifies gamepads uniquely among all clients. + int globalIndex; + + // The client-relative index is the controller number as reported by the + // client. It must be used when communicating back to the client via + // the input feedback queue. + std::uint8_t clientRelativeIndex; + }; + + struct gamepad_arrival_t { + std::uint8_t type; + std::uint16_t capabilities; + std::uint32_t supportedButtons; + }; + + struct gamepad_touch_t { + gamepad_id_t id; + std::uint8_t eventType; + std::uint32_t pointerId; + float x; + float y; + float pressure; + }; + + struct gamepad_motion_t { + gamepad_id_t id; + std::uint8_t motionType; + + // Accel: m/s^2 + // Gyro: deg/s + float x; + float y; + float z; + }; + + struct gamepad_battery_t { + gamepad_id_t id; + std::uint8_t state; + std::uint8_t percentage; + }; + + struct touch_input_t { + std::uint8_t eventType; + std::uint16_t rotation; // Degrees (0..360) or LI_ROT_UNKNOWN + std::uint32_t pointerId; + float x; + float y; + float pressureOrDistance; // Distance for hover and pressure for contact + float contactAreaMajor; + float contactAreaMinor; + }; + + struct pen_input_t { + std::uint8_t eventType; + std::uint8_t toolType; + std::uint8_t penButtons; + std::uint8_t tilt; // Degrees (0..90) or LI_TILT_UNKNOWN + std::uint16_t rotation; // Degrees (0..360) or LI_ROT_UNKNOWN + float x; + float y; + float pressureOrDistance; // Distance for hover and pressure for contact + float contactAreaMajor; + float contactAreaMinor; + }; + class deinit_t { public: virtual ~deinit_t() = default; @@ -204,15 +353,28 @@ namespace platf { std::optional null; }; - struct hwdevice_t { + struct encode_device_t { + virtual ~encode_device_t() = default; + + virtual int + convert(platf::img_t &img) = 0; + + video::sunshine_colorspace_t colorspace; + }; + + struct avcodec_encode_device_t: encode_device_t { void *data {}; AVFrame *frame {}; - virtual int - convert(platf::img_t &img) { + int + convert(platf::img_t &img) override { return -1; } + virtual void + apply_colorspace() { + } + /** * implementations must take ownership of 'frame' */ @@ -222,9 +384,6 @@ namespace platf { return -1; }; - virtual void - set_colorspace(std::uint32_t colorspace, std::uint32_t color_range) {}; - /** * Implementations may set parameters during initialization of the hwframes context */ @@ -238,8 +397,13 @@ namespace platf { prepare_to_derive_context(int hw_device_type) { return 0; }; + }; + + struct nvenc_encode_device_t: encode_device_t { + virtual bool + init_encoder(const video::config_t &client_config, const video::sunshine_colorspace_t &colorspace) = 0; - virtual ~hwdevice_t() = default; + nvenc::nvenc_base *nvenc = nullptr; }; enum class capture_e : int { @@ -284,7 +448,7 @@ namespace platf { * from the pool. If backend uses multiple threads, calls to this * callback must be synchronized. Calls to this callback and * push_captured_image_cb must be synchronized as well. - * bool *cursor --> A pointer to the flag that indicates wether the cursor should be captured as well + * bool *cursor --> A pointer to the flag that indicates whether the cursor should be captured as well * * Returns either: * capture_e::ok when stopping @@ -300,9 +464,14 @@ namespace platf { virtual int dummy_img(img_t *img) = 0; - virtual std::shared_ptr - make_hwdevice(pix_fmt_e pix_fmt) { - return std::make_shared(); + virtual std::unique_ptr + make_avcodec_encode_device(pix_fmt_e pix_fmt) { + return nullptr; + } + + virtual std::unique_ptr + make_nvenc_encode_device(pix_fmt_e pix_fmt) { + return nullptr; } virtual bool @@ -316,6 +485,17 @@ namespace platf { return false; } + /** + * @brief Checks that a given codec is supported by the display device. + * @param name The FFmpeg codec name (or similar for non-FFmpeg codecs). + * @param config The codec configuration. + * @return true if supported, false otherwise. + */ + virtual bool + is_codec_supported(std::string_view name, const ::video::config_t &config) { + return true; + } + virtual ~display_t() = default; // Offsets for when streaming a specific monitor. By default, they are 0. @@ -323,6 +503,22 @@ namespace platf { int env_width, env_height; int width, height; + + protected: + // collect capture timing data (at loglevel debug) + stat_trackers::min_max_avg_tracker sleep_overshoot_tracker; + void + log_sleep_overshoot(std::chrono::nanoseconds overshoot_ns) { + if (config::sunshine.min_log_level <= 1) { + // Print sleep overshoot stats to debug log every 20 seconds + auto print_info = [&](double min_overshoot, double max_overshoot, double avg_overshoot) { + auto f = stat_trackers::one_digit_after_decimal(); + BOOST_LOG(debug) << "Sleep overshoot (min/max/avg): " << f % min_overshoot << "ms/" << f % max_overshoot << "ms/" << f % avg_overshoot << "ms"; + }; + // std::chrono::nanoseconds overshoot_ns = std::chrono::steady_clock::now() - next_frame; + sleep_overshoot_tracker.collect_and_callback_on_interval(overshoot_ns.count() / 1000000., print_info, 20s); + } + } }; class mic_t { @@ -369,7 +565,7 @@ namespace platf { /** * display_name --> The name of the monitor that SHOULD be displayed * If display_name is empty --> Use the first monitor that's compatible you can find - * If you require to use this parameter in a seperate thread --> make a copy of it. + * If you require to use this parameter in a separate thread --> make a copy of it. * * config --> Stream configuration * @@ -382,8 +578,15 @@ namespace platf { std::vector display_names(mem_type_e hwdevice_type); + /** + * @brief Returns if GPUs/drivers have changed since the last call to this function. + * @return `true` if a change has occurred or if it is unknown whether a change occurred. + */ + bool + needs_encoder_reenumeration(); + boost::process::child - run_command(bool elevated, bool interactive, const std::string &cmd, boost::filesystem::path &working_dir, boost::process::environment &env, FILE *file, std::error_code &ec, boost::process::group *group); + run_command(bool elevated, bool interactive, const std::string &cmd, boost::filesystem::path &working_dir, const boost::process::environment &env, FILE *file, std::error_code &ec, boost::process::group *group); enum class thread_priority_e : int { low, @@ -411,16 +614,38 @@ namespace platf { std::uintptr_t native_socket; boost::asio::ip::address &target_address; uint16_t target_port; + boost::asio::ip::address &source_address; }; bool send_batch(batched_send_info_t &send_info); + struct send_info_t { + const char *buffer; + size_t size; + + std::uintptr_t native_socket; + boost::asio::ip::address &target_address; + uint16_t target_port; + boost::asio::ip::address &source_address; + }; + bool + send(send_info_t &send_info); + enum class qos_data_type_e : int { audio, video }; + + /** + * @brief Enables QoS on the given socket for traffic to the specified destination. + * @param native_socket The native socket handle. + * @param address The destination address for traffic sent on this socket. + * @param port The destination port for traffic sent on this socket. + * @param data_type The type of traffic sent on this socket. + * @param dscp_tagging Specifies whether to enable DSCP tagging on outgoing traffic. + */ std::unique_ptr - enable_socket_qos(uintptr_t native_socket, boost::asio::ip::address &address, uint16_t port, qos_data_type_e data_type); + enable_socket_qos(uintptr_t native_socket, boost::asio::ip::address &address, uint16_t port, qos_data_type_e data_type, bool dscp_tagging); /** * @brief Open a url in the default web browser. @@ -429,6 +654,22 @@ namespace platf { void open_url(const std::string &url); + /** + * @brief Attempt to gracefully terminate a process group. + * @param native_handle The native handle of the process group. + * @return true if termination was successfully requested. + */ + bool + request_process_group_exit(std::uintptr_t native_handle); + + /** + * @brief Checks if a process group still has running children. + * @param native_handle The native handle of the process group. + * @return true if processes are still running. + */ + bool + process_group_running(std::uintptr_t native_handle); + input_t input(); void @@ -448,11 +689,78 @@ namespace platf { void unicode(input_t &input, char *utf8, int size); + typedef deinit_t client_input_t; + + /** + * @brief Allocates a context to store per-client input data. + * @param input The global input context. + * @return A unique pointer to a per-client input data context. + */ + std::unique_ptr + allocate_client_input_context(input_t &input); + + /** + * @brief Sends a touch event to the OS. + * @param input The client-specific input context. + * @param touch_port The current viewport for translating to screen coordinates. + * @param touch The touch event. + */ + void + touch(client_input_t *input, const touch_port_t &touch_port, const touch_input_t &touch); + + /** + * @brief Sends a pen event to the OS. + * @param input The client-specific input context. + * @param touch_port The current viewport for translating to screen coordinates. + * @param pen The pen event. + */ + void + pen(client_input_t *input, const touch_port_t &touch_port, const pen_input_t &pen); + + /** + * @brief Sends a gamepad touch event to the OS. + * @param input The global input context. + * @param touch The touch event. + */ + void + gamepad_touch(input_t &input, const gamepad_touch_t &touch); + + /** + * @brief Sends a gamepad motion event to the OS. + * @param input The global input context. + * @param motion The motion event. + */ + void + gamepad_motion(input_t &input, const gamepad_motion_t &motion); + + /** + * @brief Sends a gamepad battery event to the OS. + * @param input The global input context. + * @param battery The battery event. + */ + void + gamepad_battery(input_t &input, const gamepad_battery_t &battery); + + /** + * @brief Creates a new virtual gamepad. + * @param input The global input context. + * @param id The gamepad ID. + * @param metadata Controller metadata from client (empty if none provided). + * @param feedback_queue The queue for posting messages back to the client. + * @return 0 on success. + */ int - alloc_gamepad(input_t &input, int nr, rumble_queue_t rumble_queue); + alloc_gamepad(input_t &input, const gamepad_id_t &id, const gamepad_arrival_t &metadata, feedback_queue_t feedback_queue); void free_gamepad(input_t &input, int nr); + /** + * @brief Returns the supported platform capabilities to advertise to the client. + * @return Capability flags. + */ + platform_caps::caps_t + get_capabilities(); + #define SERVICE_NAME "Sunshine" #define SERVICE_TYPE "_nvstream._tcp" diff --git a/src/platform/linux/audio.cpp b/src/platform/linux/audio.cpp index e31f539f615..e663c811ba2 100644 --- a/src/platform/linux/audio.cpp +++ b/src/platform/linux/audio.cpp @@ -4,6 +4,7 @@ */ #include #include +#include #include @@ -14,7 +15,7 @@ #include "src/platform/common.h" #include "src/config.h" -#include "src/main.h" +#include "src/logging.h" #include "src/thread_safe.h" namespace platf { diff --git a/src/platform/linux/cuda.cpp b/src/platform/linux/cuda.cpp index c2a2e0fd591..0c73dcde0b2 100644 --- a/src/platform/linux/cuda.cpp +++ b/src/platform/linux/cuda.cpp @@ -3,6 +3,9 @@ * @brief todo */ #include +#include +#include +#include #include #include @@ -15,7 +18,7 @@ extern "C" { #include "cuda.h" #include "graphics.h" -#include "src/main.h" +#include "src/logging.h" #include "src/utility.h" #include "src/video.h" #include "wayland.h" @@ -29,6 +32,8 @@ extern "C" { #define CU_CHECK_IGNORE(x, y) \ check((x), SUNSHINE_STRINGVIEW(y ": ")) +namespace fs = std::filesystem; + using namespace std::literals; namespace cuda { constexpr auto cudaDevAttrMaxThreadsPerBlock = (CUdevice_attribute) 1; @@ -69,6 +74,13 @@ namespace cuda { CU_CHECK_IGNORE(cdf->cuStreamDestroy(stream), "Couldn't destroy cuda stream"); } + void + unregisterResource(CUgraphicsResource resource) { + CU_CHECK_IGNORE(cdf->cuGraphicsUnregisterResource(resource), "Couldn't unregister resource"); + } + + using registered_resource_t = util::safe_ptr; + class img_t: public platf::img_t { public: tex_t tex; @@ -88,7 +100,7 @@ namespace cuda { return 0; } - class cuda_t: public platf::hwdevice_t { + class cuda_t: public platf::avcodec_encode_device_t { public: int init(int in_width, int in_height) { @@ -145,8 +157,8 @@ namespace cuda { } void - set_colorspace(std::uint32_t colorspace, std::uint32_t color_range) override { - sws.set_colorspace(colorspace, color_range); + apply_colorspace() override { + sws.apply_colorspace(colorspace); auto tex = tex_t::make(height, width * 4); if (!tex) { @@ -183,7 +195,7 @@ namespace cuda { int width, height; - // When heigth and width don't change, it's not necessary to use linear interpolation + // When height and width don't change, it's not necessary to use linear interpolation bool linear_interpolation; sws_t sws; @@ -223,19 +235,258 @@ namespace cuda { } }; - std::shared_ptr - make_hwdevice(int width, int height, bool vram) { + /** + * @brief Opens the DRM device associated with the CUDA device index. + * @param index CUDA device index to open. + * @return File descriptor or -1 on failure. + */ + file_t + open_drm_fd_for_cuda_device(int index) { + CUdevice device; + CU_CHECK(cdf->cuDeviceGet(&device, index), "Couldn't get CUDA device"); + + // There's no way to directly go from CUDA to a DRM device, so we'll + // use sysfs to look up the DRM device name from the PCI ID. + std::array pci_bus_id; + CU_CHECK(cdf->cuDeviceGetPCIBusId(pci_bus_id.data(), pci_bus_id.size(), device), "Couldn't get CUDA device PCI bus ID"); + BOOST_LOG(debug) << "Found CUDA device with PCI bus ID: "sv << pci_bus_id.data(); + + // Linux uses lowercase hexadecimal while CUDA uses uppercase + std::transform(pci_bus_id.begin(), pci_bus_id.end(), pci_bus_id.begin(), + [](char c) { return std::tolower(c); }); + + // Look for the name of the primary node in sysfs + try { + char sysfs_path[PATH_MAX]; + std::snprintf(sysfs_path, sizeof(sysfs_path), "/sys/bus/pci/devices/%s/drm", pci_bus_id.data()); + fs::path sysfs_dir { sysfs_path }; + for (auto &entry : fs::directory_iterator { sysfs_dir }) { + auto file = entry.path().filename(); + auto filestring = file.generic_string(); + if (std::string_view { filestring }.substr(0, 4) != "card"sv) { + continue; + } + + BOOST_LOG(debug) << "Found DRM primary node: "sv << filestring; + + fs::path dri_path { "/dev/dri"sv }; + auto device_path = dri_path / file; + return open(device_path.c_str(), O_RDWR); + } + } + catch (const std::filesystem::filesystem_error &err) { + BOOST_LOG(error) << "Failed to read sysfs: "sv << err.what(); + } + + BOOST_LOG(error) << "Unable to find DRM device with PCI bus ID: "sv << pci_bus_id.data(); + return -1; + } + + class gl_cuda_vram_t: public platf::avcodec_encode_device_t { + public: + /** + * @brief Initialize the GL->CUDA encoding device. + * @param in_width Width of captured frames. + * @param in_height Height of captured frames. + * @param offset_x Offset of content in captured frame. + * @param offset_y Offset of content in captured frame. + * @return 0 on success or -1 on failure. + */ + int + init(int in_width, int in_height, int offset_x, int offset_y) { + // This must be non-zero to tell the video core that it's a hardware encoding device. + data = (void *) 0x1; + + // TODO: Support more than one CUDA device + file = std::move(open_drm_fd_for_cuda_device(0)); + if (file.el < 0) { + char string[1024]; + BOOST_LOG(error) << "Couldn't open DRM FD for CUDA device: "sv << strerror_r(errno, string, sizeof(string)); + return -1; + } + + gbm.reset(gbm::create_device(file.el)); + if (!gbm) { + BOOST_LOG(error) << "Couldn't create GBM device: ["sv << util::hex(eglGetError()).to_string_view() << ']'; + return -1; + } + + display = egl::make_display(gbm.get()); + if (!display) { + return -1; + } + + auto ctx_opt = egl::make_ctx(display.get()); + if (!ctx_opt) { + return -1; + } + + ctx = std::move(*ctx_opt); + + width = in_width; + height = in_height; + + sequence = 0; + + this->offset_x = offset_x; + this->offset_y = offset_y; + + return 0; + } + + /** + * @brief Initialize color conversion into target CUDA frame. + * @param frame Destination CUDA frame to write into. + * @param hw_frames_ctx_buf FFmpeg hardware frame context. + * @return 0 on success or -1 on failure. + */ + int + set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx_buf) override { + this->hwframe.reset(frame); + this->frame = frame; + + if (!frame->buf[0]) { + if (av_hwframe_get_buffer(hw_frames_ctx_buf, frame, 0)) { + BOOST_LOG(error) << "Couldn't get hwframe for VAAPI"sv; + return -1; + } + } + + auto hw_frames_ctx = (AVHWFramesContext *) hw_frames_ctx_buf->data; + sw_format = hw_frames_ctx->sw_format; + + auto nv12_opt = egl::create_target(frame->width, frame->height, sw_format); + if (!nv12_opt) { + return -1; + } + + auto sws_opt = egl::sws_t::make(width, height, frame->width, frame->height, sw_format); + if (!sws_opt) { + return -1; + } + + this->sws = std::move(*sws_opt); + this->nv12 = std::move(*nv12_opt); + + auto cuda_ctx = (AVCUDADeviceContext *) hw_frames_ctx->device_ctx->hwctx; + + stream = make_stream(); + if (!stream) { + return -1; + } + + cuda_ctx->stream = stream.get(); + + CU_CHECK(cdf->cuGraphicsGLRegisterImage(&y_res, nv12->tex[0], GL_TEXTURE_2D, CU_GRAPHICS_REGISTER_FLAGS_READ_ONLY), + "Couldn't register Y plane texture"); + CU_CHECK(cdf->cuGraphicsGLRegisterImage(&uv_res, nv12->tex[1], GL_TEXTURE_2D, CU_GRAPHICS_REGISTER_FLAGS_READ_ONLY), + "Couldn't register UV plane texture"); + + return 0; + } + + /** + * @brief Convert the captured image into the target CUDA frame. + * @param img Captured screen image. + * @return 0 on success or -1 on failure. + */ + int + convert(platf::img_t &img) override { + auto &descriptor = (egl::img_descriptor_t &) img; + + if (descriptor.sequence == 0) { + // For dummy images, use a blank RGB texture instead of importing a DMA-BUF + rgb = egl::create_blank(img); + } + else if (descriptor.sequence > sequence) { + sequence = descriptor.sequence; + + rgb = egl::rgb_t {}; + + auto rgb_opt = egl::import_source(display.get(), descriptor.sd); + + if (!rgb_opt) { + return -1; + } + + rgb = std::move(*rgb_opt); + } + + // Perform the color conversion and scaling in GL + sws.load_vram(descriptor, offset_x, offset_y, rgb->tex[0]); + sws.convert(nv12->buf); + + auto fmt_desc = av_pix_fmt_desc_get(sw_format); + + // Map the GL textures to read for CUDA + CUgraphicsResource resources[2] = { y_res.get(), uv_res.get() }; + CU_CHECK(cdf->cuGraphicsMapResources(2, resources, stream.get()), "Couldn't map GL textures in CUDA"); + + // Copy from the GL textures to the target CUDA frame + for (int i = 0; i < 2; i++) { + CUDA_MEMCPY2D cpy = {}; + cpy.srcMemoryType = CU_MEMORYTYPE_ARRAY; + CU_CHECK(cdf->cuGraphicsSubResourceGetMappedArray(&cpy.srcArray, resources[i], 0, 0), "Couldn't get mapped plane array"); + + cpy.dstMemoryType = CU_MEMORYTYPE_DEVICE; + cpy.dstDevice = (CUdeviceptr) frame->data[i]; + cpy.dstPitch = frame->linesize[i]; + cpy.WidthInBytes = (frame->width * fmt_desc->comp[i].step) >> (i ? fmt_desc->log2_chroma_w : 0); + cpy.Height = frame->height >> (i ? fmt_desc->log2_chroma_h : 0); + + CU_CHECK_IGNORE(cdf->cuMemcpy2DAsync(&cpy, stream.get()), "Couldn't copy texture to CUDA frame"); + } + + // Unmap the textures to allow modification from GL again + CU_CHECK(cdf->cuGraphicsUnmapResources(2, resources, stream.get()), "Couldn't unmap GL textures from CUDA"); + return 0; + } + + /** + * @brief Configures shader parameters for the specified colorspace. + */ + void + apply_colorspace() override { + sws.apply_colorspace(colorspace); + } + + file_t file; + gbm::gbm_t gbm; + egl::display_t display; + egl::ctx_t ctx; + + // This must be destroyed before display_t + stream_t stream; + frame_t hwframe; + + egl::sws_t sws; + egl::nv12_t nv12; + AVPixelFormat sw_format; + + int width, height; + + std::uint64_t sequence; + egl::rgb_t rgb; + + registered_resource_t y_res; + registered_resource_t uv_res; + + int offset_x, offset_y; + }; + + std::unique_ptr + make_avcodec_encode_device(int width, int height, bool vram) { if (init()) { return nullptr; } - std::shared_ptr cuda; + std::unique_ptr cuda; if (vram) { - cuda = std::make_shared(); + cuda = std::make_unique(); } else { - cuda = std::make_shared(); + cuda = std::make_unique(); } if (cuda->init(width, height)) { @@ -245,6 +496,29 @@ namespace cuda { return cuda; } + /** + * @brief Create a GL->CUDA encoding device for consuming captured dmabufs. + * @param in_width Width of captured frames. + * @param in_height Height of captured frames. + * @param offset_x Offset of content in captured frame. + * @param offset_y Offset of content in captured frame. + * @return FFmpeg encoding device context. + */ + std::unique_ptr + make_avcodec_gl_encode_device(int width, int height, int offset_x, int offset_y) { + if (init()) { + return nullptr; + } + + auto cuda = std::make_unique(); + + if (cuda->init(width, height, offset_x, offset_y)) { + return nullptr; + } + + return cuda; + } + namespace nvfbc { static PNVFBCCREATEINSTANCE createInstance {}; static NVFBC_API_FUNCTION_LIST func { NVFBC_VERSION }; @@ -340,6 +614,12 @@ namespace cuda { make() { NVFBC_CREATE_HANDLE_PARAMS params { NVFBC_CREATE_HANDLE_PARAMS_VER }; + // Set privateData to allow NvFBC on consumer NVIDIA GPUs. + // Based on https://github.com/keylase/nvidia-patch/blob/3193b4b1cea91527bf09ea9b8db5aade6a3f3c0a/win/nvfbcwrp/nvfbcwrp_main.cpp#L23-L25 . + const unsigned int MAGIC_PRIVATE_DATA[4] = { 0xAEF57AC5, 0x401D1A39, 0x1B856BBE, 0x9ED0CEBA }; + params.privateData = MAGIC_PRIVATE_DATA; + params.privateDataSize = sizeof(MAGIC_PRIVATE_DATA); + handle_t handle; auto status = func.nvFBCCreateHandle(&handle.handle, ¶ms); if (status) { @@ -526,16 +806,21 @@ namespace cuda { handle.reset(); }); + sleep_overshoot_tracker.reset(); + while (true) { auto now = std::chrono::steady_clock::now(); if (next_frame > now) { - std::this_thread::sleep_for((next_frame - now) / 3 * 2); + std::this_thread::sleep_for(next_frame - now); } - while (next_frame > now) { - std::this_thread::sleep_for(1ns); - now = std::chrono::steady_clock::now(); + now = std::chrono::steady_clock::now(); + std::chrono::nanoseconds overshoot_ns = now - next_frame; + log_sleep_overshoot(overshoot_ns); + + next_frame += delay; + if (next_frame < now) { // some major slowdown happened; we couldn't keep up + next_frame = now + delay; } - next_frame = now + delay; std::shared_ptr img_out; auto status = snapshot(pull_free_image_cb, img_out, 150ms, *cursor); @@ -675,9 +960,9 @@ namespace cuda { return platf::capture_e::ok; } - std::shared_ptr - make_hwdevice(platf::pix_fmt_e pix_fmt) override { - return ::cuda::make_hwdevice(width, height, true); + std::unique_ptr + make_avcodec_encode_device(platf::pix_fmt_e pix_fmt) { + return ::cuda::make_avcodec_encode_device(width, height, true); } std::shared_ptr @@ -770,4 +1055,4 @@ namespace platf { return display_names; } -} // namespace platf \ No newline at end of file +} // namespace platf diff --git a/src/platform/linux/cuda.cu b/src/platform/linux/cuda.cu index 107075d99cd..8fb1a5ee2d6 100644 --- a/src/platform/linux/cuda.cu +++ b/src/platform/linux/cuda.cu @@ -56,12 +56,11 @@ public: }; } // namespace platf -namespace video { -using __float4 = float[4]; -using __float3 = float[3]; -using __float2 = float[2]; +// End special declarations + +namespace cuda { -struct alignas(16) color_t { +struct alignas(16) cuda_color_t { float4 color_vec_y; float4 color_vec_u; float4 color_vec_v; @@ -69,22 +68,8 @@ struct alignas(16) color_t { float2 range_uv; }; -struct alignas(16) color_extern_t { - __float4 color_vec_y; - __float4 color_vec_u; - __float4 color_vec_v; - __float2 range_y; - __float2 range_uv; -}; - -static_assert(sizeof(video::color_t) == sizeof(video::color_extern_t), "color matrix struct mismatch"); - -extern color_t colors[6]; -} // namespace video +static_assert(sizeof(video::color_t) == sizeof(cuda::cuda_color_t), "color matrix struct mismatch"); -// End special declarations - -namespace cuda { auto constexpr INVALID_TEXTURE = std::numeric_limits::max(); template @@ -144,7 +129,7 @@ inline __device__ float3 bgra_to_rgb(float4 vec) { return make_float3(vec.z, vec.y, vec.x); } -inline __device__ float2 calcUV(float3 pixel, const video::color_t *const color_matrix) { +inline __device__ float2 calcUV(float3 pixel, const cuda_color_t *const color_matrix) { float4 vec_u = color_matrix->color_vec_u; float4 vec_v = color_matrix->color_vec_v; @@ -157,7 +142,7 @@ inline __device__ float2 calcUV(float3 pixel, const video::color_t *const color_ return make_float2(u, v); } -inline __device__ float calcY(float3 pixel, const video::color_t *const color_matrix) { +inline __device__ float calcY(float3 pixel, const cuda_color_t *const color_matrix) { float4 vec_y = color_matrix->color_vec_y; return (dot(pixel, make_float3(vec_y)) + vec_y.w) * color_matrix->range_y.x + color_matrix->range_y.y; @@ -166,7 +151,7 @@ inline __device__ float calcY(float3 pixel, const video::color_t *const color_ma __global__ void RGBA_to_NV12( cudaTextureObject_t srcImage, std::uint8_t *dstY, std::uint8_t *dstUV, std::uint32_t dstPitchY, std::uint32_t dstPitchUV, - float scale, const viewport_t viewport, const video::color_t *const color_matrix) { + float scale, const viewport_t viewport, const cuda_color_t *const color_matrix) { int idX = (threadIdx.x + blockDim.x * blockIdx.x) * 2; int idY = (threadIdx.y + blockDim.y * blockIdx.y) * 2; @@ -198,10 +183,10 @@ __global__ void RGBA_to_NV12( dstUV[0] = uv.x; dstUV[1] = uv.y; - dstY0[0] = calcY(rgb_lt, color_matrix) * 245.0f; // 245.0f is a magic number to ensure slight changes in luminosity are more visisble - dstY0[1] = calcY(rgb_rt, color_matrix) * 245.0f; // 245.0f is a magic number to ensure slight changes in luminosity are more visisble - dstY1[0] = calcY(rgb_lb, color_matrix) * 245.0f; // 245.0f is a magic number to ensure slight changes in luminosity are more visisble - dstY1[1] = calcY(rgb_rb, color_matrix) * 245.0f; // 245.0f is a magic number to ensure slight changes in luminosity are more visisble + dstY0[0] = calcY(rgb_lt, color_matrix) * 245.0f; // 245.0f is a magic number to ensure slight changes in luminosity are more visible + dstY0[1] = calcY(rgb_rt, color_matrix) * 245.0f; // 245.0f is a magic number to ensure slight changes in luminosity are more visible + dstY1[0] = calcY(rgb_lb, color_matrix) * 245.0f; // 245.0f is a magic number to ensure slight changes in luminosity are more visible + dstY1[1] = calcY(rgb_rb, color_matrix) * 245.0f; // 245.0f is a magic number to ensure slight changes in luminosity are more visible } int tex_t::copy(std::uint8_t *src, int height, int pitch) { @@ -237,7 +222,7 @@ std::optional tex_t::make(int height, int pitch) { return tex; } -tex_t::tex_t() : array {}, texture { INVALID_TEXTURE } {} +tex_t::tex_t() : array {}, texture { INVALID_TEXTURE, INVALID_TEXTURE } {} tex_t::tex_t(tex_t &&other) : array { other.array }, texture { other.texture } { other.array = 0; other.texture.point = INVALID_TEXTURE; @@ -297,7 +282,7 @@ std::optional sws_t::make(int in_width, int in_height, int out_width, int CU_CHECK_OPT(cudaGetDevice(&device), "Couldn't get cuda device"); CU_CHECK_OPT(cudaGetDeviceProperties(&props, device), "Couldn't get cuda device properties"); - auto ptr = make_ptr(); + auto ptr = make_ptr(); if(!ptr) { return std::nullopt; } @@ -316,32 +301,13 @@ int sws_t::convert(std::uint8_t *Y, std::uint8_t *UV, std::uint32_t pitchY, std: dim3 block(threadsPerBlock); dim3 grid(div_align(threadsX, threadsPerBlock), threadsY); - RGBA_to_NV12<<>>(texture, Y, UV, pitchY, pitchUV, scale, viewport, (video::color_t *)color_matrix.get()); + RGBA_to_NV12<<>>(texture, Y, UV, pitchY, pitchUV, scale, viewport, (cuda_color_t *)color_matrix.get()); return CU_CHECK_IGNORE(cudaGetLastError(), "RGBA_to_NV12 failed"); } -void sws_t::set_colorspace(std::uint32_t colorspace, std::uint32_t color_range) { - video::color_t *color_p; - switch(colorspace) { - case 5: // SWS_CS_SMPTE170M - color_p = &video::colors[0]; - break; - case 1: // SWS_CS_ITU709 - color_p = &video::colors[2]; - break; - case 9: // SWS_CS_BT2020 - color_p = &video::colors[4]; - break; - default: - color_p = &video::colors[0]; - }; - - if(color_range > 1) { - // Full range - ++color_p; - } - +void sws_t::apply_colorspace(const video::sunshine_colorspace_t& colorspace) { + auto color_p = video::color_vectors_from_colorspace(colorspace); CU_CHECK_IGNORE(cudaMemcpy(color_matrix.get(), color_p, sizeof(video::color_t), cudaMemcpyHostToDevice), "Couldn't copy color matrix to cuda"); } diff --git a/src/platform/linux/cuda.h b/src/platform/linux/cuda.h index e2094d81b25..91564174745 100644 --- a/src/platform/linux/cuda.h +++ b/src/platform/linux/cuda.h @@ -6,6 +6,8 @@ #if defined(SUNSHINE_BUILD_CUDA) + #include "src/video_colorspace.h" + #include #include #include @@ -13,7 +15,7 @@ #include namespace platf { - class hwdevice_t; + class avcodec_encode_device_t; class img_t; } // namespace platf @@ -23,8 +25,20 @@ namespace cuda { std::vector display_names(); } - std::shared_ptr - make_hwdevice(int width, int height, bool vram); + std::unique_ptr + make_avcodec_encode_device(int width, int height, bool vram); + + /** + * @brief Create a GL->CUDA encoding device for consuming captured dmabufs. + * @param in_width Width of captured frames. + * @param in_height Height of captured frames. + * @param offset_x Offset of content in captured frame. + * @param offset_y Offset of content in captured frame. + * @return FFmpeg encoding device context. + */ + std::unique_ptr + make_avcodec_gl_encode_device(int width, int height, int offset_x, int offset_y); + int init(); } // namespace cuda @@ -109,7 +123,7 @@ namespace cuda { convert(std::uint8_t *Y, std::uint8_t *UV, std::uint32_t pitchY, std::uint32_t pitchUV, cudaTextureObject_t texture, stream_t::pointer stream, const viewport_t &viewport); void - set_colorspace(std::uint32_t colorspace, std::uint32_t color_range); + apply_colorspace(const video::sunshine_colorspace_t &colorspace); int load_ram(platf::img_t &img, cudaArray_t array); diff --git a/src/platform/linux/graphics.cpp b/src/platform/linux/graphics.cpp index fcb7ab234be..2cd81dd451e 100644 --- a/src/platform/linux/graphics.cpp +++ b/src/platform/linux/graphics.cpp @@ -3,10 +3,16 @@ * @brief todo */ #include "graphics.h" +#include "src/file_handler.h" +#include "src/logging.h" #include "src/video.h" #include +extern "C" { +#include +} + // I want to have as little build dependencies as possible // There aren't that many DRM_FORMAT I need to use, so define them here // @@ -14,14 +20,11 @@ #define fourcc_code(a, b, c, d) ((std::uint32_t)(a) | ((std::uint32_t)(b) << 8) | \ ((std::uint32_t)(c) << 16) | ((std::uint32_t)(d) << 24)) #define fourcc_mod_code(vendor, val) ((((uint64_t) vendor) << 56) | ((val) &0x00ffffffffffffffULL)) -#define DRM_FORMAT_R8 fourcc_code('R', '8', ' ', ' ') /* [7:0] R */ -#define DRM_FORMAT_GR88 fourcc_code('G', 'R', '8', '8') /* [15:0] G:R 8:8 little endian */ -#define DRM_FORMAT_ARGB8888 fourcc_code('A', 'R', '2', '4') /* [31:0] A:R:G:B 8:8:8:8 little endian */ -#define DRM_FORMAT_XRGB8888 fourcc_code('X', 'R', '2', '4') /* [31:0] x:R:G:B 8:8:8:8 little endian */ -#define DRM_FORMAT_XBGR8888 fourcc_code('X', 'B', '2', '4') /* [31:0] x:B:G:R 8:8:8:8 little endian */ #define DRM_FORMAT_MOD_INVALID fourcc_mod_code(0, ((1ULL << 56) - 1)) -#define SUNSHINE_SHADERS_DIR SUNSHINE_ASSETS_DIR "/shaders/opengl" +#if !defined(SUNSHINE_SHADERS_DIR) // for testing this needs to be defined in cmake as we don't do an install + #define SUNSHINE_SHADERS_DIR SUNSHINE_ASSETS_DIR "/shaders/opengl" +#endif using namespace std::literals; namespace gl { @@ -36,7 +39,7 @@ namespace gl { } tex_t::~tex_t() { - if (!size() == 0) { + if (size() != 0) { ctx.DeleteTextures(size(), begin()); } } @@ -193,7 +196,7 @@ namespace gl { ctx.AttachShader(program.handle(), frag.handle()); // p_handle stores a copy of the program handle, since program will be moved before - // the fail guard funcion is called. + // the fail guard function is called. auto fg = util::fail_guard([p_handle = program.handle(), &vert, &frag]() { ctx.DetachShader(p_handle, vert.handle()); ctx.DetachShader(p_handle, frag.handle()); @@ -500,45 +503,56 @@ namespace egl { return {}; } - std::optional - import_source(display_t::pointer egl_display, const surface_descriptor_t &xrgb) { - EGLAttrib attribs[47]; - int atti = 0; - attribs[atti++] = EGL_WIDTH; - attribs[atti++] = xrgb.width; - attribs[atti++] = EGL_HEIGHT; - attribs[atti++] = xrgb.height; - attribs[atti++] = EGL_LINUX_DRM_FOURCC_EXT; - attribs[atti++] = xrgb.fourcc; + /** + * @brief Returns EGL attributes for eglCreateImage() to import the provided surface. + * @param surface The surface descriptor. + * @return Vector of EGL attributes. + */ + std::vector + surface_descriptor_to_egl_attribs(const surface_descriptor_t &surface) { + std::vector attribs; - for (auto x = 0; x < 4; ++x) { - auto fd = xrgb.fds[x]; + attribs.emplace_back(EGL_WIDTH); + attribs.emplace_back(surface.width); + attribs.emplace_back(EGL_HEIGHT); + attribs.emplace_back(surface.height); + attribs.emplace_back(EGL_LINUX_DRM_FOURCC_EXT); + attribs.emplace_back(surface.fourcc); + for (auto x = 0; x < 4; ++x) { + auto fd = surface.fds[x]; if (fd < 0) { continue; } auto plane_attr = get_plane(x); - attribs[atti++] = plane_attr.fd; - attribs[atti++] = fd; - attribs[atti++] = plane_attr.offset; - attribs[atti++] = xrgb.offsets[x]; - attribs[atti++] = plane_attr.pitch; - attribs[atti++] = xrgb.pitches[x]; - - if (xrgb.modifier != DRM_FORMAT_MOD_INVALID) { - attribs[atti++] = plane_attr.lo; - attribs[atti++] = xrgb.modifier & 0xFFFFFFFF; - attribs[atti++] = plane_attr.hi; - attribs[atti++] = xrgb.modifier >> 32; + attribs.emplace_back(plane_attr.fd); + attribs.emplace_back(fd); + attribs.emplace_back(plane_attr.offset); + attribs.emplace_back(surface.offsets[x]); + attribs.emplace_back(plane_attr.pitch); + attribs.emplace_back(surface.pitches[x]); + + if (surface.modifier != DRM_FORMAT_MOD_INVALID) { + attribs.emplace_back(plane_attr.lo); + attribs.emplace_back(surface.modifier & 0xFFFFFFFF); + attribs.emplace_back(plane_attr.hi); + attribs.emplace_back(surface.modifier >> 32); } } - attribs[atti++] = EGL_NONE; + + attribs.emplace_back(EGL_NONE); + return attribs; + } + + std::optional + import_source(display_t::pointer egl_display, const surface_descriptor_t &xrgb) { + auto attribs = surface_descriptor_to_egl_attribs(xrgb); rgb_t rgb { egl_display, - eglCreateImage(egl_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, attribs), + eglCreateImage(egl_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, attribs.data()), gl::tex_t::make(1) }; @@ -558,37 +572,52 @@ namespace egl { return rgb; } - std::optional - import_target(display_t::pointer egl_display, std::array &&fds, const surface_descriptor_t &r8, const surface_descriptor_t &gr88) { - EGLAttrib img_attr_planes[2][13] { - { EGL_LINUX_DRM_FOURCC_EXT, DRM_FORMAT_R8, - EGL_WIDTH, r8.width, - EGL_HEIGHT, r8.height, - EGL_DMA_BUF_PLANE0_FD_EXT, r8.fds[0], - EGL_DMA_BUF_PLANE0_OFFSET_EXT, r8.offsets[0], - EGL_DMA_BUF_PLANE0_PITCH_EXT, r8.pitches[0], - EGL_NONE }, - - { EGL_LINUX_DRM_FOURCC_EXT, DRM_FORMAT_GR88, - EGL_WIDTH, gr88.width, - EGL_HEIGHT, gr88.height, - EGL_DMA_BUF_PLANE0_FD_EXT, r8.fds[0], - EGL_DMA_BUF_PLANE0_OFFSET_EXT, gr88.offsets[0], - EGL_DMA_BUF_PLANE0_PITCH_EXT, gr88.pitches[0], - EGL_NONE }, + /** + * @brief Creates a black RGB texture of the specified image size. + * @param img The image to use for texture sizing. + * @return The new RGB texture. + */ + rgb_t + create_blank(platf::img_t &img) { + rgb_t rgb { + EGL_NO_DISPLAY, + EGL_NO_IMAGE, + gl::tex_t::make(1) }; + gl::ctx.BindTexture(GL_TEXTURE_2D, rgb->tex[0]); + gl::ctx.TexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, img.width, img.height); + gl::ctx.BindTexture(GL_TEXTURE_2D, 0); + + auto framebuf = gl::frame_buf_t::make(1); + framebuf.bind(&rgb->tex[0], &rgb->tex[0] + 1); + + GLenum attachment = GL_COLOR_ATTACHMENT0; + gl::ctx.DrawBuffers(1, &attachment); + const GLuint rgb_black[] = { 0, 0, 0, 0 }; + gl::ctx.ClearBufferuiv(GL_COLOR, 0, rgb_black); + + gl_drain_errors; + + return rgb; + } + + std::optional + import_target(display_t::pointer egl_display, std::array &&fds, const surface_descriptor_t &y, const surface_descriptor_t &uv) { + auto y_attribs = surface_descriptor_to_egl_attribs(y); + auto uv_attribs = surface_descriptor_to_egl_attribs(uv); + nv12_t nv12 { egl_display, - eglCreateImage(egl_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, img_attr_planes[0]), - eglCreateImage(egl_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, img_attr_planes[1]), + eglCreateImage(egl_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, y_attribs.data()), + eglCreateImage(egl_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, uv_attribs.data()), gl::tex_t::make(2), gl::frame_buf_t::make(2), std::move(fds) }; if (!nv12->r8 || !nv12->bg88) { - BOOST_LOG(error) << "Couldn't create KHR Image"sv; + BOOST_LOG(error) << "Couldn't import YUV target: "sv << util::hex(eglGetError()).to_string_view(); return std::nullopt; } @@ -601,34 +630,96 @@ namespace egl { nv12->buf.bind(std::begin(nv12->tex), std::end(nv12->tex)); + GLenum attachments[] { + GL_COLOR_ATTACHMENT0, + GL_COLOR_ATTACHMENT1 + }; + + for (int x = 0; x < sizeof(attachments) / sizeof(decltype(attachments[0])); ++x) { + gl::ctx.BindFramebuffer(GL_FRAMEBUFFER, nv12->buf[x]); + gl::ctx.DrawBuffers(1, &attachments[x]); + + const float y_black[] = { 0.0f, 0.0f, 0.0f, 0.0f }; + const float uv_black[] = { 0.5f, 0.5f, 0.5f, 0.5f }; + gl::ctx.ClearBufferfv(GL_COLOR, 0, x == 0 ? y_black : uv_black); + } + + gl::ctx.BindFramebuffer(GL_FRAMEBUFFER, 0); + gl_drain_errors; return nv12; } - void - sws_t::set_colorspace(std::uint32_t colorspace, std::uint32_t color_range) { - video::color_t *color_p; - switch (colorspace) { - case 5: // SWS_CS_SMPTE170M - color_p = &video::colors[0]; - break; - case 1: // SWS_CS_ITU709 - color_p = &video::colors[2]; - break; - case 9: // SWS_CS_BT2020 - color_p = &video::colors[4]; - break; - default: - BOOST_LOG(warning) << "Colorspace: ["sv << colorspace << "] not yet supported: switching to default"sv; - color_p = &video::colors[0]; + /** + * @brief Creates biplanar YUV textures to render into. + * @param width Width of the target frame. + * @param height Height of the target frame. + * @param format Format of the target frame. + * @return The new RGB texture. + */ + std::optional + create_target(int width, int height, AVPixelFormat format) { + nv12_t nv12 { + EGL_NO_DISPLAY, + EGL_NO_IMAGE, + EGL_NO_IMAGE, + gl::tex_t::make(2), + gl::frame_buf_t::make(2), + }; + + GLint y_format; + GLint uv_format; + + // Determine the size of each plane element + auto fmt_desc = av_pix_fmt_desc_get(format); + if (fmt_desc->comp[0].depth <= 8) { + y_format = GL_R8; + uv_format = GL_RG8; + } + else if (fmt_desc->comp[0].depth <= 16) { + y_format = GL_R16; + uv_format = GL_RG16; + } + else { + BOOST_LOG(error) << "Unsupported target pixel format: "sv << format; + return std::nullopt; + } + + gl::ctx.BindTexture(GL_TEXTURE_2D, nv12->tex[0]); + gl::ctx.TexStorage2D(GL_TEXTURE_2D, 1, y_format, width, height); + + gl::ctx.BindTexture(GL_TEXTURE_2D, nv12->tex[1]); + gl::ctx.TexStorage2D(GL_TEXTURE_2D, 1, uv_format, + width >> fmt_desc->log2_chroma_w, height >> fmt_desc->log2_chroma_h); + + nv12->buf.bind(std::begin(nv12->tex), std::end(nv12->tex)); + + GLenum attachments[] { + GL_COLOR_ATTACHMENT0, + GL_COLOR_ATTACHMENT1 }; - if (color_range > 1) { - // Full range - ++color_p; + for (int x = 0; x < sizeof(attachments) / sizeof(decltype(attachments[0])); ++x) { + gl::ctx.BindFramebuffer(GL_FRAMEBUFFER, nv12->buf[x]); + gl::ctx.DrawBuffers(1, &attachments[x]); + + const float y_black[] = { 0.0f, 0.0f, 0.0f, 0.0f }; + const float uv_black[] = { 0.5f, 0.5f, 0.5f, 0.5f }; + gl::ctx.ClearBufferfv(GL_COLOR, 0, x == 0 ? y_black : uv_black); } + gl::ctx.BindFramebuffer(GL_FRAMEBUFFER, 0); + + gl_drain_errors; + + return nv12; + } + + void + sws_t::apply_colorspace(const video::sunshine_colorspace_t &colorspace) { + auto color_p = video::color_vectors_from_colorspace(colorspace); + std::string_view members[] { util::view(color_p->color_vec_y), util::view(color_p->color_vec_u), @@ -644,19 +735,19 @@ namespace egl { } std::optional - sws_t::make(int in_width, int in_height, int out_width, int out_heigth, gl::tex_t &&tex) { + sws_t::make(int in_width, int in_height, int out_width, int out_height, gl::tex_t &&tex) { sws_t sws; sws.serial = std::numeric_limits::max(); // Ensure aspect ratio is maintained - auto scalar = std::fminf(out_width / (float) in_width, out_heigth / (float) in_height); + auto scalar = std::fminf(out_width / (float) in_width, out_height / (float) in_height); auto out_width_f = in_width * scalar; auto out_height_f = in_height * scalar; // result is always positive auto offsetX_f = (out_width - out_width_f) / 2; - auto offsetY_f = (out_heigth - out_height_f) / 2; + auto offsetY_f = (out_height - out_height_f) / 2; sws.out_width = out_width_f; sws.out_height = out_height_f; @@ -691,7 +782,7 @@ namespace egl { for (int x = 0; x < count; ++x) { auto &compiled_source = compiled_sources[x]; - compiled_source = gl::shader_t::compile(read_file(sources[x]), shader_type[x % 2]); + compiled_source = gl::shader_t::compile(file_handler::read_file(sources[x]), shader_type[x % 2]); gl_drain_errors; if (compiled_source.has_right()) { @@ -741,7 +832,7 @@ namespace egl { gl::ctx.UseProgram(sws.program[1].handle()); gl::ctx.Uniform1fv(loc_width_i, 1, &width_i); - auto color_p = &video::colors[0]; + auto color_p = video::color_vectors_from_colorspace(video::colorspace_e::rec601, false); std::pair members[] { std::make_pair("color_vec_y", util::view(color_p->color_vec_y)), std::make_pair("color_vec_u", util::view(color_p->color_vec_u)), @@ -788,12 +879,38 @@ namespace egl { } std::optional - sws_t::make(int in_width, int in_height, int out_width, int out_heigth) { + sws_t::make(int in_width, int in_height, int out_width, int out_height, AVPixelFormat format) { + GLint gl_format; + + // Decide the bit depth format of the backing texture based the target frame format + auto fmt_desc = av_pix_fmt_desc_get(format); + switch (fmt_desc->comp[0].depth) { + case 8: + gl_format = GL_RGBA8; + break; + + case 10: + gl_format = GL_RGB10_A2; + break; + + case 12: + gl_format = GL_RGBA12; + break; + + case 16: + gl_format = GL_RGBA16; + break; + + default: + BOOST_LOG(error) << "Unsupported pixel format for EGL frame: "sv << (int) format; + return std::nullopt; + } + auto tex = gl::tex_t::make(2); gl::ctx.BindTexture(GL_TEXTURE_2D, tex[0]); - gl::ctx.TexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, in_width, in_height); + gl::ctx.TexStorage2D(GL_TEXTURE_2D, 1, gl_format, in_width, in_height); - return make(in_width, in_height, out_width, out_heigth, std::move(tex)); + return make(in_width, in_height, out_width, out_height, std::move(tex)); } void @@ -840,7 +957,7 @@ namespace egl { if (serial != img.serial) { serial = img.serial; - gl::ctx.TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, img.width, img.height, 0, GL_BGRA, GL_UNSIGNED_BYTE, img.data); + gl::ctx.TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, img.src_w, img.src_h, 0, GL_BGRA, GL_UNSIGNED_BYTE, img.data); } gl::ctx.Enable(GL_BLEND); diff --git a/src/platform/linux/graphics.h b/src/platform/linux/graphics.h index fbb0e92d3b9..a45b26fd173 100644 --- a/src/platform/linux/graphics.h +++ b/src/platform/linux/graphics.h @@ -11,9 +11,10 @@ #include #include "misc.h" -#include "src/main.h" +#include "src/logging.h" #include "src/platform/common.h" #include "src/utility.h" +#include "src/video_colorspace.h" #define SUNSHINE_STRINGIFY_HELPER(x) #x #define SUNSHINE_STRINGIFY(x) SUNSHINE_STRINGIFY_HELPER(x) @@ -267,15 +268,29 @@ namespace egl { display_t::pointer egl_display, const surface_descriptor_t &xrgb); + rgb_t + create_blank(platf::img_t &img); + std::optional import_target( display_t::pointer egl_display, std::array &&fds, - const surface_descriptor_t &r8, const surface_descriptor_t &gr88); + const surface_descriptor_t &y, const surface_descriptor_t &uv); + + /** + * @brief Creates biplanar YUV textures to render into. + * @param width Width of the target frame. + * @param height Height of the target frame. + * @param format Format of the target frame. + * @return The new RGB texture. + */ + std::optional + create_target(int width, int height, AVPixelFormat format); class cursor_t: public platf::img_t { public: int x, y; + int src_w, src_h; unsigned long serial; @@ -309,9 +324,9 @@ namespace egl { class sws_t { public: static std::optional - make(int in_width, int in_height, int out_width, int out_heigth, gl::tex_t &&tex); + make(int in_width, int in_height, int out_width, int out_height, gl::tex_t &&tex); static std::optional - make(int in_width, int in_height, int out_width, int out_heigth); + make(int in_width, int in_height, int out_width, int out_height, AVPixelFormat format); // Convert the loaded image into the first two framebuffers int @@ -327,7 +342,7 @@ namespace egl { load_vram(img_descriptor_t &img, int offset_x, int offset_y, int texture); void - set_colorspace(std::uint32_t colorspace, std::uint32_t color_range); + apply_colorspace(const video::sunshine_colorspace_t &colorspace); // The first texture is the monitor image. // The second texture is the cursor image diff --git a/src/platform/linux/input.cpp b/src/platform/linux/input.cpp index 85980ef37b5..14ee50fe70b 100644 --- a/src/platform/linux/input.cpp +++ b/src/platform/linux/input.cpp @@ -6,8 +6,10 @@ #include #include +extern "C" { #include #include +} #ifdef SUNSHINE_BUILD_X11 #include @@ -20,8 +22,11 @@ #include #include #include +#include -#include "src/main.h" +#include "src/config.h" +#include "src/input.h" +#include "src/logging.h" #include "src/platform/common.h" #include "src/utility.h" @@ -41,6 +46,8 @@ using namespace std::literals; namespace platf { + static bool has_uinput = false; + #ifdef SUNSHINE_BUILD_X11 namespace x11 { #define _FN(x, ret, args) \ @@ -133,7 +140,7 @@ namespace platf { } }); - using mail_evdev_t = std::tuple; + using mail_evdev_t = std::tuple; struct keycode_t { std::uint32_t keycode; @@ -452,7 +459,7 @@ namespace platf { public: KITTY_DEFAULT_CONSTR_MOVE(effect_t) - effect_t(int gamepadnr, uinput_t::pointer dev, rumble_queue_t &&q): + effect_t(std::uint8_t gamepadnr, uinput_t::pointer dev, feedback_queue_t &&q): gamepadnr { gamepadnr }, dev { dev }, rumble_queue { std::move(q) }, gain { 0xFFFF }, id_to_data {} {} class data_t { @@ -580,8 +587,8 @@ namespace platf { weak_strong += data.rumble(tp); } - std::clamp(weak_strong.first, 0, 0xFFFF); - std::clamp(weak_strong.second, 0, 0xFFFF); + weak_strong.first = std::clamp(weak_strong.first, 0, 0xFFFF); + weak_strong.second = std::clamp(weak_strong.second, 0, 0xFFFF); old_rumble = weak_strong * gain / 0xFFFF; return old_rumble; @@ -628,13 +635,13 @@ namespace platf { BOOST_LOG(debug) << "Removed rumble effect id ["sv << id << ']'; } - // Used as ID for rumble notifications - int gamepadnr; + // Client-relative gamepad index for rumble notifications + std::uint8_t gamepadnr; // Used as ID for adding/removinf devices from evdev notifications uinput_t::pointer dev; - rumble_queue_t rumble_queue; + feedback_queue_t rumble_queue; int gain; @@ -673,14 +680,14 @@ namespace platf { struct input_raw_t { public: void - clear_touchscreen() { - std::filesystem::path touch_path { appdata() / "sunshine_touchscreen"sv }; + clear_mouse_rel() { + std::filesystem::path mouse_path { appdata() / "sunshine_mouse_rel"sv }; - if (std::filesystem::is_symlink(touch_path)) { - std::filesystem::remove(touch_path); + if (std::filesystem::is_symlink(mouse_path)) { + std::filesystem::remove(mouse_path); } - touch_input.reset(); + mouse_rel_input.reset(); } void @@ -695,14 +702,14 @@ namespace platf { } void - clear_mouse() { - std::filesystem::path mouse_path { appdata() / "sunshine_mouse"sv }; + clear_mouse_abs() { + std::filesystem::path mouse_path { appdata() / "sunshine_mouse_abs"sv }; if (std::filesystem::is_symlink(mouse_path)) { std::filesystem::remove(mouse_path); } - mouse_input.reset(); + mouse_abs_input.reset(); } void @@ -729,29 +736,29 @@ namespace platf { } int - create_mouse() { - int err = libevdev_uinput_create_from_device(mouse_dev.get(), LIBEVDEV_UINPUT_OPEN_MANAGED, &mouse_input); + create_mouse_abs() { + int err = libevdev_uinput_create_from_device(mouse_abs_dev.get(), LIBEVDEV_UINPUT_OPEN_MANAGED, &mouse_abs_input); if (err) { - BOOST_LOG(error) << "Could not create Sunshine Mouse: "sv << strerror(-err); + BOOST_LOG(error) << "Could not create Sunshine Mouse (Absolute): "sv << strerror(-err); return -1; } - std::filesystem::create_symlink(libevdev_uinput_get_devnode(mouse_input.get()), appdata() / "sunshine_mouse"sv); + std::filesystem::create_symlink(libevdev_uinput_get_devnode(mouse_abs_input.get()), appdata() / "sunshine_mouse_abs"sv); return 0; } int - create_touchscreen() { - int err = libevdev_uinput_create_from_device(touch_dev.get(), LIBEVDEV_UINPUT_OPEN_MANAGED, &touch_input); + create_mouse_rel() { + int err = libevdev_uinput_create_from_device(mouse_rel_dev.get(), LIBEVDEV_UINPUT_OPEN_MANAGED, &mouse_rel_input); if (err) { - BOOST_LOG(error) << "Could not create Sunshine Touchscreen: "sv << strerror(-err); + BOOST_LOG(error) << "Could not create Sunshine Mouse (Relative): "sv << strerror(-err); return -1; } - std::filesystem::create_symlink(libevdev_uinput_get_devnode(touch_input.get()), appdata() / "sunshine_touchscreen"sv); + std::filesystem::create_symlink(libevdev_uinput_get_devnode(mouse_rel_input.get()), appdata() / "sunshine_mouse_rel"sv); return 0; } @@ -770,9 +777,16 @@ namespace platf { return 0; } + /** + * @brief Creates a new virtual gamepad. + * @param id The gamepad ID. + * @param metadata Controller metadata from client (empty if none provided). + * @param feedback_queue The queue for posting messages back to the client. + * @return 0 on success. + */ int - alloc_gamepad(int nr, rumble_queue_t &&rumble_queue) { - TUPLE_2D_REF(input, gamepad_state, gamepads[nr]); + alloc_gamepad(const gamepad_id_t &id, const gamepad_arrival_t &metadata, feedback_queue_t &&feedback_queue) { + TUPLE_2D_REF(input, gamepad_state, gamepads[id.globalIndex]); int err = libevdev_uinput_create_from_device(gamepad_dev.get(), LIBEVDEV_UINPUT_OPEN_MANAGED, &input); @@ -784,7 +798,7 @@ namespace platf { } std::stringstream ss; - ss << "sunshine_gamepad_"sv << nr; + ss << "sunshine_gamepad_"sv << id.globalIndex; auto gamepad_path = platf::appdata() / ss.str(); if (std::filesystem::is_symlink(gamepad_path)) { @@ -794,9 +808,9 @@ namespace platf { auto dev_node = libevdev_uinput_get_devnode(input.get()); rumble_ctx->rumble_queue_queue.raise( - nr, + id.clientRelativeIndex, input.get(), - std::move(rumble_queue), + std::move(feedback_queue), pollfd_t { dup(libevdev_uinput_get_fd(input.get())), (std::int16_t) POLLIN, @@ -809,9 +823,9 @@ namespace platf { void clear() { - clear_touchscreen(); clear_keyboard(); - clear_mouse(); + clear_mouse_abs(); + clear_mouse_rel(); for (int x = 0; x < gamepads.size(); ++x) { clear_gamepad(x); } @@ -831,14 +845,25 @@ namespace platf { safe::shared_t::ptr_t rumble_ctx; std::vector> gamepads; - uinput_t mouse_input; - uinput_t touch_input; + uinput_t mouse_rel_input; + uinput_t mouse_abs_input; uinput_t keyboard_input; + uint8_t mouse_rel_buttons_down = 0; + uint8_t mouse_abs_buttons_down = 0; + + uinput_t::pointer last_mouse_device_used = nullptr; + uint8_t *last_mouse_device_buttons_down = nullptr; + evdev_t gamepad_dev; - evdev_t touch_dev; - evdev_t mouse_dev; + evdev_t mouse_rel_dev; + evdev_t mouse_abs_dev; evdev_t keyboard_dev; + evdev_t touchscreen_dev; + evdev_t pen_dev; + + int accumulated_vscroll_delta = 0; + int accumulated_hscroll_delta = 0; #ifdef SUNSHINE_BUILD_X11 Display *display; @@ -877,7 +902,7 @@ namespace platf { // on error if (polls_recv[x].revents & (POLLHUP | POLLRDHUP | POLLERR)) { - BOOST_LOG(warning) << "Gamepad ["sv << x << "] file discriptor closed unexpectedly"sv; + BOOST_LOG(warning) << "Gamepad ["sv << x << "] file descriptor closed unexpectedly"sv; polls.erase(poll); effects.erase(effect_it); @@ -1041,9 +1066,9 @@ namespace platf { TUPLE_2D(weak, strong, effect.rumble(now)); if (old_weak != weak || old_strong != strong) { - BOOST_LOG(debug) << "Sending haptic feedback: lowfreq [0x"sv << util::hex(weak).to_string_view() << "]: highfreq [0x"sv << util::hex(strong).to_string_view() << ']'; + BOOST_LOG(debug) << "Sending haptic feedback: lowfreq [0x"sv << util::hex(strong).to_string_view() << "]: highfreq [0x"sv << util::hex(weak).to_string_view() << ']'; - effect.rumble_queue->raise(effect.gamepadnr, weak, strong); + effect.rumble_queue->raise(gamepad_feedback_msg_t::make_rumble(effect.gamepadnr, strong, weak)); } } } @@ -1087,8 +1112,9 @@ namespace platf { */ void abs_mouse(input_t &input, const touch_port_t &touch_port, float x, float y) { - auto touchscreen = ((input_raw_t *) input.get())->touch_input.get(); - if (!touchscreen) { + auto raw = (input_raw_t *) input.get(); + auto mouse_abs = raw->mouse_abs_input.get(); + if (!mouse_abs) { x_abs_mouse(input, x, y); return; } @@ -1096,12 +1122,13 @@ namespace platf { auto scaled_x = (int) std::lround((x + touch_port.offset_x) * ((float) target_touch_port.width / (float) touch_port.width)); auto scaled_y = (int) std::lround((y + touch_port.offset_y) * ((float) target_touch_port.height / (float) touch_port.height)); - libevdev_uinput_write_event(touchscreen, EV_ABS, ABS_X, scaled_x); - libevdev_uinput_write_event(touchscreen, EV_ABS, ABS_Y, scaled_y); - libevdev_uinput_write_event(touchscreen, EV_KEY, BTN_TOOL_FINGER, 1); - libevdev_uinput_write_event(touchscreen, EV_KEY, BTN_TOOL_FINGER, 0); + libevdev_uinput_write_event(mouse_abs, EV_ABS, ABS_X, scaled_x); + libevdev_uinput_write_event(mouse_abs, EV_ABS, ABS_Y, scaled_y); + libevdev_uinput_write_event(mouse_abs, EV_SYN, SYN_REPORT, 0); - libevdev_uinput_write_event(touchscreen, EV_SYN, SYN_REPORT, 0); + // Remember this was the last device we sent input on + raw->last_mouse_device_used = mouse_abs; + raw->last_mouse_device_buttons_down = &raw->mouse_abs_buttons_down; } /** @@ -1140,21 +1167,26 @@ namespace platf { */ void move_mouse(input_t &input, int deltaX, int deltaY) { - auto mouse = ((input_raw_t *) input.get())->mouse_input.get(); - if (!mouse) { + auto raw = (input_raw_t *) input.get(); + auto mouse_rel = raw->mouse_rel_input.get(); + if (!mouse_rel) { x_move_mouse(input, deltaX, deltaY); return; } if (deltaX) { - libevdev_uinput_write_event(mouse, EV_REL, REL_X, deltaX); + libevdev_uinput_write_event(mouse_rel, EV_REL, REL_X, deltaX); } if (deltaY) { - libevdev_uinput_write_event(mouse, EV_REL, REL_Y, deltaY); + libevdev_uinput_write_event(mouse_rel, EV_REL, REL_Y, deltaY); } - libevdev_uinput_write_event(mouse, EV_SYN, SYN_REPORT, 0); + libevdev_uinput_write_event(mouse_rel, EV_SYN, SYN_REPORT, 0); + + // Remember this was the last device we sent input on + raw->last_mouse_device_used = mouse_rel; + raw->last_mouse_device_buttons_down = &raw->mouse_rel_buttons_down; } /** @@ -1213,8 +1245,39 @@ namespace platf { */ void button_mouse(input_t &input, int button, bool release) { - auto mouse = ((input_raw_t *) input.get())->mouse_input.get(); - if (!mouse) { + auto raw = (input_raw_t *) input.get(); + + // We mimic the Linux vmmouse driver here and prefer to send buttons + // on the last mouse device we used. However, we make an exception + // if it's a release event and the button is down on the other device. + uinput_t::pointer chosen_mouse_dev = nullptr; + uint8_t *chosen_mouse_dev_buttons_down = nullptr; + if (release) { + // Prefer to send the release on the mouse with the button down + if (raw->mouse_rel_buttons_down & (1 << button)) { + chosen_mouse_dev = raw->mouse_rel_input.get(); + chosen_mouse_dev_buttons_down = &raw->mouse_rel_buttons_down; + } + else if (raw->mouse_abs_buttons_down & (1 << button)) { + chosen_mouse_dev = raw->mouse_abs_input.get(); + chosen_mouse_dev_buttons_down = &raw->mouse_abs_buttons_down; + } + } + + if (!chosen_mouse_dev) { + if (raw->last_mouse_device_used) { + // Prefer to use the last device we sent motion + chosen_mouse_dev = raw->last_mouse_device_used; + chosen_mouse_dev_buttons_down = raw->last_mouse_device_buttons_down; + } + else { + // Send on the relative device if we have no preference yet + chosen_mouse_dev = raw->mouse_rel_input.get(); + chosen_mouse_dev_buttons_down = &raw->mouse_rel_buttons_down; + } + } + + if (!chosen_mouse_dev) { x_button_mouse(input, button, release); return; } @@ -1243,9 +1306,16 @@ namespace platf { scan = 90005; } - libevdev_uinput_write_event(mouse, EV_MSC, MSC_SCAN, scan); - libevdev_uinput_write_event(mouse, EV_KEY, btn_type, release ? 0 : 1); - libevdev_uinput_write_event(mouse, EV_SYN, SYN_REPORT, 0); + libevdev_uinput_write_event(chosen_mouse_dev, EV_MSC, MSC_SCAN, scan); + libevdev_uinput_write_event(chosen_mouse_dev, EV_KEY, btn_type, release ? 0 : 1); + libevdev_uinput_write_event(chosen_mouse_dev, EV_SYN, SYN_REPORT, 0); + + if (release) { + *chosen_mouse_dev_buttons_down &= ~(1 << button); + } + else { + *chosen_mouse_dev_buttons_down |= (1 << button); + } } /** @@ -1289,17 +1359,26 @@ namespace platf { */ void scroll(input_t &input, int high_res_distance) { - int distance = high_res_distance / 120; + auto raw = ((input_raw_t *) input.get()); - auto mouse = ((input_raw_t *) input.get())->mouse_input.get(); - if (!mouse) { - x_scroll(input, distance, 4, 5); - return; + raw->accumulated_vscroll_delta += high_res_distance; + int full_ticks = raw->accumulated_vscroll_delta / 120; + + // We mimic the Linux vmmouse driver and always send scroll events + // via the relative pointing device for Xorg compatibility. + auto mouse = raw->mouse_rel_input.get(); + if (mouse) { + if (full_ticks) { + libevdev_uinput_write_event(mouse, EV_REL, REL_WHEEL, full_ticks); + } + libevdev_uinput_write_event(mouse, EV_REL, REL_WHEEL_HI_RES, high_res_distance); + libevdev_uinput_write_event(mouse, EV_SYN, SYN_REPORT, 0); + } + else if (full_ticks) { + x_scroll(input, full_ticks, 4, 5); } - libevdev_uinput_write_event(mouse, EV_REL, REL_WHEEL, distance); - libevdev_uinput_write_event(mouse, EV_REL, REL_WHEEL_HI_RES, high_res_distance); - libevdev_uinput_write_event(mouse, EV_SYN, SYN_REPORT, 0); + raw->accumulated_vscroll_delta -= full_ticks * 120; } /** @@ -1314,17 +1393,26 @@ namespace platf { */ void hscroll(input_t &input, int high_res_distance) { - int distance = high_res_distance / 120; + auto raw = ((input_raw_t *) input.get()); - auto mouse = ((input_raw_t *) input.get())->mouse_input.get(); - if (!mouse) { - x_scroll(input, distance, 6, 7); - return; + raw->accumulated_hscroll_delta += high_res_distance; + int full_ticks = raw->accumulated_hscroll_delta / 120; + + // We mimic the Linux vmmouse driver and always send scroll events + // via the relative pointing device for Xorg compatibility. + auto mouse_rel = raw->mouse_rel_input.get(); + if (mouse_rel) { + if (full_ticks) { + libevdev_uinput_write_event(mouse_rel, EV_REL, REL_HWHEEL, full_ticks); + } + libevdev_uinput_write_event(mouse_rel, EV_REL, REL_HWHEEL_HI_RES, high_res_distance); + libevdev_uinput_write_event(mouse_rel, EV_SYN, SYN_REPORT, 0); + } + else if (full_ticks) { + x_scroll(input, full_ticks, 6, 7); } - libevdev_uinput_write_event(mouse, EV_REL, REL_HWHEEL, distance); - libevdev_uinput_write_event(mouse, EV_REL, REL_HWHEEL_HI_RES, high_res_distance); - libevdev_uinput_write_event(mouse, EV_SYN, SYN_REPORT, 0); + raw->accumulated_hscroll_delta -= full_ticks * 120; } static keycode_t @@ -1422,7 +1510,7 @@ namespace platf { std::stringstream ss; ss << std::hex << std::setfill('0'); for (const auto &ch : str) { - ss << ch; + ss << static_cast(ch); } std::string hex_unicode(ss.str()); @@ -1480,9 +1568,17 @@ namespace platf { keyboard_ev(kb, KEY_LEFTCTRL, 0); } + /** + * @brief Creates a new virtual gamepad. + * @param input The global input context. + * @param id The gamepad ID. + * @param metadata Controller metadata from client (empty if none provided). + * @param feedback_queue The queue for posting messages back to the client. + * @return 0 on success. + */ int - alloc_gamepad(input_t &input, int nr, rumble_queue_t rumble_queue) { - return ((input_raw_t *) input.get())->alloc_gamepad(nr, std::move(rumble_queue)); + alloc_gamepad(input_t &input, const gamepad_id_t &id, const gamepad_arrival_t &metadata, feedback_queue_t feedback_queue) { + return ((input_raw_t *) input.get())->alloc_gamepad(id, metadata, std::move(feedback_queue)); } void @@ -1517,7 +1613,7 @@ namespace platf { if (RIGHT_STICK & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_THUMBR, bf_new & RIGHT_STICK ? 1 : 0); if (LEFT_BUTTON & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_TL, bf_new & LEFT_BUTTON ? 1 : 0); if (RIGHT_BUTTON & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_TR, bf_new & RIGHT_BUTTON ? 1 : 0); - if (HOME & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_MODE, bf_new & HOME ? 1 : 0); + if ((HOME | MISC_BUTTON) & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_MODE, bf_new & (HOME | MISC_BUTTON) ? 1 : 0); if (A & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_SOUTH, bf_new & A ? 1 : 0); if (B & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_EAST, bf_new & B ? 1 : 0); if (X & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_NORTH, bf_new & X ? 1 : 0); @@ -1552,6 +1648,446 @@ namespace platf { libevdev_uinput_write_event(uinput.get(), EV_SYN, SYN_REPORT, 0); } + constexpr auto NUM_TOUCH_SLOTS = 10; + constexpr auto DISTANCE_MAX = 1024; + constexpr auto PRESSURE_MAX = 4096; + constexpr int64_t INVALID_TRACKING_ID = -1; + + // HACK: Contacts with very small pressure values get discarded by libinput, but + // we assume that the client has already excluded such errant touches. We enforce + // a minimum pressure value to prevent our touches from being discarded. + constexpr auto PRESSURE_MIN = 0.10f; + + struct client_input_raw_t: public client_input_t { + client_input_raw_t(input_t &input) { + global = (input_raw_t *) input.get(); + touch_slots.fill(INVALID_TRACKING_ID); + } + + input_raw_t *global; + + // Device state and handles for pen and touch input must be stored in the per-client + // input context, because each connected client may be sending their own independent + // pen/touch events. To maintain separation, we expose separate pen and touch devices + // for each client. + + // Mapping of ABS_MT_SLOT/ABS_MT_TRACKING_ID -> pointerId + std::array touch_slots; + uinput_t touch_input; + uinput_t pen_input; + }; + + /** + * @brief Allocates a context to store per-client input data. + * @param input The global input context. + * @return A unique pointer to a per-client input data context. + */ + std::unique_ptr + allocate_client_input_context(input_t &input) { + return std::make_unique(input); + } + + /** + * @brief Retrieves the slot index for a given pointer ID. + * @param input The client-specific input context. + * @param pointerId The pointer ID sent from the client. + * @return Slot index or -1 if not found. + */ + int + slot_index_by_pointer_id(client_input_raw_t *input, uint32_t pointerId) { + for (int i = 0; i < input->touch_slots.size(); i++) { + if (input->touch_slots[i] == pointerId) { + return i; + } + } + return -1; + } + + /** + * @brief Reserves a slot index for a new pointer ID. + * @param input The client-specific input context. + * @param pointerId The pointer ID sent from the client. + * @return Slot index or -1 if no unallocated slots remain. + */ + int + allocate_slot_index_for_pointer_id(client_input_raw_t *input, uint32_t pointerId) { + int i = slot_index_by_pointer_id(input, pointerId); + if (i >= 0) { + BOOST_LOG(warning) << "Pointer "sv << pointerId << " already down. Did the client drop an up/cancel event?"sv; + return i; + } + + for (int i = 0; i < input->touch_slots.size(); i++) { + if (input->touch_slots[i] == INVALID_TRACKING_ID) { + input->touch_slots[i] = pointerId; + return i; + } + } + + return -1; + } + + /** + * @brief Sends a touch event to the OS. + * @param input The client-specific input context. + * @param touch_port The current viewport for translating to screen coordinates. + * @param touch The touch event. + */ + void + touch(client_input_t *input, const touch_port_t &touch_port, const touch_input_t &touch) { + auto raw = (client_input_raw_t *) input; + + if (!raw->touch_input) { + int err = libevdev_uinput_create_from_device(raw->global->touchscreen_dev.get(), LIBEVDEV_UINPUT_OPEN_MANAGED, &raw->touch_input); + if (err) { + BOOST_LOG(error) << "Could not create Sunshine Touchscreen: "sv << strerror(-err); + return; + } + } + + auto touch_input = raw->touch_input.get(); + + float pressure = std::max(PRESSURE_MIN, touch.pressureOrDistance); + + if (touch.eventType == LI_TOUCH_EVENT_CANCEL_ALL) { + for (int i = 0; i < raw->touch_slots.size(); i++) { + libevdev_uinput_write_event(touch_input, EV_ABS, ABS_MT_SLOT, i); + libevdev_uinput_write_event(touch_input, EV_ABS, ABS_MT_TRACKING_ID, -1); + } + raw->touch_slots.fill(INVALID_TRACKING_ID); + + libevdev_uinput_write_event(touch_input, EV_KEY, BTN_TOUCH, 0); + libevdev_uinput_write_event(touch_input, EV_ABS, ABS_PRESSURE, 0); + libevdev_uinput_write_event(touch_input, EV_SYN, SYN_REPORT, 0); + return; + } + + if (touch.eventType == LI_TOUCH_EVENT_CANCEL) { + // Stop tracking this slot + auto slot_index = slot_index_by_pointer_id(raw, touch.pointerId); + if (slot_index >= 0) { + libevdev_uinput_write_event(touch_input, EV_ABS, ABS_MT_SLOT, slot_index); + libevdev_uinput_write_event(touch_input, EV_ABS, ABS_MT_TRACKING_ID, -1); + + raw->touch_slots[slot_index] = INVALID_TRACKING_ID; + + // Raise BTN_TOUCH if no touches are down + if (std::all_of(raw->touch_slots.cbegin(), raw->touch_slots.cend(), + [](uint64_t pointer_id) { return pointer_id == INVALID_TRACKING_ID; })) { + libevdev_uinput_write_event(touch_input, EV_KEY, BTN_TOUCH, 0); + + // This may have been the final slot down which was also being emulated + // through the single-touch axes. Reset ABS_PRESSURE to ensure code that + // uses ABS_PRESSURE instead of BTN_TOUCH will work properly. + libevdev_uinput_write_event(touch_input, EV_ABS, ABS_PRESSURE, 0); + } + } + } + else if (touch.eventType == LI_TOUCH_EVENT_DOWN || + touch.eventType == LI_TOUCH_EVENT_MOVE || + touch.eventType == LI_TOUCH_EVENT_UP) { + int slot_index; + if (touch.eventType == LI_TOUCH_EVENT_DOWN) { + // Allocate a new slot for this new touch + slot_index = allocate_slot_index_for_pointer_id(raw, touch.pointerId); + if (slot_index < 0) { + BOOST_LOG(error) << "No unused pointer entries! Cancelling all active touches!"sv; + + for (int i = 0; i < raw->touch_slots.size(); i++) { + libevdev_uinput_write_event(touch_input, EV_ABS, ABS_MT_SLOT, i); + libevdev_uinput_write_event(touch_input, EV_ABS, ABS_MT_TRACKING_ID, -1); + } + raw->touch_slots.fill(INVALID_TRACKING_ID); + + libevdev_uinput_write_event(touch_input, EV_KEY, BTN_TOUCH, 0); + libevdev_uinput_write_event(touch_input, EV_ABS, ABS_PRESSURE, 0); + libevdev_uinput_write_event(touch_input, EV_SYN, SYN_REPORT, 0); + + // All slots are clear, so this should never fail on the second try + slot_index = allocate_slot_index_for_pointer_id(raw, touch.pointerId); + assert(slot_index >= 0); + } + } + else { + // Lookup the slot of the previous touch with this pointer ID + slot_index = slot_index_by_pointer_id(raw, touch.pointerId); + if (slot_index < 0) { + BOOST_LOG(warning) << "Pointer "sv << touch.pointerId << " is not down. Did the client drop a down event?"sv; + return; + } + } + + libevdev_uinput_write_event(touch_input, EV_ABS, ABS_MT_SLOT, slot_index); + + if (touch.eventType == LI_TOUCH_EVENT_UP) { + // Stop tracking this touch + libevdev_uinput_write_event(touch_input, EV_ABS, ABS_MT_TRACKING_ID, -1); + raw->touch_slots[slot_index] = INVALID_TRACKING_ID; + + // Raise BTN_TOUCH if no touches are down + if (std::all_of(raw->touch_slots.cbegin(), raw->touch_slots.cend(), + [](uint64_t pointer_id) { return pointer_id == INVALID_TRACKING_ID; })) { + libevdev_uinput_write_event(touch_input, EV_KEY, BTN_TOUCH, 0); + + // This may have been the final slot down which was also being emulated + // through the single-touch axes. Reset ABS_PRESSURE to ensure code that + // uses ABS_PRESSURE instead of BTN_TOUCH will work properly. + libevdev_uinput_write_event(touch_input, EV_ABS, ABS_PRESSURE, 0); + } + } + else { + float x = touch.x * touch_port.width; + float y = touch.y * touch_port.height; + + auto scaled_x = (int) std::lround((x + touch_port.offset_x) * ((float) target_touch_port.width / (float) touch_port.width)); + auto scaled_y = (int) std::lround((y + touch_port.offset_y) * ((float) target_touch_port.height / (float) touch_port.height)); + + libevdev_uinput_write_event(touch_input, EV_ABS, ABS_MT_TRACKING_ID, slot_index); + libevdev_uinput_write_event(touch_input, EV_ABS, ABS_MT_POSITION_X, scaled_x); + libevdev_uinput_write_event(touch_input, EV_ABS, ABS_MT_POSITION_Y, scaled_y); + + if (touch.pressureOrDistance) { + libevdev_uinput_write_event(touch_input, EV_ABS, ABS_MT_PRESSURE, PRESSURE_MAX * pressure); + } + else if (touch.eventType == LI_TOUCH_EVENT_DOWN) { + // Always report some moderate pressure value when down + libevdev_uinput_write_event(touch_input, EV_ABS, ABS_MT_PRESSURE, PRESSURE_MAX / 2); + } + + if (touch.rotation != LI_ROT_UNKNOWN) { + // Convert our 0..360 range to -90..90 relative to Y axis + int adjusted_angle = touch.rotation; + + if (touch.rotation > 90 && touch.rotation < 270) { + // Lower hemisphere + adjusted_angle = 180 - adjusted_angle; + } + + // Wrap the value if it's out of range + if (adjusted_angle > 90) { + adjusted_angle -= 360; + } + else if (adjusted_angle < -90) { + adjusted_angle += 360; + } + + libevdev_uinput_write_event(touch_input, EV_ABS, ABS_MT_ORIENTATION, adjusted_angle); + } + + if (touch.contactAreaMajor) { + // Contact area comes from the input core scaled to the provided touch_port, + // however we need it rescaled to target_touch_port instead. + auto target_scaled_contact_area = input::scale_client_contact_area( + { touch.contactAreaMajor * 65535.f, touch.contactAreaMinor * 65535.f }, + touch.rotation, + { target_touch_port.width / (touch_port.width * 65535.f), + target_touch_port.height / (touch_port.height * 65535.f) }); + + libevdev_uinput_write_event(touch_input, EV_ABS, ABS_MT_TOUCH_MAJOR, target_scaled_contact_area.first); + + // scale_client_contact_area() will treat the contact area as circular (major == minor) + // if the minor axis wasn't specified, so we unconditionally report ABS_MT_TOUCH_MINOR. + libevdev_uinput_write_event(touch_input, EV_ABS, ABS_MT_TOUCH_MINOR, target_scaled_contact_area.second); + } + + // If this slot is the first active one, send our data through the single touch axes as well + for (int i = 0; i <= slot_index; i++) { + if (raw->touch_slots[i] != INVALID_TRACKING_ID) { + if (i == slot_index) { + libevdev_uinput_write_event(touch_input, EV_ABS, ABS_X, scaled_x); + libevdev_uinput_write_event(touch_input, EV_ABS, ABS_Y, scaled_y); + if (touch.pressureOrDistance) { + libevdev_uinput_write_event(touch_input, EV_ABS, ABS_PRESSURE, PRESSURE_MAX * pressure); + } + else if (touch.eventType == LI_TOUCH_EVENT_DOWN) { + libevdev_uinput_write_event(touch_input, EV_ABS, ABS_PRESSURE, PRESSURE_MAX / 2); + } + } + break; + } + } + } + + libevdev_uinput_write_event(touch_input, EV_SYN, SYN_REPORT, 0); + } + } + + /** + * @brief Sends a pen event to the OS. + * @param input The client-specific input context. + * @param touch_port The current viewport for translating to screen coordinates. + * @param pen The pen event. + */ + void + pen(client_input_t *input, const touch_port_t &touch_port, const pen_input_t &pen) { + auto raw = (client_input_raw_t *) input; + + if (!raw->pen_input) { + int err = libevdev_uinput_create_from_device(raw->global->pen_dev.get(), LIBEVDEV_UINPUT_OPEN_MANAGED, &raw->pen_input); + if (err) { + BOOST_LOG(error) << "Could not create Sunshine Pen: "sv << strerror(-err); + return; + } + } + + auto pen_input = raw->pen_input.get(); + + float x = pen.x * touch_port.width; + float y = pen.y * touch_port.height; + float pressure = std::max(PRESSURE_MIN, pen.pressureOrDistance); + + auto scaled_x = (int) std::lround((x + touch_port.offset_x) * ((float) target_touch_port.width / (float) touch_port.width)); + auto scaled_y = (int) std::lround((y + touch_port.offset_y) * ((float) target_touch_port.height / (float) touch_port.height)); + + // First, process location updates for applicable events + switch (pen.eventType) { + case LI_TOUCH_EVENT_HOVER: + libevdev_uinput_write_event(pen_input, EV_ABS, ABS_X, scaled_x); + libevdev_uinput_write_event(pen_input, EV_ABS, ABS_Y, scaled_y); + + libevdev_uinput_write_event(pen_input, EV_ABS, ABS_PRESSURE, 0); + if (pen.pressureOrDistance) { + libevdev_uinput_write_event(pen_input, EV_ABS, ABS_DISTANCE, DISTANCE_MAX * pen.pressureOrDistance); + } + else { + // Always report some moderate distance value when hovering to ensure hovering + // can be detected properly by code that uses ABS_DISTANCE. + libevdev_uinput_write_event(pen_input, EV_ABS, ABS_DISTANCE, DISTANCE_MAX / 2); + } + break; + + case LI_TOUCH_EVENT_DOWN: + libevdev_uinput_write_event(pen_input, EV_ABS, ABS_X, scaled_x); + libevdev_uinput_write_event(pen_input, EV_ABS, ABS_Y, scaled_y); + + libevdev_uinput_write_event(pen_input, EV_ABS, ABS_DISTANCE, 0); + libevdev_uinput_write_event(pen_input, EV_ABS, ABS_PRESSURE, PRESSURE_MAX * pressure); + break; + + case LI_TOUCH_EVENT_UP: + libevdev_uinput_write_event(pen_input, EV_ABS, ABS_X, scaled_x); + libevdev_uinput_write_event(pen_input, EV_ABS, ABS_Y, scaled_y); + + libevdev_uinput_write_event(pen_input, EV_ABS, ABS_PRESSURE, 0); + break; + + case LI_TOUCH_EVENT_MOVE: + libevdev_uinput_write_event(pen_input, EV_ABS, ABS_X, scaled_x); + libevdev_uinput_write_event(pen_input, EV_ABS, ABS_Y, scaled_y); + + // Update the pressure value if it's present, otherwise leave the default/previous value alone + if (pen.pressureOrDistance) { + libevdev_uinput_write_event(pen_input, EV_ABS, ABS_PRESSURE, PRESSURE_MAX * pressure); + } + break; + } + + if (pen.contactAreaMajor) { + // Contact area comes from the input core scaled to the provided touch_port, + // however we need it rescaled to target_touch_port instead. + auto target_scaled_contact_area = input::scale_client_contact_area( + { pen.contactAreaMajor * 65535.f, pen.contactAreaMinor * 65535.f }, + pen.rotation, + { target_touch_port.width / (touch_port.width * 65535.f), + target_touch_port.height / (touch_port.height * 65535.f) }); + + // ABS_TOOL_WIDTH assumes a circular tool, so we just report the major axis + libevdev_uinput_write_event(pen_input, EV_ABS, ABS_TOOL_WIDTH, target_scaled_contact_area.first); + } + + // We require rotation and tilt to perform the conversion to X and Y tilt angles + if (pen.tilt != LI_TILT_UNKNOWN && pen.rotation != LI_ROT_UNKNOWN) { + auto rotation_rads = pen.rotation * (M_PI / 180.f); + auto tilt_rads = pen.tilt * (M_PI / 180.f); + auto r = std::sin(tilt_rads); + auto z = std::cos(tilt_rads); + + // Convert polar coordinates into X and Y tilt angles + libevdev_uinput_write_event(pen_input, EV_ABS, ABS_TILT_X, std::atan2(std::sin(-rotation_rads) * r, z) * 180.f / M_PI); + libevdev_uinput_write_event(pen_input, EV_ABS, ABS_TILT_Y, std::atan2(std::cos(-rotation_rads) * r, z) * 180.f / M_PI); + } + + // Don't update tool type if we're cancelling or ending a touch/hover + if (pen.eventType != LI_TOUCH_EVENT_CANCEL && + pen.eventType != LI_TOUCH_EVENT_CANCEL_ALL && + pen.eventType != LI_TOUCH_EVENT_HOVER_LEAVE && + pen.eventType != LI_TOUCH_EVENT_UP) { + // Update the tool type if it is known + switch (pen.toolType) { + default: + // We need to have _some_ tool type set, otherwise there's no way to know a tool is in + // range when hovering. If we don't know the type of tool, let's assume it's a pen. + if (pen.eventType != LI_TOUCH_EVENT_DOWN && pen.eventType != LI_TOUCH_EVENT_HOVER) { + break; + } + // fall-through + case LI_TOOL_TYPE_PEN: + libevdev_uinput_write_event(pen_input, EV_KEY, BTN_TOOL_RUBBER, 0); + libevdev_uinput_write_event(pen_input, EV_KEY, BTN_TOOL_PEN, 1); + break; + case LI_TOOL_TYPE_ERASER: + libevdev_uinput_write_event(pen_input, EV_KEY, BTN_TOOL_PEN, 0); + libevdev_uinput_write_event(pen_input, EV_KEY, BTN_TOOL_RUBBER, 1); + break; + } + } + + // Next, process touch state changes + switch (pen.eventType) { + case LI_TOUCH_EVENT_CANCEL: + case LI_TOUCH_EVENT_CANCEL_ALL: + case LI_TOUCH_EVENT_HOVER_LEAVE: + case LI_TOUCH_EVENT_UP: + libevdev_uinput_write_event(pen_input, EV_KEY, BTN_TOUCH, 0); + + // Leaving hover range is detected by all BTN_TOOL_* being cleared + libevdev_uinput_write_event(pen_input, EV_KEY, BTN_TOOL_PEN, 0); + libevdev_uinput_write_event(pen_input, EV_KEY, BTN_TOOL_RUBBER, 0); + break; + + case LI_TOUCH_EVENT_DOWN: + libevdev_uinput_write_event(pen_input, EV_KEY, BTN_TOUCH, 1); + break; + } + + // Finally, process pen buttons + libevdev_uinput_write_event(pen_input, EV_KEY, BTN_STYLUS, !!(pen.penButtons & LI_PEN_BUTTON_PRIMARY)); + libevdev_uinput_write_event(pen_input, EV_KEY, BTN_STYLUS2, !!(pen.penButtons & LI_PEN_BUTTON_SECONDARY)); + libevdev_uinput_write_event(pen_input, EV_KEY, BTN_STYLUS3, !!(pen.penButtons & LI_PEN_BUTTON_TERTIARY)); + + libevdev_uinput_write_event(pen_input, EV_SYN, SYN_REPORT, 0); + } + + /** + * @brief Sends a gamepad touch event to the OS. + * @param input The global input context. + * @param touch The touch event. + */ + void + gamepad_touch(input_t &input, const gamepad_touch_t &touch) { + // Unimplemented feature - platform_caps::controller_touch + } + + /** + * @brief Sends a gamepad motion event to the OS. + * @param input The global input context. + * @param motion The motion event. + */ + void + gamepad_motion(input_t &input, const gamepad_motion_t &motion) { + // Unimplemented + } + + /** + * @brief Sends a gamepad battery event to the OS. + * @param input The global input context. + * @param battery The battery event. + */ + void + gamepad_battery(input_t &input, const gamepad_battery_t &battery) { + // Unimplemented + } + /** * @brief Initialize a new keyboard and return it. * @@ -1582,18 +2118,18 @@ namespace platf { } /** - * @brief Initialize a new `uinput` virtual mouse and return it. + * @brief Initialize a new `uinput` virtual relative mouse and return it. * * EXAMPLES: * ```cpp - * auto my_mouse = mouse(); + * auto my_mouse = mouse_rel(); * ``` */ evdev_t - mouse() { + mouse_rel() { evdev_t dev { libevdev_new() }; - libevdev_set_uniq(dev.get(), "Sunshine Mouse"); + libevdev_set_uniq(dev.get(), "Sunshine Mouse (Rel)"); libevdev_set_id_product(dev.get(), 0x4038); libevdev_set_id_vendor(dev.get(), 0x46D); libevdev_set_id_bustype(dev.get(), 0x3); @@ -1633,30 +2169,35 @@ namespace platf { } /** - * @brief Initialize a new `uinput` virtual touchscreen and return it. + * @brief Initialize a new `uinput` virtual absolute mouse and return it. * * EXAMPLES: * ```cpp - * auto my_touchscreen = touchscreen(); + * auto my_mouse = mouse_abs(); * ``` */ evdev_t - touchscreen() { + mouse_abs() { evdev_t dev { libevdev_new() }; - libevdev_set_uniq(dev.get(), "Sunshine Touch"); + libevdev_set_uniq(dev.get(), "Sunshine Mouse (Abs)"); libevdev_set_id_product(dev.get(), 0xDEAD); libevdev_set_id_vendor(dev.get(), 0xBEEF); libevdev_set_id_bustype(dev.get(), 0x3); libevdev_set_id_version(dev.get(), 0x111); - libevdev_set_name(dev.get(), "Touchscreen passthrough"); + libevdev_set_name(dev.get(), "Mouse passthrough"); libevdev_enable_property(dev.get(), INPUT_PROP_DIRECT); libevdev_enable_event_type(dev.get(), EV_KEY); - libevdev_enable_event_code(dev.get(), EV_KEY, BTN_TOUCH, nullptr); - libevdev_enable_event_code(dev.get(), EV_KEY, BTN_TOOL_PEN, nullptr); // Needed to be enabled for BTN_TOOL_FINGER to work. - libevdev_enable_event_code(dev.get(), EV_KEY, BTN_TOOL_FINGER, nullptr); + libevdev_enable_event_code(dev.get(), EV_KEY, BTN_LEFT, nullptr); + libevdev_enable_event_code(dev.get(), EV_KEY, BTN_RIGHT, nullptr); + libevdev_enable_event_code(dev.get(), EV_KEY, BTN_MIDDLE, nullptr); + libevdev_enable_event_code(dev.get(), EV_KEY, BTN_SIDE, nullptr); + libevdev_enable_event_code(dev.get(), EV_KEY, BTN_EXTRA, nullptr); + + libevdev_enable_event_type(dev.get(), EV_MSC); + libevdev_enable_event_code(dev.get(), EV_MSC, MSC_SCAN, nullptr); input_absinfo absx { 0, @@ -1682,6 +2223,212 @@ namespace platf { return dev; } + /** + * @brief Initialize a new `uinput` virtual touchscreen and return it. + * + * EXAMPLES: + * ```cpp + * auto my_touchscreen = touchscreen(); + * ``` + */ + evdev_t + touchscreen() { + evdev_t dev { libevdev_new() }; + + libevdev_set_uniq(dev.get(), "Sunshine Touchscreen"); + libevdev_set_id_product(dev.get(), 0xDEAD); + libevdev_set_id_vendor(dev.get(), 0xBEEF); + libevdev_set_id_bustype(dev.get(), 0x3); + libevdev_set_id_version(dev.get(), 0x111); + libevdev_set_name(dev.get(), "Touch passthrough"); + + libevdev_enable_property(dev.get(), INPUT_PROP_DIRECT); + + constexpr auto RESOLUTION = 28; + + input_absinfo abs_slot { + 0, + 0, + NUM_TOUCH_SLOTS - 1, + 0, + 0, + 0 + }; + + input_absinfo abs_tracking_id { + 0, + 0, + NUM_TOUCH_SLOTS - 1, + 0, + 0, + 0 + }; + + input_absinfo abs_x { + 0, + 0, + target_touch_port.width, + 1, + 0, + RESOLUTION + }; + + input_absinfo abs_y { + 0, + 0, + target_touch_port.height, + 1, + 0, + RESOLUTION + }; + + input_absinfo abs_pressure { + 0, + 0, + PRESSURE_MAX, + 0, + 0, + 0 + }; + + // Degrees of a half revolution + input_absinfo abs_orientation { + 0, + -90, + 90, + 0, + 0, + 0 + }; + + // Fractions of the full diagonal + input_absinfo abs_contact_area { + 0, + 0, + (__s32) std::sqrt(std::pow(target_touch_port.width, 2) + std::pow(target_touch_port.height, 2)), + 1, + 0, + RESOLUTION + }; + + libevdev_enable_event_type(dev.get(), EV_ABS); + libevdev_enable_event_code(dev.get(), EV_ABS, ABS_X, &abs_x); + libevdev_enable_event_code(dev.get(), EV_ABS, ABS_Y, &abs_y); + libevdev_enable_event_code(dev.get(), EV_ABS, ABS_PRESSURE, &abs_pressure); + libevdev_enable_event_code(dev.get(), EV_ABS, ABS_MT_SLOT, &abs_slot); + libevdev_enable_event_code(dev.get(), EV_ABS, ABS_MT_TRACKING_ID, &abs_tracking_id); + libevdev_enable_event_code(dev.get(), EV_ABS, ABS_MT_POSITION_X, &abs_x); + libevdev_enable_event_code(dev.get(), EV_ABS, ABS_MT_POSITION_Y, &abs_y); + libevdev_enable_event_code(dev.get(), EV_ABS, ABS_MT_PRESSURE, &abs_pressure); + libevdev_enable_event_code(dev.get(), EV_ABS, ABS_MT_ORIENTATION, &abs_orientation); + libevdev_enable_event_code(dev.get(), EV_ABS, ABS_MT_TOUCH_MAJOR, &abs_contact_area); + libevdev_enable_event_code(dev.get(), EV_ABS, ABS_MT_TOUCH_MINOR, &abs_contact_area); + + libevdev_enable_event_type(dev.get(), EV_KEY); + libevdev_enable_event_code(dev.get(), EV_KEY, BTN_TOUCH, nullptr); + + return dev; + } + + /** + * @brief Initialize a new `uinput` virtual pen pad and return it. + * + * EXAMPLES: + * ```cpp + * auto my_penpad = penpad(); + * ``` + */ + evdev_t + penpad() { + evdev_t dev { libevdev_new() }; + + libevdev_set_uniq(dev.get(), "Sunshine Pen"); + libevdev_set_id_product(dev.get(), 0xDEAD); + libevdev_set_id_vendor(dev.get(), 0xBEEF); + libevdev_set_id_bustype(dev.get(), 0x3); + libevdev_set_id_version(dev.get(), 0x111); + libevdev_set_name(dev.get(), "Pen passthrough"); + + libevdev_enable_property(dev.get(), INPUT_PROP_DIRECT); + + constexpr auto RESOLUTION = 28; + + input_absinfo abs_x { + 0, + 0, + target_touch_port.width, + 1, + 0, + RESOLUTION + }; + + input_absinfo abs_y { + 0, + 0, + target_touch_port.height, + 1, + 0, + RESOLUTION + }; + + input_absinfo abs_pressure { + 0, + 0, + PRESSURE_MAX, + 0, + 0, + 0 + }; + + input_absinfo abs_distance { + 0, + 0, + DISTANCE_MAX, + 0, + 0, + 0 + }; + + // Degrees of tilt + input_absinfo abs_tilt { + 0, + -90, + 90, + 0, + 0, + 0 + }; + + // Fractions of the full diagonal + input_absinfo abs_contact_area { + 0, + 0, + (__s32) std::sqrt(std::pow(target_touch_port.width, 2) + std::pow(target_touch_port.height, 2)), + 1, + 0, + RESOLUTION + }; + + libevdev_enable_event_type(dev.get(), EV_ABS); + libevdev_enable_event_code(dev.get(), EV_ABS, ABS_X, &abs_x); + libevdev_enable_event_code(dev.get(), EV_ABS, ABS_Y, &abs_y); + libevdev_enable_event_code(dev.get(), EV_ABS, ABS_PRESSURE, &abs_pressure); + libevdev_enable_event_code(dev.get(), EV_ABS, ABS_DISTANCE, &abs_distance); + libevdev_enable_event_code(dev.get(), EV_ABS, ABS_TILT_X, &abs_tilt); + libevdev_enable_event_code(dev.get(), EV_ABS, ABS_TILT_Y, &abs_tilt); + libevdev_enable_event_code(dev.get(), EV_ABS, ABS_TOOL_WIDTH, &abs_contact_area); + + libevdev_enable_event_type(dev.get(), EV_KEY); + libevdev_enable_event_code(dev.get(), EV_KEY, BTN_TOUCH, nullptr); + libevdev_enable_event_code(dev.get(), EV_KEY, BTN_TOOL_PEN, nullptr); + libevdev_enable_event_code(dev.get(), EV_KEY, BTN_TOOL_RUBBER, nullptr); + libevdev_enable_event_code(dev.get(), EV_KEY, BTN_STYLUS, nullptr); + libevdev_enable_event_code(dev.get(), EV_KEY, BTN_STYLUS2, nullptr); + libevdev_enable_event_code(dev.get(), EV_KEY, BTN_STYLUS3, nullptr); + + return dev; + } + /** * @brief Initialize a new `uinput` virtual X360 gamepad and return it. * @@ -1779,29 +2526,34 @@ namespace platf { // Ensure starting from clean slate gp.clear(); gp.keyboard_dev = keyboard(); - gp.touch_dev = touchscreen(); - gp.mouse_dev = mouse(); + gp.mouse_rel_dev = mouse_rel(); + gp.mouse_abs_dev = mouse_abs(); + gp.touchscreen_dev = touchscreen(); + gp.pen_dev = penpad(); gp.gamepad_dev = x360(); - gp.create_mouse(); - gp.create_touchscreen(); + gp.create_mouse_rel(); + gp.create_mouse_abs(); gp.create_keyboard(); - // If we do not have a keyboard, touchscreen, or mouse, fall back to XTest - if (!gp.mouse_input || !gp.touch_input || !gp.keyboard_input) { - BOOST_LOG(error) << "Unable to create some input devices! Are you a member of the 'input' group?"sv; - + // If we do not have a keyboard or mouse, fall back to XTest + if (!gp.mouse_rel_input || !gp.mouse_abs_input || !gp.keyboard_input) { #ifdef SUNSHINE_BUILD_X11 if (x11::init() || x11::tst::init()) { - BOOST_LOG(error) << "Unable to initialize X11 and/or XTest fallback"sv; + BOOST_LOG(fatal) << "Unable to create virtual input devices or use XTest fallback! Are you a member of the 'input' group?"sv; } else { - BOOST_LOG(info) << "Falling back to XTest"sv; + BOOST_LOG(error) << "Falling back to XTest for virtual input! Are you a member of the 'input' group?"sv; x11::InitThreads(); gp.display = x11::OpenDisplay(NULL); } +#else + BOOST_LOG(fatal) << "Unable to create virtual input devices! Are you a member of the 'input' group?"sv; #endif } + else { + has_uinput = true; + } return result; } @@ -1818,4 +2570,20 @@ namespace platf { return gamepads; } + + /** + * @brief Returns the supported platform capabilities to advertise to the client. + * @return Capability flags. + */ + platform_caps::caps_t + get_capabilities() { + platform_caps::caps_t caps = 0; + + // Pen and touch emulation requires uinput + if (has_uinput && config::input.native_pen_touch) { + caps |= platform_caps::pen_touch; + } + + return caps; + } } // namespace platf diff --git a/src/platform/linux/kmsgrab.cpp b/src/platform/linux/kmsgrab.cpp index 094aee5f4e9..bad467d95bf 100644 --- a/src/platform/linux/kmsgrab.cpp +++ b/src/platform/linux/kmsgrab.cpp @@ -5,24 +5,27 @@ #include #include #include +#include #include +#include #include #include #include #include +#include -#include "src/main.h" +#include "src/config.h" +#include "src/logging.h" #include "src/platform/common.h" #include "src/round_robin.h" #include "src/utility.h" #include "src/video.h" -// Cursor rendering support through x11 +#include "cuda.h" #include "graphics.h" #include "vaapi.h" #include "wayland.h" -#include "x11grab.h" using namespace std::literals; namespace fs = std::filesystem; @@ -105,6 +108,8 @@ namespace platf { using crtc_t = util::safe_ptr; using obj_prop_t = util::safe_ptr; using prop_t = util::safe_ptr; + using prop_blob_t = util::safe_ptr; + using version_t = util::safe_ptr; using conn_type_count_t = std::map; @@ -135,14 +140,20 @@ namespace platf { // For example HDMI-A-{index} or HDMI-{index} std::uint32_t index; + // ID of the connector + std::uint32_t connector_id; + bool connected; }; struct monitor_t { + // Connector attributes std::uint32_t type; - std::uint32_t index; + // Monitor index in the global list + std::uint32_t monitor_index; + platf::touch_port_t viewport; }; @@ -159,20 +170,53 @@ namespace platf { #define _CONVERT(x, y) \ if (string == x) return DRM_MODE_CONNECTOR_##y + // This list was created from the following sources: + // https://gitlab.freedesktop.org/mesa/drm/-/blob/main/xf86drmMode.c (drmModeGetConnectorTypeName) + // https://gitlab.freedesktop.org/wayland/weston/-/blob/e74f2897b9408b6356a555a0ce59146836307ff5/libweston/backend-drm/drm.c#L1458-1477 + // https://github.com/GNOME/mutter/blob/65d481594227ea7188c0416e8e00b57caeea214f/src/backends/meta-monitor-manager.c#L1618-L1639 _CONVERT("VGA"sv, VGA); + _CONVERT("DVII"sv, DVII); _CONVERT("DVI-I"sv, DVII); + _CONVERT("DVID"sv, DVID); _CONVERT("DVI-D"sv, DVID); + _CONVERT("DVIA"sv, DVIA); _CONVERT("DVI-A"sv, DVIA); + _CONVERT("Composite"sv, Composite); + _CONVERT("SVIDEO"sv, SVIDEO); _CONVERT("S-Video"sv, SVIDEO); _CONVERT("LVDS"sv, LVDS); + _CONVERT("Component"sv, Component); + _CONVERT("9PinDIN"sv, 9PinDIN); _CONVERT("DIN"sv, 9PinDIN); _CONVERT("DisplayPort"sv, DisplayPort); _CONVERT("DP"sv, DisplayPort); + _CONVERT("HDMIA"sv, HDMIA); _CONVERT("HDMI-A"sv, HDMIA); _CONVERT("HDMI"sv, HDMIA); + _CONVERT("HDMIB"sv, HDMIB); _CONVERT("HDMI-B"sv, HDMIB); + _CONVERT("TV"sv, TV); _CONVERT("eDP"sv, eDP); + _CONVERT("VIRTUAL"sv, VIRTUAL); + _CONVERT("Virtual"sv, VIRTUAL); _CONVERT("DSI"sv, DSI); + _CONVERT("DPI"sv, DPI); + _CONVERT("WRITEBACK"sv, WRITEBACK); + _CONVERT("Writeback"sv, WRITEBACK); + _CONVERT("SPI"sv, SPI); +#ifdef DRM_MODE_CONNECTOR_USB + _CONVERT("USB"sv, USB); +#endif + + // If the string starts with "Unknown", it may have the raw type + // value appended to the string. Let's try to read it. + if (string.find("Unknown"sv) == 0) { + std::uint32_t type; + std::string null_terminated_string { string }; + if (std::sscanf(null_terminated_string.c_str(), "Unknown%u", &type) == 1) { + return type; + } + } BOOST_LOG(error) << "Unknown Monitor connector type ["sv << string << "]: Please report this to the GitHub issue tracker"sv; return DRM_MODE_CONNECTOR_Unknown; @@ -182,35 +226,34 @@ namespace platf { public: plane_it_t(int fd, std::uint32_t *plane_p, std::uint32_t *end): fd { fd }, plane_p { plane_p }, end { end } { - inc(); + load_next_valid_plane(); } plane_it_t(int fd, std::uint32_t *end): fd { fd }, plane_p { end }, end { end } {} void - inc() { + load_next_valid_plane() { this->plane.reset(); for (; plane_p != end; ++plane_p) { plane_t plane = drmModeGetPlane(fd, *plane_p); - if (!plane) { BOOST_LOG(error) << "Couldn't get drm plane ["sv << (end - plane_p) << "]: "sv << strerror(errno); continue; } - // If this plane is unused - if (plane->fb_id) { - this->plane = util::make_shared(plane.release()); - - // One last increment - ++plane_p; - break; - } + this->plane = util::make_shared(plane.release()); + break; } } + void + inc() { + ++plane_p; + load_next_valid_plane(); + } + bool eq(const plane_it_t &other) const { return plane_p == other.plane_p; @@ -228,6 +271,20 @@ namespace platf { util::shared_t plane; }; + struct cursor_t { + // Public properties used during blending + bool visible = false; + std::int32_t x, y; + std::uint32_t dst_w, dst_h; + std::uint32_t src_w, src_h; + std::vector pixels; + unsigned long serial; + + // Private properties used for tracking cursor changes + std::uint64_t prop_src_x, prop_src_y, prop_src_w, prop_src_h; + std::uint32_t fb_id; + }; + class card_t { public: using connector_interal_t = util::safe_ptr; @@ -242,13 +299,42 @@ namespace platf { return -1; } + version_t ver { drmGetVersion(fd.el) }; + BOOST_LOG(info) << path << " -> "sv << ((ver && ver->name) ? ver->name : "UNKNOWN"); + + // Open the render node for this card to share with libva. + // If it fails, we'll just share the primary node instead. + char *rendernode_path = drmGetRenderDeviceNameFromFd(fd.el); + if (rendernode_path) { + BOOST_LOG(debug) << "Opening render node: "sv << rendernode_path; + render_fd.el = open(rendernode_path, O_RDWR); + if (render_fd.el < 0) { + BOOST_LOG(warning) << "Couldn't open render node: "sv << rendernode_path << ": "sv << strerror(errno); + render_fd.el = dup(fd.el); + } + free(rendernode_path); + } + else { + BOOST_LOG(warning) << "No render device name for: "sv << path; + render_fd.el = dup(fd.el); + } + if (drmSetClientCap(fd.el, DRM_CLIENT_CAP_UNIVERSAL_PLANES, 1)) { - BOOST_LOG(error) << "Couldn't expose some/all drm planes for card: "sv << path; + BOOST_LOG(error) << "GPU driver doesn't support universal planes: "sv << path; return -1; } if (drmSetClientCap(fd.el, DRM_CLIENT_CAP_ATOMIC, 1)) { - BOOST_LOG(warning) << "Couldn't expose some properties for card: "sv << path; + BOOST_LOG(warning) << "GPU driver doesn't support atomic mode-setting: "sv << path; +#if defined(SUNSHINE_BUILD_X11) + // We won't be able to capture the mouse cursor with KMS on non-atomic drivers, + // so fall back to X11 if it's available and the user didn't explicitly force KMS. + if (window_system == window_system_e::X11 && config::video.capture != "kms") { + BOOST_LOG(info) << "Avoiding KMS capture under X11 due to lack of atomic mode-setting"sv; + return -1; + } +#endif + BOOST_LOG(warning) << "Cursor capture may fail without atomic mode-setting support!"sv; } plane_res.reset(drmModeGetPlaneResources(fd.el)); @@ -292,6 +378,12 @@ namespace platf { return drmModeGetResources(fd.el); } + bool + is_nvidia() { + version_t ver { drmGetVersion(fd.el) }; + return ver && ver->name && strncmp(ver->name, "nvidia-drm", 10) == 0; + } + bool is_cursor(std::uint32_t plane_id) { auto props = plane_props(plane_id); @@ -309,19 +401,39 @@ namespace platf { return false; } - std::uint32_t - get_panel_orientation(std::uint32_t plane_id) { - auto props = plane_props(plane_id); + std::optional + prop_value_by_name(const std::vector> &props, std::string_view name) { for (auto &[prop, val] : props) { - if (prop->name == "rotation"sv) { + if (prop->name == name) { return val; } } + return std::nullopt; + } + + std::uint32_t + get_panel_orientation(std::uint32_t plane_id) { + auto props = plane_props(plane_id); + auto value = prop_value_by_name(props, "rotation"sv); + if (value) { + return *value; + } BOOST_LOG(error) << "Failed to determine panel orientation, defaulting to landscape."; return DRM_MODE_ROTATE_0; } + int + get_crtc_index_by_id(std::uint32_t crtc_id) { + auto resources = res(); + for (int i = 0; i < resources->count_crtcs; i++) { + if (resources->crtcs[i] == crtc_id) { + return i; + } + } + return -1; + } + connector_interal_t connector(std::uint32_t id) { return drmModeGetConnector(fd.el, id); @@ -354,6 +466,7 @@ namespace platf { conn->connector_type, crtc_id, index, + conn->connector_id, conn->connection == DRM_MODE_CONNECTED, }); }); @@ -376,6 +489,9 @@ namespace platf { std::vector> props(std::uint32_t id, std::uint32_t type) { obj_prop_t obj_prop = drmModeObjectGetProperties(fd.el, id, type); + if (!obj_prop) { + return {}; + } std::vector> props; props.reserve(obj_prop->count_props); @@ -423,6 +539,7 @@ namespace platf { } file_t fd; + file_t render_fd; plane_res_t plane_res; }; @@ -497,18 +614,32 @@ namespace platf { for (auto &entry : fs::directory_iterator { card_dir }) { auto file = entry.path().filename(); - auto filestring = file.generic_u8string(); + auto filestring = file.generic_string(); if (filestring.size() < 4 || std::string_view { filestring }.substr(0, 4) != "card"sv) { continue; } kms::card_t card; if (card.init(entry.path().c_str())) { - return {}; + continue; + } + + // Skip non-Nvidia cards if we're looking for CUDA devices + // unless NVENC is selected manually by the user + if (mem_type == mem_type_e::cuda && !card.is_nvidia()) { + BOOST_LOG(debug) << file << " is not a CUDA device"sv; + if (config::video.encoder != "nvenc") { + continue; + } } auto end = std::end(card); for (auto plane = std::begin(card); plane != end; ++plane) { + // Skip unused planes + if (!plane->fb_id) { + continue; + } + if (card.is_cursor(plane->plane_id)) { continue; } @@ -525,8 +656,7 @@ namespace platf { } if (!fb->handles[0]) { - BOOST_LOG(error) - << "Couldn't get handle for DRM Framebuffer ["sv << plane->fb_id << "]: Possibly not permitted: do [sudo setcap cap_sys_admin+p sunshine]"sv; + BOOST_LOG(error) << "Couldn't get handle for DRM Framebuffer ["sv << plane->fb_id << "]: Probably not permitted"sv; return -1; } @@ -542,6 +672,12 @@ namespace platf { } } + auto crtc = card.crtc(plane->crtc_id); + if (!crtc) { + BOOST_LOG(error) << "Couldn't get CRTC info: "sv << strerror(errno); + continue; + } + BOOST_LOG(info) << "Found monitor for DRM screencasting"sv; // We need to find the correct /dev/dri/card{nr} to correlate the crtc_id with the monitor descriptor @@ -550,7 +686,7 @@ namespace platf { }); if (pos == std::end(card_descriptors)) { - // This code path shouldn't happend, but it's there just in case. + // This code path shouldn't happen, but it's there just in case. // card_descriptors is part of the guesswork after all. BOOST_LOG(error) << "Couldn't find ["sv << entry.path() << "]: This shouldn't have happened :/"sv; return -1; @@ -558,13 +694,12 @@ namespace platf { // TODO: surf_sd = fb->to_sd(); - auto crct = card.crtc(plane->crtc_id); - kms::print(plane.get(), fb.get(), crct.get()); + kms::print(plane.get(), fb.get(), crtc.get()); img_width = fb->width; img_height = fb->height; - img_offset_x = crct->x; - img_offset_y = crct->y; + img_offset_x = crtc->x; + img_offset_y = crtc->y; this->env_width = ::platf::kms::env_width; this->env_height = ::platf::kms::env_height; @@ -592,50 +727,370 @@ namespace platf { offset_y = viewport.offset_y; } - // This code path shouldn't happend, but it's there just in case. + // This code path shouldn't happen, but it's there just in case. // crtc_to_monitor is part of the guesswork after all. else { BOOST_LOG(warning) << "Couldn't find crtc_id, this shouldn't have happened :\\"sv; - width = crct->width; - height = crct->height; - offset_x = crct->x; - offset_y = crct->y; + width = crtc->width; + height = crtc->height; + offset_x = crtc->x; + offset_y = crtc->y; } - this->card = std::move(card); - plane_id = plane->plane_id; + crtc_id = plane->crtc_id; + crtc_index = card.get_crtc_index_by_id(plane->crtc_id); + + // Find the connector for this CRTC + kms::conn_type_count_t conn_type_count; + for (auto &connector : card.monitors(conn_type_count)) { + if (connector.crtc_id == crtc_id) { + BOOST_LOG(info) << "Found connector ID ["sv << connector.connector_id << ']'; + connector_id = connector.connector_id; + + auto connector_props = card.connector_props(*connector_id); + hdr_metadata_blob_id = card.prop_value_by_name(connector_props, "HDR_OUTPUT_METADATA"sv); + } + } + + this->card = std::move(card); goto break_loop; } } + BOOST_LOG(error) << "Couldn't find monitor ["sv << monitor_index << ']'; + return -1; + // Neatly break from nested for loop break_loop: - if (monitor != monitor_index) { - BOOST_LOG(error) << "Couldn't find monitor ["sv << monitor_index << ']'; - return -1; + // Look for the cursor plane for this CRTC + cursor_plane_id = -1; + auto end = std::end(card); + for (auto plane = std::begin(card); plane != end; ++plane) { + if (!card.is_cursor(plane->plane_id)) { + continue; + } + + // NB: We do not skip unused planes here because cursor planes + // will look unused if the cursor is currently hidden. + + if (!(plane->possible_crtcs & (1 << crtc_index))) { + // Skip cursor planes for other CRTCs + continue; + } + else if (plane->possible_crtcs != (1 << crtc_index)) { + // We assume a 1:1 mapping between cursor planes and CRTCs, which seems to + // match the behavior of drivers in the real world. If it's violated, we'll + // proceed anyway but print a warning in the log. + BOOST_LOG(warning) << "Cursor plane spans multiple CRTCs!"sv; + } + + BOOST_LOG(info) << "Found cursor plane ["sv << plane->plane_id << ']'; + cursor_plane_id = plane->plane_id; + break; } - cursor_opt = x11::cursor_t::make(); + if (cursor_plane_id < 0) { + BOOST_LOG(warning) << "No KMS cursor plane found. Cursor may not be displayed while streaming!"sv; + } return 0; } + bool + is_hdr() { + if (!hdr_metadata_blob_id || *hdr_metadata_blob_id == 0) { + return false; + } + + prop_blob_t hdr_metadata_blob = drmModeGetPropertyBlob(card.fd.el, *hdr_metadata_blob_id); + if (hdr_metadata_blob == nullptr) { + BOOST_LOG(error) << "Unable to get HDR metadata blob: "sv << strerror(errno); + return false; + } + + if (hdr_metadata_blob->length < sizeof(uint32_t) + sizeof(hdr_metadata_infoframe)) { + BOOST_LOG(error) << "HDR metadata blob is too small: "sv << hdr_metadata_blob->length; + return false; + } + + auto raw_metadata = (hdr_output_metadata *) hdr_metadata_blob->data; + if (raw_metadata->metadata_type != 0) { // HDMI_STATIC_METADATA_TYPE1 + BOOST_LOG(error) << "Unknown HDMI_STATIC_METADATA_TYPE value: "sv << raw_metadata->metadata_type; + return false; + } + + if (raw_metadata->hdmi_metadata_type1.metadata_type != 0) { // Static Metadata Type 1 + BOOST_LOG(error) << "Unknown secondary metadata type value: "sv << raw_metadata->hdmi_metadata_type1.metadata_type; + return false; + } + + // We only support Traditional Gamma SDR or SMPTE 2084 PQ HDR EOTFs. + // Print a warning if we encounter any others. + switch (raw_metadata->hdmi_metadata_type1.eotf) { + case 0: // HDMI_EOTF_TRADITIONAL_GAMMA_SDR + return false; + case 1: // HDMI_EOTF_TRADITIONAL_GAMMA_HDR + BOOST_LOG(warning) << "Unsupported HDR EOTF: Traditional Gamma"sv; + return true; + case 2: // HDMI_EOTF_SMPTE_ST2084 + return true; + case 3: // HDMI_EOTF_BT_2100_HLG + BOOST_LOG(warning) << "Unsupported HDR EOTF: HLG"sv; + return true; + default: + BOOST_LOG(warning) << "Unsupported HDR EOTF: "sv << raw_metadata->hdmi_metadata_type1.eotf; + return true; + } + } + + bool + get_hdr_metadata(SS_HDR_METADATA &metadata) { + // This performs all the metadata validation + if (!is_hdr()) { + return false; + } + + prop_blob_t hdr_metadata_blob = drmModeGetPropertyBlob(card.fd.el, *hdr_metadata_blob_id); + if (hdr_metadata_blob == nullptr) { + BOOST_LOG(error) << "Unable to get HDR metadata blob: "sv << strerror(errno); + return false; + } + + auto raw_metadata = (hdr_output_metadata *) hdr_metadata_blob->data; + + for (int i = 0; i < 3; i++) { + metadata.displayPrimaries[i].x = raw_metadata->hdmi_metadata_type1.display_primaries[i].x; + metadata.displayPrimaries[i].y = raw_metadata->hdmi_metadata_type1.display_primaries[i].y; + } + + metadata.whitePoint.x = raw_metadata->hdmi_metadata_type1.white_point.x; + metadata.whitePoint.y = raw_metadata->hdmi_metadata_type1.white_point.y; + metadata.maxDisplayLuminance = raw_metadata->hdmi_metadata_type1.max_display_mastering_luminance; + metadata.minDisplayLuminance = raw_metadata->hdmi_metadata_type1.min_display_mastering_luminance; + metadata.maxContentLightLevel = raw_metadata->hdmi_metadata_type1.max_cll; + metadata.maxFrameAverageLightLevel = raw_metadata->hdmi_metadata_type1.max_fall; + + return true; + } + + void + update_cursor() { + if (cursor_plane_id < 0) { + return; + } + + plane_t plane = drmModeGetPlane(card.fd.el, cursor_plane_id); + + std::optional prop_crtc_x; + std::optional prop_crtc_y; + std::optional prop_crtc_w; + std::optional prop_crtc_h; + + std::optional prop_src_x; + std::optional prop_src_y; + std::optional prop_src_w; + std::optional prop_src_h; + + auto props = card.plane_props(cursor_plane_id); + for (auto &[prop, val] : props) { + if (prop->name == "CRTC_X"sv) { + prop_crtc_x = val; + } + else if (prop->name == "CRTC_Y"sv) { + prop_crtc_y = val; + } + else if (prop->name == "CRTC_W"sv) { + prop_crtc_w = val; + } + else if (prop->name == "CRTC_H"sv) { + prop_crtc_h = val; + } + else if (prop->name == "SRC_X"sv) { + prop_src_x = val; + } + else if (prop->name == "SRC_Y"sv) { + prop_src_y = val; + } + else if (prop->name == "SRC_W"sv) { + prop_src_w = val; + } + else if (prop->name == "SRC_H"sv) { + prop_src_h = val; + } + } + + if (!prop_crtc_w || !prop_crtc_h || !prop_crtc_x || !prop_crtc_y) { + BOOST_LOG(error) << "Cursor plane is missing required plane CRTC properties!"sv; + BOOST_LOG(error) << "Atomic mode-setting must be enabled to capture the cursor!"sv; + cursor_plane_id = -1; + captured_cursor.visible = false; + return; + } + if (!prop_src_x || !prop_src_y || !prop_src_w || !prop_src_h) { + BOOST_LOG(error) << "Cursor plane is missing required plane SRC properties!"sv; + BOOST_LOG(error) << "Atomic mode-setting must be enabled to capture the cursor!"sv; + cursor_plane_id = -1; + captured_cursor.visible = false; + return; + } + + // Update the cursor position and size unconditionally + captured_cursor.x = *prop_crtc_x; + captured_cursor.y = *prop_crtc_y; + captured_cursor.dst_w = *prop_crtc_w; + captured_cursor.dst_h = *prop_crtc_h; + + // We're technically cheating a bit here by assuming that we can detect + // changes to the cursor plane via property adjustments. If this isn't + // true, we'll really have to mmap() the dmabuf and draw that every time. + bool cursor_dirty = false; + + if (!plane->fb_id) { + captured_cursor.visible = false; + captured_cursor.fb_id = 0; + } + else if (plane->fb_id != captured_cursor.fb_id) { + BOOST_LOG(debug) << "Refreshing cursor image after FB changed"sv; + cursor_dirty = true; + } + else if (*prop_src_x != captured_cursor.prop_src_x || + *prop_src_y != captured_cursor.prop_src_y || + *prop_src_w != captured_cursor.prop_src_w || + *prop_src_h != captured_cursor.prop_src_h) { + BOOST_LOG(debug) << "Refreshing cursor image after source dimensions changed"sv; + cursor_dirty = true; + } + + // If the cursor is dirty, map it so we can download the new image + if (cursor_dirty) { + auto fb = card.fb(plane.get()); + if (!fb || !fb->handles[0]) { + // This means the cursor is not currently visible + captured_cursor.visible = false; + return; + } + + // All known cursor planes in the wild are ARGB8888 + if (fb->pixel_format != DRM_FORMAT_ARGB8888) { + BOOST_LOG(error) << "Unsupported non-ARGB8888 cursor format: "sv << fb->pixel_format; + captured_cursor.visible = false; + cursor_plane_id = -1; + return; + } + + // All known cursor planes in the wild require linear buffers + if (fb->modifier != DRM_FORMAT_MOD_LINEAR && fb->modifier != DRM_FORMAT_MOD_INVALID) { + BOOST_LOG(error) << "Unsupported non-linear cursor modifier: "sv << fb->modifier; + captured_cursor.visible = false; + cursor_plane_id = -1; + return; + } + + // The SRC_* properties are in Q16.16 fixed point, so convert to integers + auto src_x = *prop_src_x >> 16; + auto src_y = *prop_src_y >> 16; + auto src_w = *prop_src_w >> 16; + auto src_h = *prop_src_h >> 16; + + // Check for a legal source rectangle + if (src_x + src_w > fb->width || src_y + src_h > fb->height) { + BOOST_LOG(error) << "Illegal source size: ["sv << src_x + src_w << ',' << src_y + src_h << "] > ["sv << fb->width << ',' << fb->height << ']'; + captured_cursor.visible = false; + return; + } + + file_t plane_fd = card.handleFD(fb->handles[0]); + if (plane_fd.el < 0) { + captured_cursor.visible = false; + return; + } + + // We will map the entire region, but only copy what the source rectangle specifies + size_t mapped_size = ((size_t) fb->pitches[0]) * fb->height; + void *mapped_data = mmap(nullptr, mapped_size, PROT_READ, MAP_SHARED, plane_fd.el, fb->offsets[0]); + + // If we got ENOSYS back, let's try to map it as a dumb buffer instead (required for Nvidia GPUs) + if (mapped_data == MAP_FAILED && errno == ENOSYS) { + drm_mode_map_dumb map = {}; + map.handle = fb->handles[0]; + if (drmIoctl(card.fd.el, DRM_IOCTL_MODE_MAP_DUMB, &map) < 0) { + BOOST_LOG(error) << "Failed to map cursor FB as dumb buffer: "sv << strerror(errno); + captured_cursor.visible = false; + return; + } + + mapped_data = mmap(nullptr, mapped_size, PROT_READ, MAP_SHARED, card.fd.el, map.offset); + } + + if (mapped_data == MAP_FAILED) { + BOOST_LOG(error) << "Failed to mmap cursor FB: "sv << strerror(errno); + captured_cursor.visible = false; + return; + } + + captured_cursor.pixels.resize(src_w * src_h * 4); + + // Prepare to read the dmabuf from the CPU + struct dma_buf_sync sync; + sync.flags = DMA_BUF_SYNC_START | DMA_BUF_SYNC_READ; + drmIoctl(plane_fd.el, DMA_BUF_IOCTL_SYNC, &sync); + + // If the image is tightly packed, copy it in one shot + if (fb->pitches[0] == src_w * 4 && src_x == 0) { + memcpy(captured_cursor.pixels.data(), &((std::uint8_t *) mapped_data)[src_y * fb->pitches[0]], src_h * fb->pitches[0]); + } + else { + // Copy row by row to deal with mismatched pitch or an X offset + auto pixel_dst = captured_cursor.pixels.data(); + for (int y = 0; y < src_h; y++) { + memcpy(&pixel_dst[y * (src_w * 4)], &((std::uint8_t *) mapped_data)[(y + src_y) * fb->pitches[0] + (src_x * 4)], src_w * 4); + } + } + + // End the CPU read and unmap the dmabuf + sync.flags = DMA_BUF_SYNC_END | DMA_BUF_SYNC_READ; + drmIoctl(plane_fd.el, DMA_BUF_IOCTL_SYNC, &sync); + + munmap(mapped_data, mapped_size); + + captured_cursor.visible = true; + captured_cursor.src_w = src_w; + captured_cursor.src_h = src_h; + captured_cursor.prop_src_x = *prop_src_x; + captured_cursor.prop_src_y = *prop_src_y; + captured_cursor.prop_src_w = *prop_src_w; + captured_cursor.prop_src_h = *prop_src_h; + captured_cursor.fb_id = plane->fb_id; + ++captured_cursor.serial; + } + } + inline capture_e - refresh(file_t *file, egl::surface_descriptor_t *sd) { + refresh(file_t *file, egl::surface_descriptor_t *sd, std::optional &frame_timestamp) { + // Check for a change in HDR metadata + if (connector_id) { + auto connector_props = card.connector_props(*connector_id); + if (hdr_metadata_blob_id != card.prop_value_by_name(connector_props, "HDR_OUTPUT_METADATA"sv)) { + BOOST_LOG(info) << "Reinitializing capture after HDR metadata change"sv; + return capture_e::reinit; + } + } + plane_t plane = drmModeGetPlane(card.fd.el, plane_id); + frame_timestamp = std::chrono::steady_clock::now(); auto fb = card.fb(plane.get()); if (!fb) { - BOOST_LOG(error) << "Couldn't get drm fb for plane ["sv << plane->fb_id << "]: "sv << strerror(errno); - return capture_e::error; + // This can happen if the display is being reconfigured while streaming + BOOST_LOG(warning) << "Couldn't get drm fb for plane ["sv << plane->fb_id << "]: "sv << strerror(errno); + return capture_e::timeout; } if (!fb->handles[0]) { - BOOST_LOG(error) - << "Couldn't get handle for DRM Framebuffer ["sv << plane->fb_id << "]: Possibly not permitted: do [sudo setcap cap_sys_admin+p sunshine]"sv; + BOOST_LOG(error) << "Couldn't get handle for DRM Framebuffer ["sv << plane->fb_id << "]: Probably not permitted"sv; return capture_e::error; } @@ -672,6 +1127,8 @@ namespace platf { return capture_e::reinit; } + update_cursor(); + return capture_e::ok; } @@ -683,10 +1140,16 @@ namespace platf { int img_offset_x, img_offset_y; int plane_id; + int crtc_id; + int crtc_index; - card_t card; + std::optional connector_id; + std::optional hdr_metadata_blob_id; + + int cursor_plane_id; + cursor_t captured_cursor {}; - std::optional cursor_opt; + card_t card; }; class display_ram_t: public display_t { @@ -730,17 +1193,22 @@ namespace platf { capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override { auto next_frame = std::chrono::steady_clock::now(); + sleep_overshoot_tracker.reset(); + while (true) { auto now = std::chrono::steady_clock::now(); if (next_frame > now) { - std::this_thread::sleep_for((next_frame - now) / 3 * 2); + std::this_thread::sleep_for(next_frame - now); } - while (next_frame > now) { - std::this_thread::sleep_for(1ns); - now = std::chrono::steady_clock::now(); + now = std::chrono::steady_clock::now(); + std::chrono::nanoseconds overshoot_ns = now - next_frame; + log_sleep_overshoot(overshoot_ns); + + next_frame += delay; + if (next_frame < now) { // some major slowdown happened; we couldn't keep up + next_frame = now + delay; } - next_frame = now + delay; std::shared_ptr img_out; auto status = snapshot(pull_free_image_cb, img_out, 1000ms, *cursor); @@ -768,13 +1236,71 @@ namespace platf { return capture_e::ok; } - std::shared_ptr - make_hwdevice(pix_fmt_e pix_fmt) override { + std::unique_ptr + make_avcodec_encode_device(pix_fmt_e pix_fmt) override { +#ifdef SUNSHINE_BUILD_VAAPI if (mem_type == mem_type_e::vaapi) { - return va::make_hwdevice(width, height, false); + return va::make_avcodec_encode_device(width, height, false); + } +#endif + +#ifdef SUNSHINE_BUILD_CUDA + if (mem_type == mem_type_e::cuda) { + return cuda::make_avcodec_encode_device(width, height, false); } +#endif - return std::make_shared(); + return std::make_unique(); + } + + void + blend_cursor(img_t &img) { + // TODO: Cursor scaling is not supported in this codepath. + // We always draw the cursor at the source size. + auto pixels = (int *) img.data; + + int32_t screen_height = img.height; + int32_t screen_width = img.width; + + // This is the position in the target that we will start drawing the cursor + auto cursor_x = std::max(0, captured_cursor.x - img_offset_x); + auto cursor_y = std::max(0, captured_cursor.y - img_offset_y); + + // If the cursor is partially off screen, the coordinates may be negative + // which means we will draw the top-right visible portion of the cursor only. + auto cursor_delta_x = cursor_x - std::max(-captured_cursor.src_w, captured_cursor.x - img_offset_x); + auto cursor_delta_y = cursor_y - std::max(-captured_cursor.src_h, captured_cursor.y - img_offset_y); + + auto delta_height = std::min(captured_cursor.src_h, std::max(0, screen_height - cursor_y)) - cursor_delta_y; + auto delta_width = std::min(captured_cursor.src_w, std::max(0, screen_width - cursor_x)) - cursor_delta_x; + for (auto y = 0; y < delta_height; ++y) { + // Offset into the cursor image to skip drawing the parts of the cursor image that are off screen + // + // NB: We must access the elements via the data() function because cursor_end may point to the + // the first element beyond the valid range of the vector. Using vector's [] operator in that + // manner is undefined behavior (and triggers errors when using debug libc++), while doing the + // same with an array is fine. + auto cursor_begin = (uint32_t *) &captured_cursor.pixels.data()[((y + cursor_delta_y) * captured_cursor.src_w + cursor_delta_x) * 4]; + auto cursor_end = (uint32_t *) &captured_cursor.pixels.data()[((y + cursor_delta_y) * captured_cursor.src_w + delta_width + cursor_delta_x) * 4]; + + auto pixels_begin = &pixels[(y + cursor_y) * (img.row_pitch / img.pixel_pitch) + cursor_x]; + + std::for_each(cursor_begin, cursor_end, [&](uint32_t cursor_pixel) { + auto colors_in = (uint8_t *) pixels_begin; + + auto alpha = (*(uint *) &cursor_pixel) >> 24u; + if (alpha == 255) { + *pixels_begin = cursor_pixel; + } + else { + auto colors_out = (uint8_t *) &cursor_pixel; + colors_in[0] = colors_out[0] + (colors_in[0] * (255 - alpha) + 255 / 2) / 255; + colors_in[1] = colors_out[1] + (colors_in[1] * (255 - alpha) + 255 / 2) / 255; + colors_in[2] = colors_out[2] + (colors_in[2] * (255 - alpha) + 255 / 2) / 255; + } + ++pixels_begin; + }); + } } capture_e @@ -783,7 +1309,8 @@ namespace platf { egl::surface_descriptor_t sd; - auto status = refresh(fb_fd, &sd); + std::optional frame_timestamp; + auto status = refresh(fb_fd, &sd, frame_timestamp); if (status != capture_e::ok) { return status; } @@ -798,6 +1325,7 @@ namespace platf { gl::ctx.BindTexture(GL_TEXTURE_2D, rgb->tex[0]); + // Don't remove these lines, see https://github.com/LizardByte/Sunshine/issues/453 int w, h; gl::ctx.GetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &w); gl::ctx.GetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &h); @@ -809,8 +1337,10 @@ namespace platf { gl::ctx.GetTextureSubImage(rgb->tex[0], 0, img_offset_x, img_offset_y, 0, width, height, 1, GL_BGRA, GL_UNSIGNED_BYTE, img_out->height * img_out->row_pitch, img_out->data); - if (cursor_opt && cursor) { - cursor_opt->blend(*img_out, img_offset_x, img_offset_y); + img_out->frame_timestamp = frame_timestamp; + + if (cursor && captured_cursor.visible) { + blend_cursor(*img_out); } return capture_e::ok; @@ -843,11 +1373,19 @@ namespace platf { display_vram_t(mem_type_e mem_type): display_t(mem_type) {} - std::shared_ptr - make_hwdevice(pix_fmt_e pix_fmt) override { + std::unique_ptr + make_avcodec_encode_device(pix_fmt_e pix_fmt) override { +#ifdef SUNSHINE_BUILD_VAAPI if (mem_type == mem_type_e::vaapi) { - return va::make_hwdevice(width, height, dup(card.fd.el), img_offset_x, img_offset_y, true); + return va::make_avcodec_encode_device(width, height, dup(card.render_fd.el), img_offset_x, img_offset_y, true); } +#endif + +#ifdef SUNSHINE_BUILD_CUDA + if (mem_type == mem_type_e::cuda) { + return cuda::make_avcodec_gl_encode_device(width, height, img_offset_x, img_offset_y); + } +#endif BOOST_LOG(error) << "Unsupported pixel format for egl::display_vram_t: "sv << platf::from_pix_fmt(pix_fmt); return nullptr; @@ -857,6 +1395,8 @@ namespace platf { alloc_img() override { auto img = std::make_shared(); + img->width = width; + img->height = height; img->serial = std::numeric_limitsserial)>::max(); img->data = nullptr; img->pixel_pitch = 4; @@ -869,33 +1409,30 @@ namespace platf { int dummy_img(platf::img_t *img) override { - // TODO: stop cheating and give black image - if (!img) { - return -1; - }; - auto pull_dummy_img_callback = [&img](std::shared_ptr &img_out) -> bool { - img_out = img->shared_from_this(); - return true; - }; - std::shared_ptr img_out; - return snapshot(pull_dummy_img_callback, img_out, 1s, false) != platf::capture_e::ok; + // Empty images are recognized as dummies by the zero sequence number + return 0; } capture_e capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) { auto next_frame = std::chrono::steady_clock::now(); + sleep_overshoot_tracker.reset(); + while (true) { auto now = std::chrono::steady_clock::now(); if (next_frame > now) { - std::this_thread::sleep_for((next_frame - now) / 3 * 2); + std::this_thread::sleep_for(next_frame - now); } - while (next_frame > now) { - std::this_thread::sleep_for(1ns); - now = std::chrono::steady_clock::now(); + now = std::chrono::steady_clock::now(); + std::chrono::nanoseconds overshoot_ns = now - next_frame; + log_sleep_overshoot(overshoot_ns); + + next_frame += delay; + if (next_frame < now) { // some major slowdown happened; we couldn't keep up + next_frame = now + delay; } - next_frame = now + delay; std::shared_ptr img_out; auto status = snapshot(pull_free_image_cb, img_out, 1000ms, *cursor); @@ -933,26 +1470,33 @@ namespace platf { auto img = (egl::img_descriptor_t *) img_out.get(); img->reset(); - auto status = refresh(fb_fd, &img->sd); + auto status = refresh(fb_fd, &img->sd, img->frame_timestamp); if (status != capture_e::ok) { return status; } img->sequence = ++sequence; - if (!cursor || !cursor_opt) { - img->data = nullptr; - - for (auto x = 0; x < 4; ++x) { - fb_fd[x].release(); + if (cursor && captured_cursor.visible) { + // Copy new cursor pixel data if it's been updated + if (img->serial != captured_cursor.serial) { + img->buffer = captured_cursor.pixels; + img->serial = captured_cursor.serial; } - return capture_e::ok; - } - - cursor_opt->capture(*img); - img->x -= offset_x; - img->y -= offset_y; + img->x = captured_cursor.x; + img->y = captured_cursor.y; + img->src_w = captured_cursor.src_w; + img->src_h = captured_cursor.src_h; + img->width = captured_cursor.dst_w; + img->height = captured_cursor.dst_h; + img->pixel_pitch = 4; + img->row_pitch = img->pixel_pitch * img->width; + img->data = img->buffer.data(); + } + else { + img->data = nullptr; + } for (auto x = 0; x < 4; ++x) { fb_fd[x].release(); @@ -966,24 +1510,31 @@ namespace platf { return -1; } - if (!va::validate(card.fd.el)) { +#ifdef SUNSHINE_BUILD_VAAPI + if (mem_type == mem_type_e::vaapi && !va::validate(card.render_fd.el)) { BOOST_LOG(warning) << "Monitor "sv << display_name << " doesn't support hardware encoding. Reverting back to GPU -> RAM -> GPU"sv; return -1; } +#endif - sequence = 0; +#ifndef SUNSHINE_BUILD_CUDA + if (mem_type == mem_type_e::cuda) { + BOOST_LOG(warning) << "Attempting to use NVENC without CUDA support. Reverting back to GPU -> RAM -> GPU"sv; + return -1; + } +#endif return 0; } - std::uint64_t sequence; + std::uint64_t sequence {}; }; } // namespace kms std::shared_ptr kms_display(mem_type_e hwdevice_type, const std::string &display_name, const ::video::config_t &config) { - if (hwdevice_type == mem_type_e::vaapi) { + if (hwdevice_type == mem_type_e::vaapi || hwdevice_type == mem_type_e::cuda) { auto disp = std::make_shared(hwdevice_type); if (!disp->init(display_name, config)) { @@ -1016,14 +1567,14 @@ namespace platf { correlate_to_wayland(std::vector &cds) { auto monitors = wl::monitors(); + BOOST_LOG(info) << "-------- Start of KMS monitor list --------"sv; + for (auto &monitor : monitors) { std::string_view name = monitor->name; - BOOST_LOG(info) << name << ": "sv << monitor->description; - // Try to convert names in the format: // {type}-{index} - // {index} is n'th occurence of {type} + // {index} is n'th occurrence of {type} auto index_begin = name.find_last_of('-'); std::uint32_t index; @@ -1053,6 +1604,7 @@ namespace platf { << monitor->viewport.width << 'x' << monitor->viewport.height; } + BOOST_LOG(info) << "Monitor " << monitor_descriptor.monitor_index << " is "sv << name << ": "sv << monitor->description; goto break_for_loop; } } @@ -1061,11 +1613,13 @@ namespace platf { BOOST_LOG(verbose) << "Reduced to name: "sv << name << ": "sv << index; } + + BOOST_LOG(info) << "--------- End of KMS monitor list ---------"sv; } // A list of names of displays accepted as display_name std::vector - kms_display_names() { + kms_display_names(mem_type_e hwdevice_type) { int count = 0; if (!fs::exists("/dev/dri")) { @@ -1087,20 +1641,41 @@ namespace platf { for (auto &entry : fs::directory_iterator { card_dir }) { auto file = entry.path().filename(); - auto filestring = file.generic_u8string(); + auto filestring = file.generic_string(); if (std::string_view { filestring }.substr(0, 4) != "card"sv) { continue; } kms::card_t card; if (card.init(entry.path().c_str())) { - return {}; + continue; + } + + // Skip non-Nvidia cards if we're looking for CUDA devices + // unless NVENC is selected manually by the user + if (hwdevice_type == mem_type_e::cuda && !card.is_nvidia()) { + BOOST_LOG(debug) << file << " is not a CUDA device"sv; + if (config::video.encoder == "nvenc") { + BOOST_LOG(warning) << "Using NVENC with your display connected to a different GPU may not work properly!"sv; + } + else { + continue; + } } auto crtc_to_monitor = kms::map_crtc_to_monitor(card.monitors(conn_type_count)); auto end = std::end(card); for (auto plane = std::begin(card); plane != end; ++plane) { + // Skip unused planes + if (!plane->fb_id) { + continue; + } + + if (card.is_cursor(plane->plane_id)) { + continue; + } + auto fb = card.fb(plane.get()); if (!fb) { BOOST_LOG(error) << "Couldn't get drm fb for plane ["sv << plane->fb_id << "]: "sv << strerror(errno); @@ -1108,20 +1683,19 @@ namespace platf { } if (!fb->handles[0]) { - BOOST_LOG(error) - << "Couldn't get handle for DRM Framebuffer ["sv << plane->fb_id << "]: Possibly not permitted: do [sudo setcap cap_sys_admin+p sunshine]"sv; + BOOST_LOG(error) << "Couldn't get handle for DRM Framebuffer ["sv << plane->fb_id << "]: Probably not permitted"sv; + BOOST_LOG((window_system != window_system_e::X11 || config::video.capture == "kms") ? fatal : error) + << "You must run [sudo setcap cap_sys_admin+p $(readlink -f $(which sunshine))] for KMS display capture to work!\n"sv + << "If you installed from AppImage or Flatpak, please refer to the official documentation:\n"sv + << "https://docs.lizardbyte.dev/projects/sunshine/en/latest/about/setup.html#install"sv; break; } - if (card.is_cursor(plane->plane_id)) { - continue; - } - // This appears to return the offset of the monitor auto crtc = card.crtc(plane->crtc_id); if (!crtc) { - BOOST_LOG(error) << "Couldn't get crtc info: "sv << strerror(errno); - return {}; + BOOST_LOG(error) << "Couldn't get CRTC info: "sv << strerror(errno); + continue; } auto it = crtc_to_monitor.find(plane->crtc_id); @@ -1132,6 +1706,7 @@ namespace platf { (int) crtc->width, (int) crtc->height, }; + it->second.monitor_index = count; } kms::env_width = std::max(kms::env_width, (int) (crtc->x + crtc->width)); diff --git a/src/platform/linux/misc.cpp b/src/platform/linux/misc.cpp index 73f6011a144..980c0804858 100644 --- a/src/platform/linux/misc.cpp +++ b/src/platform/linux/misc.cpp @@ -2,8 +2,15 @@ * @file src/misc.cpp * @brief todo */ + +// Required for in6_pktinfo with glibc headers +#ifndef _GNU_SOURCE + #define _GNU_SOURCE 1 +#endif + // standard includes #include +#include // lib includes #include @@ -20,7 +27,8 @@ #include "graphics.h" #include "misc.h" #include "src/config.h" -#include "src/main.h" +#include "src/entry_handler.h" +#include "src/logging.h" #include "src/platform/common.h" #include "vaapi.h" @@ -91,14 +99,89 @@ namespace platf { return ifaddr_t { p }; } + /** + * @brief Performs migration if necessary, then returns the appdata directory. + * @details This is used for the log directory, so it cannot invoke Boost logging! + * @return The path of the appdata directory that should be used. + */ fs::path appdata() { - const char *homedir; - if ((homedir = getenv("HOME")) == nullptr) { - homedir = getpwuid(geteuid())->pw_dir; - } + static std::once_flag migration_flag; + static fs::path config_path; + + // Ensure migration is only attempted once + std::call_once(migration_flag, []() { + bool found = false; + bool migrate_config = true; + const char *dir; + const char *homedir; + const char *migrate_envvar; + + // Get the home directory + if ((homedir = getenv("HOME")) == nullptr || strlen(homedir) == 0) { + // If HOME is empty or not set, use the current user's home directory + homedir = getpwuid(geteuid())->pw_dir; + } - return fs::path { homedir } / ".config/sunshine"sv; + // May be set if running under a systemd service with the ConfigurationDirectory= option set. + if ((dir = getenv("CONFIGURATION_DIRECTORY")) != nullptr && strlen(dir) > 0) { + found = true; + config_path = fs::path(dir) / "sunshine"sv; + } + // Otherwise, follow the XDG base directory specification: + // https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html + if (!found && (dir = getenv("XDG_CONFIG_HOME")) != nullptr && strlen(dir) > 0) { + found = true; + config_path = fs::path(dir) / "sunshine"sv; + } + // As a last resort, use the home directory + if (!found) { + migrate_config = false; + config_path = fs::path(homedir) / ".config/sunshine"sv; + } + + // migrate from the old config location if necessary + migrate_envvar = getenv("SUNSHINE_MIGRATE_CONFIG"); + if (migrate_config && found && migrate_envvar && strcmp(migrate_envvar, "1") == 0) { + std::error_code ec; + fs::path old_config_path = fs::path(homedir) / ".config/sunshine"sv; + if (old_config_path != config_path && fs::exists(old_config_path, ec)) { + if (!fs::exists(config_path, ec)) { + std::cout << "Migrating config from "sv << old_config_path << " to "sv << config_path << std::endl; + if (!ec) { + // Create the new directory tree if it doesn't already exist + fs::create_directories(config_path, ec); + } + if (!ec) { + // Copy the old directory into the new location + // NB: We use a copy instead of a move so that cross-volume migrations work + fs::copy(old_config_path, config_path, fs::copy_options::recursive | fs::copy_options::copy_symlinks, ec); + } + if (!ec) { + // If the copy was successful, delete the original directory + fs::remove_all(old_config_path, ec); + if (ec) { + std::cerr << "Failed to clean up old config directory: " << ec.message() << std::endl; + + // This is not fatal. Next time we start, we'll warn the user to delete the old one. + ec.clear(); + } + } + if (ec) { + std::cerr << "Migration failed: " << ec.message() << std::endl; + config_path = old_config_path; + } + } + else { + // We cannot use Boost logging because it hasn't been initialized yet! + std::cerr << "Config exists in both "sv << old_config_path << " and "sv << config_path << ". Using "sv << config_path << " for config" << std::endl; + std::cerr << "It is recommended to remove "sv << old_config_path << std::endl; + } + } + } + }); + + return config_path; } std::string @@ -157,7 +240,7 @@ namespace platf { } bp::child - run_command(bool elevated, bool interactive, const std::string &cmd, boost::filesystem::path &working_dir, bp::environment &env, FILE *file, std::error_code &ec, bp::group *group) { + run_command(bool elevated, bool interactive, const std::string &cmd, boost::filesystem::path &working_dir, const bp::environment &env, FILE *file, std::error_code &ec, bp::group *group) { if (!group) { if (!file) { return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_out > bp::null, bp::std_err > bp::null, ec); @@ -243,49 +326,129 @@ namespace platf { lifetime::exit_sunshine(0, true); } + /** + * @brief Attempt to gracefully terminate a process group. + * @param native_handle The process group ID. + * @return true if termination was successfully requested. + */ + bool + request_process_group_exit(std::uintptr_t native_handle) { + if (kill(-((pid_t) native_handle), SIGTERM) == 0 || errno == ESRCH) { + BOOST_LOG(debug) << "Successfully sent SIGTERM to process group: "sv << native_handle; + return true; + } + else { + BOOST_LOG(warning) << "Unable to send SIGTERM to process group ["sv << native_handle << "]: "sv << errno; + return false; + } + } + + /** + * @brief Checks if a process group still has running children. + * @param native_handle The process group ID. + * @return true if processes are still running. + */ + bool + process_group_running(std::uintptr_t native_handle) { + return waitpid(-((pid_t) native_handle), nullptr, WNOHANG) >= 0; + } + + struct sockaddr_in + to_sockaddr(boost::asio::ip::address_v4 address, uint16_t port) { + struct sockaddr_in saddr_v4 = {}; + + saddr_v4.sin_family = AF_INET; + saddr_v4.sin_port = htons(port); + + auto addr_bytes = address.to_bytes(); + memcpy(&saddr_v4.sin_addr, addr_bytes.data(), sizeof(saddr_v4.sin_addr)); + + return saddr_v4; + } + + struct sockaddr_in6 + to_sockaddr(boost::asio::ip::address_v6 address, uint16_t port) { + struct sockaddr_in6 saddr_v6 = {}; + + saddr_v6.sin6_family = AF_INET6; + saddr_v6.sin6_port = htons(port); + saddr_v6.sin6_scope_id = address.scope_id(); + + auto addr_bytes = address.to_bytes(); + memcpy(&saddr_v6.sin6_addr, addr_bytes.data(), sizeof(saddr_v6.sin6_addr)); + + return saddr_v6; + } + bool send_batch(batched_send_info_t &send_info) { auto sockfd = (int) send_info.native_socket; + struct msghdr msg = {}; // Convert the target address into a sockaddr - struct sockaddr_in saddr_v4 = {}; - struct sockaddr_in6 saddr_v6 = {}; - struct sockaddr *addr; - socklen_t addr_len; + struct sockaddr_in taddr_v4 = {}; + struct sockaddr_in6 taddr_v6 = {}; if (send_info.target_address.is_v6()) { - auto address_v6 = send_info.target_address.to_v6(); + taddr_v6 = to_sockaddr(send_info.target_address.to_v6(), send_info.target_port); - saddr_v6.sin6_family = AF_INET6; - saddr_v6.sin6_port = htons(send_info.target_port); - saddr_v6.sin6_scope_id = address_v6.scope_id(); + msg.msg_name = (struct sockaddr *) &taddr_v6; + msg.msg_namelen = sizeof(taddr_v6); + } + else { + taddr_v4 = to_sockaddr(send_info.target_address.to_v4(), send_info.target_port); + + msg.msg_name = (struct sockaddr *) &taddr_v4; + msg.msg_namelen = sizeof(taddr_v4); + } + + union { + char buf[CMSG_SPACE(sizeof(uint16_t)) + + std::max(CMSG_SPACE(sizeof(struct in_pktinfo)), CMSG_SPACE(sizeof(struct in6_pktinfo)))]; + struct cmsghdr alignment; + } cmbuf = {}; // Must be zeroed for CMSG_NXTHDR() + socklen_t cmbuflen = 0; - auto addr_bytes = address_v6.to_bytes(); - memcpy(&saddr_v6.sin6_addr, addr_bytes.data(), sizeof(saddr_v6.sin6_addr)); + msg.msg_control = cmbuf.buf; + msg.msg_controllen = sizeof(cmbuf.buf); - addr = (struct sockaddr *) &saddr_v6; - addr_len = sizeof(saddr_v6); + // The PKTINFO option will always be first, then we will conditionally + // append the UDP_SEGMENT option next if applicable. + auto pktinfo_cm = CMSG_FIRSTHDR(&msg); + if (send_info.source_address.is_v6()) { + struct in6_pktinfo pktInfo; + + struct sockaddr_in6 saddr_v6 = to_sockaddr(send_info.source_address.to_v6(), 0); + pktInfo.ipi6_addr = saddr_v6.sin6_addr; + pktInfo.ipi6_ifindex = 0; + + cmbuflen += CMSG_SPACE(sizeof(pktInfo)); + + pktinfo_cm->cmsg_level = IPPROTO_IPV6; + pktinfo_cm->cmsg_type = IPV6_PKTINFO; + pktinfo_cm->cmsg_len = CMSG_LEN(sizeof(pktInfo)); + memcpy(CMSG_DATA(pktinfo_cm), &pktInfo, sizeof(pktInfo)); } else { - auto address_v4 = send_info.target_address.to_v4(); + struct in_pktinfo pktInfo; - saddr_v4.sin_family = AF_INET; - saddr_v4.sin_port = htons(send_info.target_port); + struct sockaddr_in saddr_v4 = to_sockaddr(send_info.source_address.to_v4(), 0); + pktInfo.ipi_spec_dst = saddr_v4.sin_addr; + pktInfo.ipi_ifindex = 0; - auto addr_bytes = address_v4.to_bytes(); - memcpy(&saddr_v4.sin_addr, addr_bytes.data(), sizeof(saddr_v4.sin_addr)); + cmbuflen += CMSG_SPACE(sizeof(pktInfo)); - addr = (struct sockaddr *) &saddr_v4; - addr_len = sizeof(saddr_v4); + pktinfo_cm->cmsg_level = IPPROTO_IP; + pktinfo_cm->cmsg_type = IP_PKTINFO; + pktinfo_cm->cmsg_len = CMSG_LEN(sizeof(pktInfo)); + memcpy(CMSG_DATA(pktinfo_cm), &pktInfo, sizeof(pktInfo)); } #ifdef UDP_SEGMENT { - struct msghdr msg = {}; struct iovec iov = {}; - union { - char buf[CMSG_SPACE(sizeof(uint16_t))]; - struct cmsghdr alignment; - } cmbuf; + + msg.msg_iov = &iov; + msg.msg_iovlen = 1; // UDP GSO on Linux currently only supports sending 64K or 64 segments at a time size_t seg_index = 0; @@ -294,26 +457,19 @@ namespace platf { iov.iov_base = (void *) &send_info.buffer[seg_index * send_info.block_size]; iov.iov_len = send_info.block_size * std::min(send_info.block_count - seg_index, seg_max); - msg.msg_name = addr; - msg.msg_namelen = addr_len; - msg.msg_iov = &iov; - msg.msg_iovlen = 1; - // We should not use GSO if the data is <= one full block size if (iov.iov_len > send_info.block_size) { - msg.msg_control = cmbuf.buf; - msg.msg_controllen = CMSG_SPACE(sizeof(uint16_t)); + msg.msg_controllen = cmbuflen + CMSG_SPACE(sizeof(uint16_t)); // Enable GSO to perform segmentation of our buffer for us - auto cm = CMSG_FIRSTHDR(&msg); + auto cm = CMSG_NXTHDR(&msg, pktinfo_cm); cm->cmsg_level = SOL_UDP; cm->cmsg_type = UDP_SEGMENT; cm->cmsg_len = CMSG_LEN(sizeof(uint16_t)); *((uint16_t *) CMSG_DATA(cm)) = send_info.block_size; } else { - msg.msg_control = nullptr; - msg.msg_controllen = 0; + msg.msg_controllen = cmbuflen; } // This will fail if GSO is not available, so we will fall back to non-GSO if @@ -360,10 +516,12 @@ namespace platf { iovs[i].iov_len = send_info.block_size; msgs[i] = {}; - msgs[i].msg_hdr.msg_name = addr; - msgs[i].msg_hdr.msg_namelen = addr_len; + msgs[i].msg_hdr.msg_name = msg.msg_name; + msgs[i].msg_hdr.msg_namelen = msg.msg_namelen; msgs[i].msg_hdr.msg_iov = &iovs[i]; msgs[i].msg_hdr.msg_iovlen = 1; + msgs[i].msg_hdr.msg_control = cmbuf.buf; + msgs[i].msg_hdr.msg_controllen = cmbuflen; } // Call sendmmsg() until all messages are sent @@ -398,61 +556,202 @@ namespace platf { } } + bool + send(send_info_t &send_info) { + auto sockfd = (int) send_info.native_socket; + struct msghdr msg = {}; + + // Convert the target address into a sockaddr + struct sockaddr_in taddr_v4 = {}; + struct sockaddr_in6 taddr_v6 = {}; + if (send_info.target_address.is_v6()) { + taddr_v6 = to_sockaddr(send_info.target_address.to_v6(), send_info.target_port); + + msg.msg_name = (struct sockaddr *) &taddr_v6; + msg.msg_namelen = sizeof(taddr_v6); + } + else { + taddr_v4 = to_sockaddr(send_info.target_address.to_v4(), send_info.target_port); + + msg.msg_name = (struct sockaddr *) &taddr_v4; + msg.msg_namelen = sizeof(taddr_v4); + } + + union { + char buf[std::max(CMSG_SPACE(sizeof(struct in_pktinfo)), CMSG_SPACE(sizeof(struct in6_pktinfo)))]; + struct cmsghdr alignment; + } cmbuf; + socklen_t cmbuflen = 0; + + msg.msg_control = cmbuf.buf; + msg.msg_controllen = sizeof(cmbuf.buf); + + auto pktinfo_cm = CMSG_FIRSTHDR(&msg); + if (send_info.source_address.is_v6()) { + struct in6_pktinfo pktInfo; + + struct sockaddr_in6 saddr_v6 = to_sockaddr(send_info.source_address.to_v6(), 0); + pktInfo.ipi6_addr = saddr_v6.sin6_addr; + pktInfo.ipi6_ifindex = 0; + + cmbuflen += CMSG_SPACE(sizeof(pktInfo)); + + pktinfo_cm->cmsg_level = IPPROTO_IPV6; + pktinfo_cm->cmsg_type = IPV6_PKTINFO; + pktinfo_cm->cmsg_len = CMSG_LEN(sizeof(pktInfo)); + memcpy(CMSG_DATA(pktinfo_cm), &pktInfo, sizeof(pktInfo)); + } + else { + struct in_pktinfo pktInfo; + + struct sockaddr_in saddr_v4 = to_sockaddr(send_info.source_address.to_v4(), 0); + pktInfo.ipi_spec_dst = saddr_v4.sin_addr; + pktInfo.ipi_ifindex = 0; + + cmbuflen += CMSG_SPACE(sizeof(pktInfo)); + + pktinfo_cm->cmsg_level = IPPROTO_IP; + pktinfo_cm->cmsg_type = IP_PKTINFO; + pktinfo_cm->cmsg_len = CMSG_LEN(sizeof(pktInfo)); + memcpy(CMSG_DATA(pktinfo_cm), &pktInfo, sizeof(pktInfo)); + } + + struct iovec iov = {}; + iov.iov_base = (void *) send_info.buffer; + iov.iov_len = send_info.size; + + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + + msg.msg_controllen = cmbuflen; + + auto bytes_sent = sendmsg(sockfd, &msg, 0); + + // If there's no send buffer space, wait for some to be available + while (bytes_sent < 0 && errno == EAGAIN) { + struct pollfd pfd; + + pfd.fd = sockfd; + pfd.events = POLLOUT; + + if (poll(&pfd, 1, -1) != 1) { + BOOST_LOG(warning) << "poll() failed: "sv << errno; + break; + } + + // Try to send again + bytes_sent = sendmsg(sockfd, &msg, 0); + } + + if (bytes_sent < 0) { + BOOST_LOG(warning) << "sendmsg() failed: "sv << errno; + return false; + } + + return true; + } + + // We can't track QoS state separately for each destination on this OS, + // so we keep a ref count to only disable QoS options when all clients + // are disconnected. + static std::atomic qos_ref_count = 0; + class qos_t: public deinit_t { public: - qos_t(int sockfd, int level, int option): - sockfd(sockfd), level(level), option(option) {} + qos_t(int sockfd, std::vector> options): + sockfd(sockfd), options(options) { + qos_ref_count++; + } virtual ~qos_t() { - int reset_val = -1; - if (setsockopt(sockfd, level, option, &reset_val, sizeof(reset_val)) < 0) { - BOOST_LOG(warning) << "Failed to reset IP TOS: "sv << errno; + if (--qos_ref_count == 0) { + for (const auto &tuple : options) { + auto reset_val = std::get<2>(tuple); + if (setsockopt(sockfd, std::get<0>(tuple), std::get<1>(tuple), &reset_val, sizeof(reset_val)) < 0) { + BOOST_LOG(warning) << "Failed to reset option: "sv << errno; + } + } } } private: int sockfd; - int level; - int option; + std::vector> options; }; + /** + * @brief Enables QoS on the given socket for traffic to the specified destination. + * @param native_socket The native socket handle. + * @param address The destination address for traffic sent on this socket. + * @param port The destination port for traffic sent on this socket. + * @param data_type The type of traffic sent on this socket. + * @param dscp_tagging Specifies whether to enable DSCP tagging on outgoing traffic. + */ std::unique_ptr - enable_socket_qos(uintptr_t native_socket, boost::asio::ip::address &address, uint16_t port, qos_data_type_e data_type) { + enable_socket_qos(uintptr_t native_socket, boost::asio::ip::address &address, uint16_t port, qos_data_type_e data_type, bool dscp_tagging) { int sockfd = (int) native_socket; + std::vector> reset_options; - int level; - int option; - if (address.is_v6()) { - level = SOL_IPV6; - option = IPV6_TCLASS; - } - else { - level = SOL_IP; - option = IP_TOS; - } + if (dscp_tagging) { + int level; + int option; - // The specific DSCP values here are chosen to be consistent with Windows - int dscp; - switch (data_type) { - case qos_data_type_e::video: - dscp = 40; - break; - case qos_data_type_e::audio: - dscp = 56; - break; - default: - BOOST_LOG(error) << "Unknown traffic type: "sv << (int) data_type; - return nullptr; - } + // With dual-stack sockets, Linux uses IPV6_TCLASS for IPv6 traffic + // and IP_TOS for IPv4 traffic. + if (address.is_v6() && !address.to_v6().is_v4_mapped()) { + level = SOL_IPV6; + option = IPV6_TCLASS; + } + else { + level = SOL_IP; + option = IP_TOS; + } - // Shift to put the DSCP value in the correct position in the TOS field - dscp <<= 2; + // The specific DSCP values here are chosen to be consistent with Windows, + // except that we use CS6 instead of CS7 for audio traffic. + int dscp = 0; + switch (data_type) { + case qos_data_type_e::video: + dscp = 40; + break; + case qos_data_type_e::audio: + dscp = 48; + break; + default: + BOOST_LOG(error) << "Unknown traffic type: "sv << (int) data_type; + break; + } - if (setsockopt(sockfd, level, option, &dscp, sizeof(dscp)) < 0) { - return nullptr; + if (dscp) { + // Shift to put the DSCP value in the correct position in the TOS field + dscp <<= 2; + + if (setsockopt(sockfd, level, option, &dscp, sizeof(dscp)) == 0) { + // Reset TOS to -1 when QoS is disabled + reset_options.emplace_back(std::make_tuple(level, option, -1)); + } + else { + BOOST_LOG(error) << "Failed to set TOS/TCLASS: "sv << errno; + } + } + } + + // We can use SO_PRIORITY to set outgoing traffic priority without DSCP tagging. + // + // NB: We set this after IP_TOS/IPV6_TCLASS since setting TOS value seems to + // reset SO_PRIORITY back to 0. + // + // 6 is the highest priority that can be used without SYS_CAP_ADMIN. + int priority = data_type == qos_data_type_e::audio ? 6 : 5; + if (setsockopt(sockfd, SOL_SOCKET, SO_PRIORITY, &priority, sizeof(priority)) == 0) { + // Reset SO_PRIORITY to 0 when QoS is disabled + reset_options.emplace_back(std::make_tuple(SOL_SOCKET, SO_PRIORITY, 0)); + } + else { + BOOST_LOG(error) << "Failed to set SO_PRIORITY: "sv << errno; } - return std::make_unique(sockfd, level, option); + return std::make_unique(sockfd, reset_options); } namespace source { @@ -501,13 +800,13 @@ namespace platf { #ifdef SUNSHINE_BUILD_DRM std::vector - kms_display_names(); + kms_display_names(mem_type_e hwdevice_type); std::shared_ptr kms_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config); bool verify_kms() { - return !kms_display_names().empty(); + return !kms_display_names(mem_type_e::unknown).empty(); } #endif @@ -533,7 +832,7 @@ namespace platf { if (sources[source::WAYLAND]) return wl_display_names(); #endif #ifdef SUNSHINE_BUILD_DRM - if (sources[source::KMS]) return kms_display_names(); + if (sources[source::KMS]) return kms_display_names(hwdevice_type); #endif #ifdef SUNSHINE_BUILD_X11 if (sources[source::X11]) return x11_display_names(); @@ -541,6 +840,16 @@ namespace platf { return {}; } + /** + * @brief Returns if GPUs/drivers have changed since the last call to this function. + * @return `true` if a change has occurred or if it is unknown whether a change occurred. + */ + bool + needs_encoder_reenumeration() { + // We don't track GPU state, so we will always reenumerate. Fortunately, it is fast on Linux. + return true; + } + std::shared_ptr display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config) { #ifdef SUNSHINE_BUILD_CUDA @@ -575,7 +884,6 @@ namespace platf { init() { // These are allowed to fail. gbm::init(); - va::init(); window_system = window_system_e::NONE; #ifdef SUNSHINE_BUILD_WAYLAND @@ -594,32 +902,29 @@ namespace platf { #endif #ifdef SUNSHINE_BUILD_CUDA - if (config::video.capture.empty() || config::video.capture == "nvfbc") { + if ((config::video.capture.empty() && sources.none()) || config::video.capture == "nvfbc") { if (verify_nvfbc()) { sources[source::NVFBC] = true; } } #endif #ifdef SUNSHINE_BUILD_WAYLAND - if (config::video.capture.empty() || config::video.capture == "wlr") { + if ((config::video.capture.empty() && sources.none()) || config::video.capture == "wlr") { if (verify_wl()) { sources[source::WAYLAND] = true; } } #endif #ifdef SUNSHINE_BUILD_DRM - if (config::video.capture.empty() || config::video.capture == "kms") { + if ((config::video.capture.empty() && sources.none()) || config::video.capture == "kms") { if (verify_kms()) { - if (window_system == window_system_e::WAYLAND) { - // On Wayland, using KMS, the cursor is unreliable. - // Hide it by default - display_cursor = false; - } sources[source::KMS] = true; } } #endif #ifdef SUNSHINE_BUILD_X11 + // We enumerate this capture backend regardless of other suitable sources, + // since it may be needed as a NvFBC fallback for software encoding on X11. if (config::video.capture.empty() || config::video.capture == "x11") { if (verify_x11()) { sources[source::X11] = true; diff --git a/src/platform/linux/publish.cpp b/src/platform/linux/publish.cpp index 367c7aa3a20..bc876e7728b 100644 --- a/src/platform/linux/publish.cpp +++ b/src/platform/linux/publish.cpp @@ -7,7 +7,8 @@ #include #include "misc.h" -#include "src/main.h" +#include "src/logging.h" +#include "src/network.h" #include "src/nvhttp.h" #include "src/platform/common.h" #include "src/utility.h" @@ -55,10 +56,10 @@ namespace avahi { ERR_NOT_FOUND = -30, /**< Not found */ ERR_INVALID_CONFIG = -31, /**< Configuration error */ - ERR_VERSION_MISMATCH = -32, /**< Verson mismatch */ + ERR_VERSION_MISMATCH = -32, /**< Version mismatch */ ERR_INVALID_SERVICE_SUBTYPE = -33, /**< Invalid service subtype */ ERR_INVALID_PACKET = -34, /**< Invalid packet */ - ERR_INVALID_DNS_ERROR = -35, /**< Invlaid DNS return code */ + ERR_INVALID_DNS_ERROR = -35, /**< Invalid DNS return code */ ERR_DNS_FORMERR = -36, /**< DNS Error: Form error */ ERR_DNS_SERVFAIL = -37, /**< DNS Error: Server Failure */ ERR_DNS_NXDOMAIN = -38, /**< DNS Error: No such domain */ @@ -107,7 +108,7 @@ namespace avahi { }; enum EntryGroupState { - ENTRY_GROUP_UNCOMMITED, /**< The group has not yet been commited, the user must still call avahi_entry_group_commit() */ + ENTRY_GROUP_UNCOMMITED, /**< The group has not yet been committed, the user must still call avahi_entry_group_commit() */ ENTRY_GROUP_REGISTERING, /**< The entries of the group are currently being registered */ ENTRY_GROUP_ESTABLISHED, /**< The entries have successfully been established */ ENTRY_GROUP_COLLISION, /**< A name collision for one of the entries in the group has been detected, the entries have been withdrawn */ @@ -348,7 +349,7 @@ namespace platf::publish { name.get(), SERVICE_TYPE, nullptr, nullptr, - map_port(nvhttp::PORT_HTTP), + net::map_port(nvhttp::PORT_HTTP), nullptr); if (ret < 0) { diff --git a/src/platform/linux/vaapi.cpp b/src/platform/linux/vaapi.cpp index 4a1e7df23ba..9fa751352fe 100644 --- a/src/platform/linux/vaapi.cpp +++ b/src/platform/linux/vaapi.cpp @@ -10,6 +10,7 @@ extern "C" { #include #include +#include #if !VA_CHECK_VERSION(1, 9, 0) // vaSyncBuffer stub allows Sunshine built against libva <2.9.0 to link against ffmpeg on libva 2.9.0 or later VAStatus @@ -25,7 +26,7 @@ vaSyncBuffer( #include "graphics.h" #include "misc.h" #include "src/config.h" -#include "src/main.h" +#include "src/logging.h" #include "src/platform/common.h" #include "src/utility.h" #include "src/video.h" @@ -37,7 +38,7 @@ extern "C" struct AVBufferRef; namespace va { constexpr auto SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2 = 0x40000000; constexpr auto EXPORT_SURFACE_WRITE_ONLY = 0x0002; - constexpr auto EXPORT_SURFACE_COMPOSED_LAYERS = 0x0008; + constexpr auto EXPORT_SURFACE_SEPARATE_LAYERS = 0x0004; using VADisplay = void *; using VAStatus = int; @@ -85,226 +86,23 @@ namespace va { } layers[4]; }; - /** - * @brief Defined profiles - */ - enum class profile_e { - // Profile ID used for video processing. - ProfileNone = -1, - MPEG2Simple = 0, - MPEG2Main = 1, - MPEG4Simple = 2, - MPEG4AdvancedSimple = 3, - MPEG4Main = 4, - H264Baseline = 5, - H264Main = 6, - H264High = 7, - VC1Simple = 8, - VC1Main = 9, - VC1Advanced = 10, - H263Baseline = 11, - JPEGBaseline = 12, - H264ConstrainedBaseline = 13, - VP8Version0_3 = 14, - H264MultiviewHigh = 15, - H264StereoHigh = 16, - HEVCMain = 17, - HEVCMain10 = 18, - VP9Profile0 = 19, - VP9Profile1 = 20, - VP9Profile2 = 21, - VP9Profile3 = 22, - HEVCMain12 = 23, - HEVCMain422_10 = 24, - HEVCMain422_12 = 25, - HEVCMain444 = 26, - HEVCMain444_10 = 27, - HEVCMain444_12 = 28, - HEVCSccMain = 29, - HEVCSccMain10 = 30, - HEVCSccMain444 = 31, - AV1Profile0 = 32, - AV1Profile1 = 33, - HEVCSccMain444_10 = 34, - - // Profile ID used for protected video playback. - Protected = 35 - }; - - enum class entry_e { - VLD = 1, - IZZ = 2, - IDCT = 3, - MoComp = 4, - Deblocking = 5, - EncSlice = 6, /** slice level encode */ - EncPicture = 7, /** picture encode, JPEG, etc */ - /** - * For an implementation that supports a low power/high performance variant - * for slice level encode, it can choose to expose the - * VAEntrypointEncSliceLP entrypoint. Certain encoding tools may not be - * available with this entrypoint (e.g. interlace, MBAFF) and the - * application can query the encoding configuration attributes to find - * out more details if this entrypoint is supported. - */ - EncSliceLP = 8, - VideoProc = 10, /**< Video pre/post-processing. */ - /** - * @brief FEI - * - * The purpose of FEI (Flexible Encoding Infrastructure) is to allow applications to - * have more controls and trade off quality for speed with their own IPs. - * The application can optionally provide input to ENC for extra encode control - * and get the output from ENC. Application can chose to modify the ENC - * output/PAK input during encoding, but the performance impact is significant. - * - * On top of the existing buffers for normal encode, there will be - * one extra input buffer (VAEncMiscParameterFEIFrameControl) and - * three extra output buffers (VAEncFEIMVBufferType, VAEncFEIMBModeBufferType - * and VAEncFEIDistortionBufferType) for FEI entry function. - * If separate PAK is set, two extra input buffers - * (VAEncFEIMVBufferType, VAEncFEIMBModeBufferType) are needed for PAK input. - */ - FEI = 11, - /** - * @brief Stats - * - * A pre-processing function for getting some statistics and motion vectors is added, - * and some extra controls for Encode pipeline are provided. The application can - * optionally call the statistics function to get motion vectors and statistics like - * variances, distortions before calling Encode function via this entry point. - * - * Checking whether Statistics is supported can be performed with vaQueryConfigEntrypoints(). - * If Statistics entry point is supported, then the list of returned entry-points will - * include #Stats. Supported pixel format, maximum resolution and statistics - * specific attributes can be obtained via normal attribute query. One input buffer - * (VAStatsStatisticsParameterBufferType) and one or two output buffers - * (VAStatsStatisticsBufferType, VAStatsStatisticsBottomFieldBufferType (for interlace only) - * and VAStatsMVBufferType) are needed for this entry point. - */ - Stats = 12, - /** - * @brief ProtectedTEEComm - * - * A function for communicating with TEE (Trusted Execution Environment). - */ - ProtectedTEEComm = 13, - /** - * @brief ProtectedContent - * - * A function for protected content to decrypt encrypted content. - */ - ProtectedContent = 14, - }; - - typedef VAStatus (*queryConfigEntrypoints_fn)(VADisplay dpy, profile_e profile, entry_e *entrypoint_list, int *num_entrypoints); - typedef int (*maxNumEntrypoints_fn)(VADisplay dpy); - typedef VADisplay (*getDisplayDRM_fn)(int fd); - typedef VAStatus (*terminate_fn)(VADisplay dpy); - typedef VAStatus (*initialize_fn)(VADisplay dpy, int *major_version, int *minor_version); - typedef const char *(*errorStr_fn)(VAStatus error_status); - typedef void (*VAMessageCallback)(void *user_context, const char *message); - typedef VAMessageCallback (*setErrorCallback_fn)(VADisplay dpy, VAMessageCallback callback, void *user_context); - typedef VAMessageCallback (*setInfoCallback_fn)(VADisplay dpy, VAMessageCallback callback, void *user_context); - typedef const char *(*queryVendorString_fn)(VADisplay dpy); - typedef VAStatus (*exportSurfaceHandle_fn)( - VADisplay dpy, VASurfaceID surface_id, - uint32_t mem_type, uint32_t flags, - void *descriptor); - - static maxNumEntrypoints_fn maxNumEntrypoints; - static queryConfigEntrypoints_fn queryConfigEntrypoints; - static getDisplayDRM_fn getDisplayDRM; - static terminate_fn terminate; - static initialize_fn initialize; - static errorStr_fn errorStr; - static setErrorCallback_fn setErrorCallback; - static setInfoCallback_fn setInfoCallback; - static queryVendorString_fn queryVendorString; - static exportSurfaceHandle_fn exportSurfaceHandle; - - using display_t = util::dyn_safe_ptr_v2; + using display_t = util::safe_ptr_v2; int - init_main_va() { - static void *handle { nullptr }; - static bool funcs_loaded = false; - - if (funcs_loaded) return 0; - - if (!handle) { - handle = dyn::handle({ "libva.so.2", "libva.so" }); - if (!handle) { - return -1; - } - } - - std::vector> funcs { - { (dyn::apiproc *) &maxNumEntrypoints, "vaMaxNumEntrypoints" }, - { (dyn::apiproc *) &queryConfigEntrypoints, "vaQueryConfigEntrypoints" }, - { (dyn::apiproc *) &terminate, "vaTerminate" }, - { (dyn::apiproc *) &initialize, "vaInitialize" }, - { (dyn::apiproc *) &errorStr, "vaErrorStr" }, - { (dyn::apiproc *) &setErrorCallback, "vaSetErrorCallback" }, - { (dyn::apiproc *) &setInfoCallback, "vaSetInfoCallback" }, - { (dyn::apiproc *) &queryVendorString, "vaQueryVendorString" }, - { (dyn::apiproc *) &exportSurfaceHandle, "vaExportSurfaceHandle" }, - }; - - if (dyn::load(handle, funcs)) { - return -1; - } - - funcs_loaded = true; - return 0; - } - - int - init() { - if (init_main_va()) { - return -1; - } - - static void *handle { nullptr }; - static bool funcs_loaded = false; - - if (funcs_loaded) return 0; + vaapi_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *encode_device, AVBufferRef **hw_device_buf); - if (!handle) { - handle = dyn::handle({ "libva-drm.so.2", "libva-drm.so" }); - if (!handle) { - return -1; - } - } - - std::vector> funcs { - { (dyn::apiproc *) &getDisplayDRM, "vaGetDisplayDRM" }, - }; - - if (dyn::load(handle, funcs)) { - return -1; - } - - funcs_loaded = true; - return 0; - } - - int - vaapi_make_hwdevice_ctx(platf::hwdevice_t *base, AVBufferRef **hw_device_buf); - - class va_t: public platf::hwdevice_t { + class va_t: public platf::avcodec_encode_device_t { public: int init(int in_width, int in_height, file_t &&render_device) { file = std::move(render_device); - if (!va::initialize || !gbm::create_device) { - if (!va::initialize) BOOST_LOG(warning) << "libva not initialized"sv; - if (!gbm::create_device) BOOST_LOG(warning) << "libgbm not initialized"sv; + if (!gbm::create_device) { + BOOST_LOG(warning) << "libgbm not initialized"sv; return -1; } - this->data = (void *) vaapi_make_hwdevice_ctx; + this->data = (void *) vaapi_init_avcodec_hardware_input_buffer; gbm.reset(gbm::create_device(file.el)); if (!gbm) { @@ -332,12 +130,12 @@ namespace va { } int - set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) override { + set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx_buf) override { this->hwframe.reset(frame); this->frame = frame; if (!frame->buf[0]) { - if (av_hwframe_get_buffer(hw_frames_ctx, frame, 0)) { + if (av_hwframe_get_buffer(hw_frames_ctx_buf, frame, 0)) { BOOST_LOG(error) << "Couldn't get hwframe for VAAPI"sv; return -1; } @@ -345,15 +143,16 @@ namespace va { va::DRMPRIMESurfaceDescriptor prime; va::VASurfaceID surface = (std::uintptr_t) frame->data[3]; + auto hw_frames_ctx = (AVHWFramesContext *) hw_frames_ctx_buf->data; - auto status = va::exportSurfaceHandle( + auto status = vaExportSurfaceHandle( this->va_display, surface, va::SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2, - va::EXPORT_SURFACE_WRITE_ONLY | va::EXPORT_SURFACE_COMPOSED_LAYERS, + va::EXPORT_SURFACE_WRITE_ONLY | va::EXPORT_SURFACE_SEPARATE_LAYERS, &prime); if (status) { - BOOST_LOG(error) << "Couldn't export va surface handle: ["sv << (int) surface << "]: "sv << va::errorStr(status); + BOOST_LOG(error) << "Couldn't export va surface handle: ["sv << (int) surface << "]: "sv << vaErrorStr(status); return -1; } @@ -364,29 +163,39 @@ namespace va { fds[x] = prime.objects[x].fd; } - auto nv12_opt = egl::import_target( - display.get(), - std::move(fds), - { (int) prime.width, - (int) prime.height, - { prime.objects[prime.layers[0].object_index[0]].fd, -1, -1, -1 }, - 0, - 0, - { prime.layers[0].pitch[0] }, - { prime.layers[0].offset[0] } }, - { (int) prime.width / 2, - (int) prime.height / 2, - { prime.objects[prime.layers[0].object_index[1]].fd, -1, -1, -1 }, - 0, - 0, - { prime.layers[0].pitch[1] }, - { prime.layers[0].offset[1] } }); + if (prime.num_layers != 2) { + BOOST_LOG(error) << "Invalid layer count for VA surface: expected 2, got "sv << prime.num_layers; + return -1; + } + + egl::surface_descriptor_t sds[2] = {}; + for (int plane = 0; plane < 2; ++plane) { + auto &sd = sds[plane]; + auto &layer = prime.layers[plane]; + sd.fourcc = layer.drm_format; + + // UV plane is subsampled + sd.width = prime.width / (plane == 0 ? 1 : 2); + sd.height = prime.height / (plane == 0 ? 1 : 2); + + // The modifier must be the same for all planes + sd.modifier = prime.objects[layer.object_index[0]].drm_format_modifier; + + std::fill_n(sd.fds, 4, -1); + for (int x = 0; x < layer.num_planes; ++x) { + sd.fds[x] = prime.objects[layer.object_index[x]].fd; + sd.pitches[x] = layer.pitch[x]; + sd.offsets[x] = layer.offset[x]; + } + } + + auto nv12_opt = egl::import_target(display.get(), std::move(fds), sds[0], sds[1]); if (!nv12_opt) { return -1; } - auto sws_opt = egl::sws_t::make(width, height, frame->width, frame->height); + auto sws_opt = egl::sws_t::make(width, height, frame->width, frame->height, hw_frames_ctx->sw_format); if (!sws_opt) { return -1; } @@ -398,8 +207,8 @@ namespace va { } void - set_colorspace(std::uint32_t colorspace, std::uint32_t color_range) override { - sws.set_colorspace(colorspace, color_range); + apply_colorspace() override { + sws.apply_colorspace(colorspace); } va::display_t::pointer va_display; @@ -436,7 +245,11 @@ namespace va { convert(platf::img_t &img) override { auto &descriptor = (egl::img_descriptor_t &) img; - if (descriptor.sequence > sequence) { + if (descriptor.sequence == 0) { + // For dummy images, use a blank RGB texture instead of importing a DMA-BUF + rgb = egl::create_blank(img); + } + else if (descriptor.sequence > sequence) { sequence = descriptor.sequence; rgb = egl::rgb_t {}; @@ -526,17 +339,7 @@ namespace va { } int - vaapi_make_hwdevice_ctx(platf::hwdevice_t *base, AVBufferRef **hw_device_buf) { - if (!va::initialize) { - BOOST_LOG(warning) << "libva not loaded"sv; - return -1; - } - - if (!va::getDisplayDRM) { - BOOST_LOG(warning) << "libva-drm not loaded"sv; - return -1; - } - + vaapi_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *base, AVBufferRef **hw_device_buf) { auto va = (va::va_t *) base; auto fd = dup(va->file.el); @@ -548,7 +351,7 @@ namespace va { av_free(priv); }); - va::display_t display { va::getDisplayDRM(fd) }; + va::display_t display { vaGetDisplayDRM(fd) }; if (!display) { auto render_device = config::video.adapter_name.empty() ? "/dev/dri/renderD128" : config::video.adapter_name.c_str(); @@ -558,17 +361,17 @@ namespace va { va->va_display = display.get(); - va::setErrorCallback(display.get(), __log, &error); - va::setErrorCallback(display.get(), __log, &info); + vaSetErrorCallback(display.get(), __log, &error); + vaSetErrorCallback(display.get(), __log, &info); int major, minor; - auto status = va::initialize(display.get(), &major, &minor); + auto status = vaInitialize(display.get(), &major, &minor); if (status) { - BOOST_LOG(error) << "Couldn't initialize va display: "sv << va::errorStr(status); + BOOST_LOG(error) << "Couldn't initialize va display: "sv << vaErrorStr(status); return -1; } - BOOST_LOG(debug) << "vaapi vendor: "sv << va::queryVendorString(display.get()); + BOOST_LOG(info) << "vaapi vendor: "sv << vaQueryVendorString(display.get()); *hw_device_buf = av_hwdevice_ctx_alloc(AV_HWDEVICE_TYPE_VAAPI); auto ctx = (AVHWDeviceContext *) (*hw_device_buf)->data; @@ -592,20 +395,20 @@ namespace va { } static bool - query(display_t::pointer display, profile_e profile) { - std::vector entrypoints; - entrypoints.resize(maxNumEntrypoints(display)); + query(display_t::pointer display, VAProfile profile) { + std::vector entrypoints; + entrypoints.resize(vaMaxNumEntrypoints(display)); int count; - auto status = queryConfigEntrypoints(display, profile, entrypoints.data(), &count); + auto status = vaQueryConfigEntrypoints(display, profile, entrypoints.data(), &count); if (status) { - BOOST_LOG(error) << "Couldn't query entrypoints: "sv << va::errorStr(status); + BOOST_LOG(error) << "Couldn't query entrypoints: "sv << vaErrorStr(status); return false; } entrypoints.resize(count); for (auto entrypoint : entrypoints) { - if (entrypoint == entry_e::EncSlice || entrypoint == entry_e::EncSliceLP) { + if (entrypoint == VAEntrypointEncSlice || entrypoint == VAEntrypointEncSliceLP) { return true; } } @@ -615,11 +418,7 @@ namespace va { bool validate(int fd) { - if (init()) { - return false; - } - - va::display_t display { va::getDisplayDRM(fd) }; + va::display_t display { vaGetDisplayDRM(fd) }; if (!display) { char string[1024]; @@ -632,31 +431,31 @@ namespace va { } int major, minor; - auto status = initialize(display.get(), &major, &minor); + auto status = vaInitialize(display.get(), &major, &minor); if (status) { - BOOST_LOG(error) << "Couldn't initialize va display: "sv << va::errorStr(status); + BOOST_LOG(error) << "Couldn't initialize va display: "sv << vaErrorStr(status); return false; } - if (!query(display.get(), profile_e::H264Main)) { + if (!query(display.get(), VAProfileH264Main)) { return false; } - if (video::active_hevc_mode > 1 && !query(display.get(), profile_e::HEVCMain)) { + if (video::active_hevc_mode > 1 && !query(display.get(), VAProfileHEVCMain)) { return false; } - if (video::active_hevc_mode > 2 && !query(display.get(), profile_e::HEVCMain10)) { + if (video::active_hevc_mode > 2 && !query(display.get(), VAProfileHEVCMain10)) { return false; } return true; } - std::shared_ptr - make_hwdevice(int width, int height, file_t &&card, int offset_x, int offset_y, bool vram) { + std::unique_ptr + make_avcodec_encode_device(int width, int height, file_t &&card, int offset_x, int offset_y, bool vram) { if (vram) { - auto egl = std::make_shared(); + auto egl = std::make_unique(); if (egl->init(width, height, std::move(card), offset_x, offset_y)) { return nullptr; } @@ -665,7 +464,7 @@ namespace va { } else { - auto egl = std::make_shared(); + auto egl = std::make_unique(); if (egl->init(width, height, std::move(card))) { return nullptr; } @@ -674,8 +473,8 @@ namespace va { } } - std::shared_ptr - make_hwdevice(int width, int height, int offset_x, int offset_y, bool vram) { + std::unique_ptr + make_avcodec_encode_device(int width, int height, int offset_x, int offset_y, bool vram) { auto render_device = config::video.adapter_name.empty() ? "/dev/dri/renderD128" : config::video.adapter_name.c_str(); file_t file = open(render_device, O_RDWR); @@ -686,11 +485,11 @@ namespace va { return nullptr; } - return make_hwdevice(width, height, std::move(file), offset_x, offset_y, vram); + return make_avcodec_encode_device(width, height, std::move(file), offset_x, offset_y, vram); } - std::shared_ptr - make_hwdevice(int width, int height, bool vram) { - return make_hwdevice(width, height, 0, 0, vram); + std::unique_ptr + make_avcodec_encode_device(int width, int height, bool vram) { + return make_avcodec_encode_device(width, height, 0, 0, vram); } } // namespace va diff --git a/src/platform/linux/vaapi.h b/src/platform/linux/vaapi.h index 081d004897b..50d58b7d8d0 100644 --- a/src/platform/linux/vaapi.h +++ b/src/platform/linux/vaapi.h @@ -18,17 +18,14 @@ namespace va { * offset_y --> Vertical offset of the image in the texture * file_t card --> The file descriptor of the render device used for encoding */ - std::shared_ptr - make_hwdevice(int width, int height, bool vram); - std::shared_ptr - make_hwdevice(int width, int height, int offset_x, int offset_y, bool vram); - std::shared_ptr - make_hwdevice(int width, int height, file_t &&card, int offset_x, int offset_y, bool vram); + std::unique_ptr + make_avcodec_encode_device(int width, int height, bool vram); + std::unique_ptr + make_avcodec_encode_device(int width, int height, int offset_x, int offset_y, bool vram); + std::unique_ptr + make_avcodec_encode_device(int width, int height, file_t &&card, int offset_x, int offset_y, bool vram); // Ensure the render device pointed to by fd is capable of encoding h264 with the hevc_mode configured bool validate(int fd); - - int - init(); } // namespace va diff --git a/src/platform/linux/wayland.cpp b/src/platform/linux/wayland.cpp index d601ba95daa..8dcd22c3639 100644 --- a/src/platform/linux/wayland.cpp +++ b/src/platform/linux/wayland.cpp @@ -2,13 +2,14 @@ * @file src/platform/linux/wayland.cpp * @brief todo */ +#include #include #include #include #include "graphics.h" -#include "src/main.h" +#include "src/logging.h" #include "src/platform/common.h" #include "src/round_robin.h" #include "src/utility.h" @@ -61,13 +62,52 @@ namespace wl { wl_display_roundtrip(display_internal.get()); } + /** + * @brief Waits up to the specified timeout to dispatch new events on the wl_display. + * @param timeout The timeout in milliseconds. + * @return true if new events were dispatched or false if the timeout expired. + */ + bool + display_t::dispatch(std::chrono::milliseconds timeout) { + // Check if any events are queued already. If not, flush + // outgoing events, and prepare to wait for readability. + if (wl_display_prepare_read(display_internal.get()) == 0) { + wl_display_flush(display_internal.get()); + + // Wait for an event to come in + struct pollfd pfd = {}; + pfd.fd = wl_display_get_fd(display_internal.get()); + pfd.events = POLLIN; + if (poll(&pfd, 1, timeout.count()) == 1 && (pfd.revents & POLLIN)) { + // Read the new event(s) + wl_display_read_events(display_internal.get()); + } + else { + // We timed out, so unlock the queue now + wl_display_cancel_read(display_internal.get()); + return false; + } + } + + // Dispatch any existing or new pending events + wl_display_dispatch_pending(display_internal.get()); + return true; + } + wl_registry * display_t::registry() { return wl_display_get_registry(display_internal.get()); } inline monitor_t::monitor_t(wl_output *output): - output { output }, listener { + output { output }, + wl_listener { + &CLASS_CALL(monitor_t, wl_geometry), + &CLASS_CALL(monitor_t, wl_mode), + &CLASS_CALL(monitor_t, wl_done), + &CLASS_CALL(monitor_t, wl_scale), + }, + xdg_listener { &CLASS_CALL(monitor_t, xdg_position), &CLASS_CALL(monitor_t, xdg_size), &CLASS_CALL(monitor_t, xdg_done), @@ -99,21 +139,23 @@ namespace wl { void monitor_t::xdg_size(zxdg_output_v1 *, std::int32_t width, std::int32_t height) { + BOOST_LOG(info) << "Logical size: "sv << width << 'x' << height; + } + + void + monitor_t::wl_mode(wl_output *wl_output, std::uint32_t flags, + std::int32_t width, std::int32_t height, std::int32_t refresh) { viewport.width = width; viewport.height = height; BOOST_LOG(info) << "Resolution: "sv << width << 'x' << height; } - void - monitor_t::xdg_done(zxdg_output_v1 *) { - BOOST_LOG(info) << "All info about monitor ["sv << name << "] has been send"sv; - } - void monitor_t::listen(zxdg_output_manager_v1 *output_manager) { auto xdg_output = zxdg_output_manager_v1_get_xdg_output(output_manager, output); - zxdg_output_v1_add_listener(xdg_output, &listener, this); + zxdg_output_v1_add_listener(xdg_output, &xdg_listener, this); + wl_output_add_listener(output, &wl_listener, this); } interface_t::interface_t() noexcept @@ -137,7 +179,7 @@ namespace wl { BOOST_LOG(info) << "Found interface: "sv << interface << '(' << id << ") version "sv << version; monitors.emplace_back( std::make_unique( - (wl_output *) wl_registry_bind(registry, id, &wl_output_interface, version))); + (wl_output *) wl_registry_bind(registry, id, &wl_output_interface, 2))); } else if (!std::strcmp(interface, zxdg_output_manager_v1_interface.name)) { BOOST_LOG(info) << "Found interface: "sv << interface << '(' << id << ") version "sv << version; diff --git a/src/platform/linux/wayland.h b/src/platform/linux/wayland.h index a4c3aef17d9..062aa65f1ea 100644 --- a/src/platform/linux/wayland.h +++ b/src/platform/linux/wayland.h @@ -118,7 +118,19 @@ namespace wl { void xdg_size(zxdg_output_v1 *, std::int32_t width, std::int32_t height); void - xdg_done(zxdg_output_v1 *); + xdg_done(zxdg_output_v1 *) {} + + void + wl_geometry(wl_output *wl_output, std::int32_t x, std::int32_t y, + std::int32_t physical_width, std::int32_t physical_height, std::int32_t subpixel, + const char *make, const char *model, std::int32_t transform) {} + void + wl_mode(wl_output *wl_output, std::uint32_t flags, + std::int32_t width, std::int32_t height, std::int32_t refresh); + void + wl_done(wl_output *wl_output) {} + void + wl_scale(wl_output *wl_output, std::int32_t factor) {} void listen(zxdg_output_manager_v1 *output_manager); @@ -130,7 +142,8 @@ namespace wl { platf::touch_port_t viewport; - zxdg_output_v1_listener listener; + wl_output_listener wl_listener; + zxdg_output_v1_listener xdg_listener; }; class interface_t { @@ -193,6 +206,10 @@ namespace wl { void roundtrip(); + // Wait up to the timeout to read and dispatch new events + bool + dispatch(std::chrono::milliseconds timeout); + // Get the registry associated with the display // No need to manually free the registry wl_registry * diff --git a/src/platform/linux/wlgrab.cpp b/src/platform/linux/wlgrab.cpp index 6cf7fb78070..a6ac4adbb96 100644 --- a/src/platform/linux/wlgrab.cpp +++ b/src/platform/linux/wlgrab.cpp @@ -2,10 +2,14 @@ * @file src/platform/linux/wlgrab.cpp * @brief todo */ +#include + #include "src/platform/common.h" -#include "src/main.h" +#include "src/logging.h" #include "src/video.h" + +#include "cuda.h" #include "vaapi.h" #include "wayland.h" @@ -87,11 +91,11 @@ namespace wl { snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor) { auto to = std::chrono::steady_clock::now() + timeout; + // Dispatch events until we get a new frame or the timeout expires dmabuf.listen(interface.dmabuf_manager, output, cursor); do { - display.roundtrip(); - - if (to < std::chrono::steady_clock::now()) { + auto remaining_time_ms = std::chrono::duration_cast(to - std::chrono::steady_clock::now()); + if (remaining_time_ms.count() < 0 || !display.dispatch(remaining_time_ms)) { return platf::capture_e::timeout; } } while (dmabuf.status == dmabuf_t::WAITING); @@ -125,16 +129,22 @@ namespace wl { capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override { auto next_frame = std::chrono::steady_clock::now(); + sleep_overshoot_tracker.reset(); + while (true) { auto now = std::chrono::steady_clock::now(); if (next_frame > now) { - std::this_thread::sleep_for((next_frame - now) / 3 * 2); + std::this_thread::sleep_for(next_frame - now); } - while (next_frame > now) { - now = std::chrono::steady_clock::now(); + now = std::chrono::steady_clock::now(); + std::chrono::nanoseconds overshoot_ns = now - next_frame; + log_sleep_overshoot(overshoot_ns); + + next_frame += delay; + if (next_frame < now) { // some major slowdown happened; we couldn't keep up + next_frame = now + delay; } - next_frame = now + delay; std::shared_ptr img_out; auto status = snapshot(pull_free_image_cb, img_out, 1000ms, *cursor); @@ -183,6 +193,7 @@ namespace wl { gl::ctx.BindTexture(GL_TEXTURE_2D, (*rgb_opt)->tex[0]); + // Don't remove these lines, see https://github.com/LizardByte/Sunshine/issues/453 int w, h; gl::ctx.GetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &w); gl::ctx.GetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &h); @@ -215,13 +226,21 @@ namespace wl { return 0; } - std::shared_ptr - make_hwdevice(platf::pix_fmt_e pix_fmt) override { + std::unique_ptr + make_avcodec_encode_device(platf::pix_fmt_e pix_fmt) override { +#ifdef SUNSHINE_BUILD_VAAPI if (mem_type == platf::mem_type_e::vaapi) { - return va::make_hwdevice(width, height, false); + return va::make_avcodec_encode_device(width, height, false); } +#endif - return std::make_shared(); +#ifdef SUNSHINE_BUILD_CUDA + if (mem_type == platf::mem_type_e::cuda) { + return cuda::make_avcodec_encode_device(width, height, false); + } +#endif + + return std::make_unique(); } std::shared_ptr @@ -246,16 +265,22 @@ namespace wl { capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override { auto next_frame = std::chrono::steady_clock::now(); + sleep_overshoot_tracker.reset(); + while (true) { auto now = std::chrono::steady_clock::now(); if (next_frame > now) { - std::this_thread::sleep_for((next_frame - now) / 3 * 2); + std::this_thread::sleep_for(next_frame - now); } - while (next_frame > now) { - now = std::chrono::steady_clock::now(); + now = std::chrono::steady_clock::now(); + std::chrono::nanoseconds overshoot_ns = now - next_frame; + log_sleep_overshoot(overshoot_ns); + + next_frame += delay; + if (next_frame < now) { // some major slowdown happened; we couldn't keep up + next_frame = now + delay; } - next_frame = now + delay; std::shared_ptr img_out; auto status = snapshot(pull_free_image_cb, img_out, 1000ms, *cursor); @@ -313,6 +338,8 @@ namespace wl { alloc_img() override { auto img = std::make_shared(); + img->width = width; + img->height = height; img->sequence = 0; img->serial = std::numeric_limitsserial)>::max(); img->data = nullptr; @@ -323,27 +350,27 @@ namespace wl { return img; } - std::shared_ptr - make_hwdevice(platf::pix_fmt_e pix_fmt) override { + std::unique_ptr + make_avcodec_encode_device(platf::pix_fmt_e pix_fmt) override { +#ifdef SUNSHINE_BUILD_VAAPI if (mem_type == platf::mem_type_e::vaapi) { - return va::make_hwdevice(width, height, 0, 0, true); + return va::make_avcodec_encode_device(width, height, 0, 0, true); } +#endif - return std::make_shared(); +#ifdef SUNSHINE_BUILD_CUDA + if (mem_type == platf::mem_type_e::cuda) { + return cuda::make_avcodec_gl_encode_device(width, height, 0, 0); + } +#endif + + return std::make_unique(); } int dummy_img(platf::img_t *img) override { - // TODO: stop cheating and give black image - if (!img) { - return -1; - }; - auto pull_dummy_img_callback = [&img](std::shared_ptr &img_out) -> bool { - img_out = img->shared_from_this(); - return true; - }; - std::shared_ptr img_out; - return snapshot(pull_dummy_img_callback, img_out, 1000ms, false) != platf::capture_e::ok; + // Empty images are recognized as dummies by the zero sequence number + return 0; } std::uint64_t sequence {}; @@ -359,7 +386,7 @@ namespace platf { return nullptr; } - if (hwdevice_type == platf::mem_type_e::vaapi) { + if (hwdevice_type == platf::mem_type_e::vaapi || hwdevice_type == platf::mem_type_e::cuda) { auto wlr = std::make_shared(); if (wlr->init(hwdevice_type, display_name, config)) { return nullptr; @@ -409,15 +436,21 @@ namespace platf { display.roundtrip(); + BOOST_LOG(info) << "-------- Start of Wayland monitor list --------"sv; + for (int x = 0; x < interface.monitors.size(); ++x) { auto monitor = interface.monitors[x].get(); wl::env_width = std::max(wl::env_width, (int) (monitor->viewport.offset_x + monitor->viewport.width)); wl::env_height = std::max(wl::env_height, (int) (monitor->viewport.offset_y + monitor->viewport.height)); + BOOST_LOG(info) << "Monitor " << x << " is "sv << monitor->name << ": "sv << monitor->description; + display_names.emplace_back(std::to_string(x)); } + BOOST_LOG(info) << "--------- End of Wayland monitor list ---------"sv; + return display_names; } diff --git a/src/platform/linux/x11grab.cpp b/src/platform/linux/x11grab.cpp index ad8ef0343ff..3f6cd0c51b4 100644 --- a/src/platform/linux/x11grab.cpp +++ b/src/platform/linux/x11grab.cpp @@ -5,6 +5,7 @@ #include "src/platform/common.h" #include +#include #include #include @@ -17,7 +18,8 @@ #include #include "src/config.h" -#include "src/main.h" +#include "src/globals.h" +#include "src/logging.h" #include "src/task_pool.h" #include "src/video.h" @@ -419,7 +421,7 @@ namespace platf { } if (streamedMonitor != -1) { - BOOST_LOG(info) << "Configuring selected monitor ("sv << streamedMonitor << ") to stream"sv; + BOOST_LOG(info) << "Configuring selected display ("sv << streamedMonitor << ") to stream"sv; screen_res_t screenr { x11::rr::GetScreenResources(xdisplay.get(), xwindow) }; int output = screenr->noutput; @@ -479,17 +481,22 @@ namespace platf { capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override { auto next_frame = std::chrono::steady_clock::now(); + sleep_overshoot_tracker.reset(); + while (true) { auto now = std::chrono::steady_clock::now(); if (next_frame > now) { - std::this_thread::sleep_for((next_frame - now) / 3 * 2); + std::this_thread::sleep_for(next_frame - now); } - while (next_frame > now) { - std::this_thread::sleep_for(1ns); - now = std::chrono::steady_clock::now(); + now = std::chrono::steady_clock::now(); + std::chrono::nanoseconds overshoot_ns = now - next_frame; + log_sleep_overshoot(overshoot_ns); + + next_frame += delay; + if (next_frame < now) { // some major slowdown happened; we couldn't keep up + next_frame = now + delay; } - next_frame = now + delay; std::shared_ptr img_out; auto status = snapshot(pull_free_image_cb, img_out, 1000ms, *cursor); @@ -533,6 +540,7 @@ namespace platf { auto img = (x11_img_t *) img_out.get(); XImage *x_img { x11::GetImage(xdisplay.get(), xwindow, offset_x, offset_y, width, height, AllPlanes, ZPixmap) }; + img->frame_timestamp = std::chrono::steady_clock::now(); img->width = x_img->width; img->height = x_img->height; @@ -553,19 +561,21 @@ namespace platf { return std::make_shared(); } - std::shared_ptr - make_hwdevice(pix_fmt_e pix_fmt) override { + std::unique_ptr + make_avcodec_encode_device(pix_fmt_e pix_fmt) override { +#ifdef SUNSHINE_BUILD_VAAPI if (mem_type == mem_type_e::vaapi) { - return va::make_hwdevice(width, height, false); + return va::make_avcodec_encode_device(width, height, false); } +#endif #ifdef SUNSHINE_BUILD_CUDA if (mem_type == mem_type_e::cuda) { - return cuda::make_hwdevice(width, height, false); + return cuda::make_avcodec_encode_device(width, height, false); } #endif - return std::make_shared(); + return std::make_unique(); } int @@ -617,17 +627,22 @@ namespace platf { capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override { auto next_frame = std::chrono::steady_clock::now(); + sleep_overshoot_tracker.reset(); + while (true) { auto now = std::chrono::steady_clock::now(); if (next_frame > now) { - std::this_thread::sleep_for((next_frame - now) / 3 * 2); + std::this_thread::sleep_for(next_frame - now); } - while (next_frame > now) { - std::this_thread::sleep_for(1ns); - now = std::chrono::steady_clock::now(); + now = std::chrono::steady_clock::now(); + std::chrono::nanoseconds overshoot_ns = now - next_frame; + log_sleep_overshoot(overshoot_ns); + + next_frame += delay; + if (next_frame < now) { // some major slowdown happened; we couldn't keep up + next_frame = now + delay; } - next_frame = now + delay; std::shared_ptr img_out; auto status = snapshot(pull_free_image_cb, img_out, 1000ms, *cursor); @@ -664,6 +679,7 @@ namespace platf { } else { auto img_cookie = xcb::shm_get_image_unchecked(xcb.get(), display->root, offset_x, offset_y, width, height, ~0, XCB_IMAGE_FORMAT_Z_PIXMAP, seg, 0); + auto frame_timestamp = std::chrono::steady_clock::now(); xcb_img_t img_reply { xcb::shm_get_image_reply(xcb.get(), img_cookie, nullptr) }; if (!img_reply) { @@ -676,6 +692,7 @@ namespace platf { } std::copy_n((std::uint8_t *) data.data, frame_size(), img_out->data); + img_out->frame_timestamp = frame_timestamp; if (cursor) { blend_cursor(shm_xdisplay.get(), *img_out, offset_x, offset_y); @@ -791,7 +808,7 @@ namespace platf { return {}; } - BOOST_LOG(info) << "Detecting monitors"sv; + BOOST_LOG(info) << "Detecting displays"sv; x11::xdisplay_t xdisplay { x11::OpenDisplay(nullptr) }; if (!xdisplay) { @@ -806,7 +823,7 @@ namespace platf { for (int x = 0; x < output; ++x) { output_info_t out_info { x11::rr::GetOutputInfo(xdisplay.get(), screenr.get(), screenr->outputs[x]) }; if (out_info) { - BOOST_LOG(info) << "Detected monitor "sv << monitor << ": "sv << out_info->name << ", connected: "sv << (out_info->connection == RR_Connected); + BOOST_LOG(info) << "Detected display: "sv << out_info->name << " (id: "sv << monitor << ")"sv << out_info->name << " connected: "sv << (out_info->connection == RR_Connected); ++monitor; } } @@ -881,8 +898,8 @@ namespace platf { } img.data = img.buffer.data(); - img.width = xcursor->width; - img.height = xcursor->height; + img.width = img.src_w = xcursor->width; + img.height = img.src_h = xcursor->height; img.x = xcursor->x - xcursor->xhot; img.y = xcursor->y - xcursor->yhot; img.pixel_pitch = 4; diff --git a/src/platform/linux/x11grab.h b/src/platform/linux/x11grab.h index 24e96f6a1fd..f03339cc45b 100644 --- a/src/platform/linux/x11grab.h +++ b/src/platform/linux/x11grab.h @@ -17,8 +17,6 @@ namespace egl { } namespace platf::x11 { - -#ifdef SUNSHINE_BUILD_X11 struct cursor_ctx_raw_t; void freeCursorCtx(cursor_ctx_raw_t *ctx); @@ -50,22 +48,4 @@ namespace platf::x11 { xdisplay_t make_display(); -#else - // It's never something different from nullptr - util::safe_ptr<_XDisplay, std::default_delete<_XDisplay>>; - - class cursor_t { - public: - static std::optional - make() { return std::nullopt; } - - void - capture(egl::cursor_t &) {} - void - blend(img_t &, int, int) {} - }; - - xdisplay_t - make_display() { return nullptr; } -#endif } // namespace platf::x11 diff --git a/src/platform/macos/av_audio.m b/src/platform/macos/av_audio.m index af695179c7b..cb0c83c2e97 100644 --- a/src/platform/macos/av_audio.m +++ b/src/platform/macos/av_audio.m @@ -130,9 +130,9 @@ - (void)captureOutput:(AVCaptureOutput *)output CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer(sampleBuffer, NULL, &audioBufferList, sizeof(audioBufferList), NULL, NULL, 0, &blockBuffer); - // NSAssert(audioBufferList.mNumberBuffers == 1, @"Expected interlveaved PCM format but buffer contained %u streams", audioBufferList.mNumberBuffers); + // NSAssert(audioBufferList.mNumberBuffers == 1, @"Expected interleaved PCM format but buffer contained %u streams", audioBufferList.mNumberBuffers); - // this is safe, because an interleaved PCM stream has exactly one buffer + // this is safe, because an interleaved PCM stream has exactly one buffer, // and we don't want to do sanity checks in a performance critical exec path AudioBuffer audioBuffer = audioBufferList.mBuffers[0]; diff --git a/src/platform/macos/av_img_t.h b/src/platform/macos/av_img_t.h index f42f9ceb12e..42796800684 100644 --- a/src/platform/macos/av_img_t.h +++ b/src/platform/macos/av_img_t.h @@ -10,10 +10,57 @@ #include namespace platf { - struct av_img_t: public img_t { - CVPixelBufferRef pixel_buffer = nullptr; - CMSampleBufferRef sample_buffer = nullptr; + struct av_sample_buf_t { + CMSampleBufferRef buf; - ~av_img_t(); + explicit av_sample_buf_t(CMSampleBufferRef buf): + buf((CMSampleBufferRef) CFRetain(buf)) {} + + ~av_sample_buf_t() { + if (buf != nullptr) { + CFRelease(buf); + } + } + }; + + struct av_pixel_buf_t { + CVPixelBufferRef buf; + + // Constructor + explicit av_pixel_buf_t(CMSampleBufferRef sb): + buf( + CMSampleBufferGetImageBuffer(sb)) { + CVPixelBufferLockBaseAddress(buf, kCVPixelBufferLock_ReadOnly); + } + + [[nodiscard]] uint8_t * + data() const { + return static_cast(CVPixelBufferGetBaseAddress(buf)); + } + + // Destructor + ~av_pixel_buf_t() { + if (buf != nullptr) { + CVPixelBufferUnlockBaseAddress(buf, kCVPixelBufferLock_ReadOnly); + } + } + }; + + struct av_img_t: img_t { + std::shared_ptr sample_buffer; + std::shared_ptr pixel_buffer; + }; + + struct temp_retain_av_img_t { + std::shared_ptr sample_buffer; + std::shared_ptr pixel_buffer; + uint8_t *data; + + temp_retain_av_img_t( + std::shared_ptr sb, + std::shared_ptr pb, + uint8_t *dt): + sample_buffer(std::move(sb)), + pixel_buffer(std::move(pb)), data(dt) {} }; } // namespace platf diff --git a/src/platform/macos/av_video.h b/src/platform/macos/av_video.h index 83eabb8ebd4..fac5a78c6e6 100644 --- a/src/platform/macos/av_video.h +++ b/src/platform/macos/av_video.h @@ -5,6 +5,7 @@ #pragma once #import +#import struct CaptureSession { AVCaptureVideoDataOutput *output; @@ -20,11 +21,6 @@ struct CaptureSession { @property (nonatomic, assign) OSType pixelFormat; @property (nonatomic, assign) int frameWidth; @property (nonatomic, assign) int frameHeight; -@property (nonatomic, assign) float scaling; -@property (nonatomic, assign) int paddingLeft; -@property (nonatomic, assign) int paddingRight; -@property (nonatomic, assign) int paddingTop; -@property (nonatomic, assign) int paddingBottom; typedef bool (^FrameCallbackBlock)(CMSampleBufferRef); @@ -34,6 +30,7 @@ typedef bool (^FrameCallbackBlock)(CMSampleBufferRef); @property (nonatomic, assign) NSMapTable *captureSignals; + (NSArray *)displayNames; ++ (NSString *)getDisplayName:(CGDirectDisplayID)displayID; - (id)initWithDisplay:(CGDirectDisplayID)displayID frameRate:(int)frameRate; diff --git a/src/platform/macos/av_video.m b/src/platform/macos/av_video.m index 6e3a9f81f66..874a87f7b18 100644 --- a/src/platform/macos/av_video.m +++ b/src/platform/macos/av_video.m @@ -23,13 +23,24 @@ @implementation AVVideo for (uint32_t i = 0; i < count; i++) { [result addObject:@{ @"id": [NSNumber numberWithUnsignedInt:displays[i]], - @"name": [NSString stringWithFormat:@"%d", displays[i]] + @"name": [NSString stringWithFormat:@"%d", displays[i]], + @"displayName": [self getDisplayName:displays[i]], }]; } return [NSArray arrayWithArray:result]; } ++ (NSString *)getDisplayName:(CGDirectDisplayID)displayID { + NSScreen *screens = [NSScreen screens]; + for (NSScreen *screen in screens) { + if (screen.deviceDescription[@"NSScreenNumber"] == [NSNumber numberWithUnsignedInt:displayID]) { + return screen.localizedName; + } + } + return nil; +} + - (id)initWithDisplay:(CGDirectDisplayID)displayID frameRate:(int)frameRate { self = [super init]; @@ -37,13 +48,8 @@ - (id)initWithDisplay:(CGDirectDisplayID)displayID frameRate:(int)frameRate { self.displayID = displayID; self.pixelFormat = kCVPixelFormatType_32BGRA; - self.frameWidth = CGDisplayModeGetPixelWidth(mode); - self.frameHeight = CGDisplayModeGetPixelHeight(mode); - self.scaling = CGDisplayPixelsWide(displayID) / CGDisplayModeGetPixelWidth(mode); - self.paddingLeft = 0; - self.paddingRight = 0; - self.paddingTop = 0; - self.paddingBottom = 0; + self.frameWidth = (int) CGDisplayModeGetPixelWidth(mode); + self.frameHeight = (int) CGDisplayModeGetPixelHeight(mode); self.minFrameDuration = CMTimeMake(1, frameRate); self.session = [[AVCaptureSession alloc] init]; self.videoOutputs = [[NSMapTable alloc] init]; @@ -77,48 +83,8 @@ - (void)dealloc { } - (void)setFrameWidth:(int)frameWidth frameHeight:(int)frameHeight { - CGImageRef screenshot = CGDisplayCreateImage(self.displayID); - self.frameWidth = frameWidth; self.frameHeight = frameHeight; - - double screenRatio = (double) CGImageGetWidth(screenshot) / (double) CGImageGetHeight(screenshot); - double streamRatio = (double) frameWidth / (double) frameHeight; - - if (screenRatio < streamRatio) { - int padding = frameWidth - (frameHeight * screenRatio); - self.paddingLeft = padding / 2; - self.paddingRight = padding - self.paddingLeft; - self.paddingTop = 0; - self.paddingBottom = 0; - } - else { - int padding = frameHeight - (frameWidth / screenRatio); - self.paddingLeft = 0; - self.paddingRight = 0; - self.paddingTop = padding / 2; - self.paddingBottom = padding - self.paddingTop; - } - - // XXX: if the streamed image is larger than the native resolution, we add a black box around - // the frame. Instead the frame should be resized entirely. - int delta_width = frameWidth - (CGImageGetWidth(screenshot) + self.paddingLeft + self.paddingRight); - if (delta_width > 0) { - int adjust_left = delta_width / 2; - int adjust_right = delta_width - adjust_left; - self.paddingLeft += adjust_left; - self.paddingRight += adjust_right; - } - - int delta_height = frameHeight - (CGImageGetHeight(screenshot) + self.paddingTop + self.paddingBottom); - if (delta_height > 0) { - int adjust_top = delta_height / 2; - int adjust_bottom = delta_height - adjust_top; - self.paddingTop += adjust_top; - self.paddingBottom += adjust_bottom; - } - - CFRelease(screenshot); } - (dispatch_semaphore_t)capture:(FrameCallbackBlock)frameCallback { @@ -128,11 +94,8 @@ - (dispatch_semaphore_t)capture:(FrameCallbackBlock)frameCallback { [videoOutput setVideoSettings:@{ (NSString *) kCVPixelBufferPixelFormatTypeKey: [NSNumber numberWithUnsignedInt:self.pixelFormat], (NSString *) kCVPixelBufferWidthKey: [NSNumber numberWithInt:self.frameWidth], - (NSString *) kCVPixelBufferExtendedPixelsRightKey: [NSNumber numberWithInt:self.paddingRight], - (NSString *) kCVPixelBufferExtendedPixelsLeftKey: [NSNumber numberWithInt:self.paddingLeft], - (NSString *) kCVPixelBufferExtendedPixelsTopKey: [NSNumber numberWithInt:self.paddingTop], - (NSString *) kCVPixelBufferExtendedPixelsBottomKey: [NSNumber numberWithInt:self.paddingBottom], - (NSString *) kCVPixelBufferHeightKey: [NSNumber numberWithInt:self.frameHeight] + (NSString *) kCVPixelBufferHeightKey: [NSNumber numberWithInt:self.frameHeight], + (NSString *) AVVideoScalingModeKey: AVVideoScalingModeResizeAspect, }]; dispatch_queue_attr_t qos = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, diff --git a/src/platform/macos/display.mm b/src/platform/macos/display.mm index 65f3c279ddc..dc7c7b68532 100644 --- a/src/platform/macos/display.mm +++ b/src/platform/macos/display.mm @@ -8,7 +8,7 @@ #include "src/platform/macos/nv12_zero_device.h" #include "src/config.h" -#include "src/main.h" +#include "src/logging.h" // Avoid conflict between AVFoundation and libavutil both defining AVMediaType #define AVMediaType AVMediaType_FFmpeg @@ -20,33 +20,19 @@ namespace platf { using namespace std::literals; - av_img_t::~av_img_t() { - if (pixel_buffer != NULL) { - CVPixelBufferUnlockBaseAddress(pixel_buffer, 0); - } - - if (sample_buffer != nullptr) { - CFRelease(sample_buffer); - } - - data = nullptr; - } - struct av_display_t: public display_t { - AVVideo *av_capture; - CGDirectDisplayID display_id; + AVVideo *av_capture {}; + CGDirectDisplayID display_id {}; - ~av_display_t() { + ~av_display_t() override { [av_capture release]; } capture_e capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override { auto signal = [av_capture capture:^(CMSampleBufferRef sampleBuffer) { - CFRetain(sampleBuffer); - - CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); - CVPixelBufferLockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly); + auto new_sample_buffer = std::make_shared(sampleBuffer); + auto new_pixel_buffer = std::make_shared(new_sample_buffer->buf); std::shared_ptr img_out; if (!pull_free_image_cb(img_out)) { @@ -56,25 +42,23 @@ } auto av_img = std::static_pointer_cast(img_out); - if (av_img->pixel_buffer != nullptr) - CVPixelBufferUnlockBaseAddress(av_img->pixel_buffer, 0); - - if (av_img->sample_buffer != nullptr) - CFRelease(av_img->sample_buffer); - - av_img->sample_buffer = sampleBuffer; - av_img->pixel_buffer = pixelBuffer; - img_out->data = (uint8_t *) CVPixelBufferGetBaseAddress(pixelBuffer); + auto old_data_retainer = std::make_shared( + av_img->sample_buffer, + av_img->pixel_buffer, + img_out->data); - size_t extraPixels[4]; - CVPixelBufferGetExtendedPixels(pixelBuffer, &extraPixels[0], &extraPixels[1], &extraPixels[2], &extraPixels[3]); + av_img->sample_buffer = new_sample_buffer; + av_img->pixel_buffer = new_pixel_buffer; + img_out->data = new_pixel_buffer->data(); - img_out->width = CVPixelBufferGetWidth(pixelBuffer) + extraPixels[0] + extraPixels[1]; - img_out->height = CVPixelBufferGetHeight(pixelBuffer) + extraPixels[2] + extraPixels[3]; - img_out->row_pitch = CVPixelBufferGetBytesPerRow(pixelBuffer); + img_out->width = (int) CVPixelBufferGetWidth(new_pixel_buffer->buf); + img_out->height = (int) CVPixelBufferGetHeight(new_pixel_buffer->buf); + img_out->row_pitch = (int) CVPixelBufferGetBytesPerRow(new_pixel_buffer->buf); img_out->pixel_pitch = img_out->row_pitch / img_out->width; - if (!push_captured_image_cb(std::move(img_out), false)) { + old_data_retainer = nullptr; + + if (!push_captured_image_cb(std::move(img_out), true)) { // got interrupt signal // returning false here stops capture backend return false; @@ -94,17 +78,17 @@ return std::make_shared(); } - std::shared_ptr - make_hwdevice(pix_fmt_e pix_fmt) override { + std::unique_ptr + make_avcodec_encode_device(pix_fmt_e pix_fmt) override { if (pix_fmt == pix_fmt_e::yuv420p) { av_capture.pixelFormat = kCVPixelFormatType_32BGRA; - return std::make_shared(); + return std::make_unique(); } - else if (pix_fmt == pix_fmt_e::nv12) { - auto device = std::make_shared(); + else if (pix_fmt == pix_fmt_e::nv12 || pix_fmt == pix_fmt_e::p010) { + auto device = std::make_unique(); - device->init(static_cast(av_capture), setResolution, setPixelFormat); + device->init(static_cast(av_capture), pix_fmt, setResolution, setPixelFormat); return device; } @@ -117,33 +101,27 @@ int dummy_img(img_t *img) override { auto signal = [av_capture capture:^(CMSampleBufferRef sampleBuffer) { - auto av_img = (av_img_t *) img; + auto new_sample_buffer = std::make_shared(sampleBuffer); + auto new_pixel_buffer = std::make_shared(new_sample_buffer->buf); - CFRetain(sampleBuffer); - - CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); - CVPixelBufferLockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly); - - // XXX: next_img->img should be moved to a smart pointer with - // the CFRelease as custom deallocator - if (av_img->pixel_buffer != nullptr) - CVPixelBufferUnlockBaseAddress(((av_img_t *) img)->pixel_buffer, 0); - - if (av_img->sample_buffer != nullptr) - CFRelease(av_img->sample_buffer); + auto av_img = (av_img_t *) img; - av_img->sample_buffer = sampleBuffer; - av_img->pixel_buffer = pixelBuffer; - img->data = (uint8_t *) CVPixelBufferGetBaseAddress(pixelBuffer); + auto old_data_retainer = std::make_shared( + av_img->sample_buffer, + av_img->pixel_buffer, + img->data); - size_t extraPixels[4]; - CVPixelBufferGetExtendedPixels(pixelBuffer, &extraPixels[0], &extraPixels[1], &extraPixels[2], &extraPixels[3]); + av_img->sample_buffer = new_sample_buffer; + av_img->pixel_buffer = new_pixel_buffer; + img->data = new_pixel_buffer->data(); - img->width = CVPixelBufferGetWidth(pixelBuffer) + extraPixels[0] + extraPixels[1]; - img->height = CVPixelBufferGetHeight(pixelBuffer) + extraPixels[2] + extraPixels[3]; - img->row_pitch = CVPixelBufferGetBytesPerRow(pixelBuffer); + img->width = (int) CVPixelBufferGetWidth(new_pixel_buffer->buf); + img->height = (int) CVPixelBufferGetHeight(new_pixel_buffer->buf); + img->row_pitch = (int) CVPixelBufferGetBytesPerRow(new_pixel_buffer->buf); img->pixel_pitch = img->row_pitch / img->width; + old_data_retainer = nullptr; + // returning false here stops capture backend return false; }]; @@ -173,25 +151,30 @@ std::shared_ptr display(platf::mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config) { - if (hwdevice_type != platf::mem_type_e::system) { + if (hwdevice_type != platf::mem_type_e::system && hwdevice_type != platf::mem_type_e::videotoolbox) { BOOST_LOG(error) << "Could not initialize display with the given hw device type."sv; return nullptr; } auto display = std::make_shared(); + // Default to main display display->display_id = CGMainDisplayID(); - if (!display_name.empty()) { - auto display_array = [AVVideo displayNames]; - - for (NSDictionary *item in display_array) { - NSString *name = item[@"name"]; - if (name.UTF8String == display_name) { - NSNumber *display_id = item[@"id"]; - display->display_id = [display_id unsignedIntValue]; - } + + // Print all displays available with it's name and id + auto display_array = [AVVideo displayNames]; + BOOST_LOG(info) << "Detecting displays"sv; + for (NSDictionary *item in display_array) { + NSNumber *display_id = item[@"id"]; + // We need show display's product name and corresponding display number given by user + NSString *name = item[@"displayName"]; + // We are using CGGetActiveDisplayList that only returns active displays so hardcoded connected value in log to true + BOOST_LOG(info) << "Detected display: "sv << name.UTF8String << " (id: "sv << [NSString stringWithFormat:@"%@", display_id].UTF8String << ") connected: true"sv; + if (!display_name.empty() && std::atoi(display_name.c_str()) == [display_id unsignedIntValue]) { + display->display_id = [display_id unsignedIntValue]; } } + BOOST_LOG(info) << "Configuring selected display ("sv << display->display_id << ") to stream"sv; display->av_capture = [[AVVideo alloc] initWithDisplay:display->display_id frameRate:config.framerate]; @@ -202,6 +185,9 @@ display->width = display->av_capture.frameWidth; display->height = display->av_capture.frameHeight; + // We also need set env_width and env_height for absolute mouse coordinates + display->env_width = display->width; + display->env_height = display->height; return display; } @@ -215,9 +201,19 @@ display_names.reserve([display_array count]); [display_array enumerateObjectsUsingBlock:^(NSDictionary *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) { NSString *name = obj[@"name"]; - display_names.push_back(name.UTF8String); + display_names.emplace_back(name.UTF8String); }]; return display_names; } + + /** + * @brief Returns if GPUs/drivers have changed since the last call to this function. + * @return `true` if a change has occurred or if it is unknown whether a change occurred. + */ + bool + needs_encoder_reenumeration() { + // We don't track GPU state, so we will always reenumerate. Fortunately, it is fast on macOS. + return true; + } } // namespace platf diff --git a/src/platform/macos/input.cpp b/src/platform/macos/input.cpp index 78079e3f386..11c6d228421 100644 --- a/src/platform/macos/input.cpp +++ b/src/platform/macos/input.cpp @@ -3,34 +3,36 @@ * @brief todo */ #import +#include #include -#include -#include "src/main.h" +#include "src/logging.h" #include "src/platform/common.h" #include "src/utility.h" -// Delay for a double click -// FIXME: we probably want to make this configurable -#define MULTICLICK_DELAY_NS 500000000 +/** + * @brief Delay for a double click, in milliseconds. + * @todo Make this configurable. + */ +constexpr std::chrono::milliseconds MULTICLICK_DELAY_MS(500); namespace platf { using namespace std::literals; struct macos_input_t { public: - CGDirectDisplayID display; - CGFloat displayScaling; - CGEventSourceRef source; + CGDirectDisplayID display {}; + CGFloat displayScaling {}; + CGEventSourceRef source {}; // keyboard related stuff - CGEventRef kb_event; - CGEventFlags kb_flags; + CGEventRef kb_event {}; + CGEventFlags kb_flags {}; // mouse related stuff - CGEventRef mouse_event; // mouse event source - bool mouse_down[3]; // mouse button status - uint64_t last_mouse_event[3][2]; // timestamp of last mouse events + CGEventRef mouse_event {}; // mouse event source + bool mouse_down[3] {}; // mouse button status + std::chrono::steady_clock::steady_clock::time_point last_mouse_event[3][2]; // timestamp of last mouse events }; // A struct to hold a Windows keycode to Mac virtual keycode mapping. @@ -219,7 +221,7 @@ const KeyCodeMap kKeyCodesMap[] = { int keysym(int keycode) { - KeyCodeMap key_map; + KeyCodeMap key_map {}; key_map.win_keycode = keycode; const KeyCodeMap *temp_map = std::lower_bound( @@ -288,8 +290,16 @@ const KeyCodeMap kKeyCodesMap[] = { BOOST_LOG(info) << "unicode: Unicode input not yet implemented for MacOS."sv; } + /** + * @brief Creates a new virtual gamepad. + * @param input The input context. + * @param id The gamepad ID. + * @param metadata Controller metadata from client (empty if none provided). + * @param feedback_queue The queue for posting messages back to the client. + * @return 0 on success. + */ int - alloc_gamepad(input_t &input, int nr, rumble_queue_t rumble_queue) { + alloc_gamepad(input_t &input, const gamepad_id_t &id, const gamepad_arrival_t &metadata, feedback_queue_t feedback_queue) { BOOST_LOG(info) << "alloc_gamepad: Gamepad not yet implemented for MacOS."sv; return -1; } @@ -318,15 +328,12 @@ const KeyCodeMap kKeyCodesMap[] = { auto display = macos_input->display; auto event = macos_input->mouse_event; - if (location.x < 0) - location.x = 0; - if (location.x >= CGDisplayPixelsWide(display)) - location.x = CGDisplayPixelsWide(display) - 1; + // get display bounds for current display + CGRect display_bounds = CGDisplayBounds(display); - if (location.y < 0) - location.y = 0; - if (location.y >= CGDisplayPixelsHigh(display)) - location.y = CGDisplayPixelsHigh(display) - 1; + // limit mouse to current display bounds + location.x = std::clamp(location.x, display_bounds.origin.x, display_bounds.origin.x + display_bounds.size.width - 1); + location.y = std::clamp(location.y, display_bounds.origin.y, display_bounds.origin.y + display_bounds.size.height - 1); CGEventSetType(event, type); CGEventSetLocation(event, location); @@ -369,24 +376,18 @@ const KeyCodeMap kKeyCodesMap[] = { void abs_mouse(input_t &input, const touch_port_t &touch_port, float x, float y) { - auto scaling = ((macos_input_t *) input.get())->displayScaling; + auto macos_input = static_cast(input.get()); + auto scaling = macos_input->displayScaling; + auto display = macos_input->display; CGPoint location = CGPointMake(x * scaling, y * scaling); - + CGRect display_bounds = CGDisplayBounds(display); + // in order to get the correct mouse location for capturing display , we need to add the display bounds to the location + location.x += display_bounds.origin.x; + location.y += display_bounds.origin.y; post_mouse(input, kCGMouseButtonLeft, event_type_mouse(input), location, 0); } - uint64_t - time_diff(uint64_t start) { - uint64_t elapsed; - Nanoseconds elapsedNano; - - elapsed = mach_absolute_time() - start; - elapsedNano = AbsoluteToNanoseconds(*(AbsoluteTime *) &elapsed); - - return *(uint64_t *) &elapsedNano; - } - void button_mouse(input_t &input, int button, bool release) { CGMouseButton mac_button; @@ -414,21 +415,22 @@ const KeyCodeMap kKeyCodesMap[] = { mouse->mouse_down[mac_button] = !release; - // if the last mouse down was less than MULTICLICK_DELAY_NS, we send a double click event - if (time_diff(mouse->last_mouse_event[mac_button][release]) < MULTICLICK_DELAY_NS) { + // if the last mouse down was less than MULTICLICK_DELAY_MS, we send a double click event + auto now = std::chrono::steady_clock::now(); + if (now < mouse->last_mouse_event[mac_button][release] + MULTICLICK_DELAY_MS) { post_mouse(input, mac_button, event, get_mouse_loc(input), 2); } else { post_mouse(input, mac_button, event, get_mouse_loc(input), 1); } - mouse->last_mouse_event[mac_button][release] = mach_absolute_time(); + mouse->last_mouse_event[mac_button][release] = now; } void scroll(input_t &input, int high_res_distance) { CGEventRef upEvent = CGEventCreateScrollWheelEvent( - NULL, + nullptr, kCGScrollEventUnitLine, 2, high_res_distance > 0 ? 1 : -1, high_res_distance); CGEventPost(kCGHIDEventTap, upEvent); @@ -440,15 +442,97 @@ const KeyCodeMap kKeyCodesMap[] = { // Unimplemented } + /** + * @brief Allocates a context to store per-client input data. + * @param input The global input context. + * @return A unique pointer to a per-client input data context. + */ + std::unique_ptr + allocate_client_input_context(input_t &input) { + // Unused + return nullptr; + } + + /** + * @brief Sends a touch event to the OS. + * @param input The client-specific input context. + * @param touch_port The current viewport for translating to screen coordinates. + * @param touch The touch event. + */ + void + touch(client_input_t *input, const touch_port_t &touch_port, const touch_input_t &touch) { + // Unimplemented feature - platform_caps::pen_touch + } + + /** + * @brief Sends a pen event to the OS. + * @param input The client-specific input context. + * @param touch_port The current viewport for translating to screen coordinates. + * @param pen The pen event. + */ + void + pen(client_input_t *input, const touch_port_t &touch_port, const pen_input_t &pen) { + // Unimplemented feature - platform_caps::pen_touch + } + + /** + * @brief Sends a gamepad touch event to the OS. + * @param input The global input context. + * @param touch The touch event. + */ + void + gamepad_touch(input_t &input, const gamepad_touch_t &touch) { + // Unimplemented feature - platform_caps::controller_touch + } + + /** + * @brief Sends a gamepad motion event to the OS. + * @param input The global input context. + * @param motion The motion event. + */ + void + gamepad_motion(input_t &input, const gamepad_motion_t &motion) { + // Unimplemented + } + + /** + * @brief Sends a gamepad battery event to the OS. + * @param input The global input context. + * @param battery The battery event. + */ + void + gamepad_battery(input_t &input, const gamepad_battery_t &battery) { + // Unimplemented + } + input_t input() { input_t result { new macos_input_t() }; auto macos_input = (macos_input_t *) result.get(); - // If we don't use the main display in the future, this has to be adapted + // Default to main display macos_input->display = CGMainDisplayID(); + auto output_name = config::video.output_name; + // If output_name is set, try to find the display with that display id + if (!output_name.empty()) { + uint32_t max_display = 32; + uint32_t display_count; + CGDirectDisplayID displays[max_display]; + if (CGGetActiveDisplayList(max_display, displays, &display_count) != kCGErrorSuccess) { + BOOST_LOG(error) << "Unable to get active display list , error: "sv << std::endl; + } + else { + for (int i = 0; i < display_count; i++) { + CGDirectDisplayID display_id = displays[i]; + if (display_id == std::atoi(output_name.c_str())) { + macos_input->display = display_id; + } + } + } + } + // Input coordinates are based on the virtual resolution not the physical, so we need the scaling factor CGDisplayModeRef mode = CGDisplayCopyDisplayMode(macos_input->display); macos_input->displayScaling = ((CGFloat) CGDisplayPixelsWide(macos_input->display)) / ((CGFloat) CGDisplayModeGetPixelWidth(mode)); @@ -463,14 +547,8 @@ const KeyCodeMap kKeyCodesMap[] = { macos_input->mouse_down[0] = false; macos_input->mouse_down[1] = false; macos_input->mouse_down[2] = false; - macos_input->last_mouse_event[0][0] = 0; - macos_input->last_mouse_event[0][1] = 0; - macos_input->last_mouse_event[1][0] = 0; - macos_input->last_mouse_event[1][1] = 0; - macos_input->last_mouse_event[2][0] = 0; - macos_input->last_mouse_event[2][1] = 0; - BOOST_LOG(debug) << "Display "sv << macos_input->display << ", pixel dimention: " << CGDisplayPixelsWide(macos_input->display) << "x"sv << CGDisplayPixelsHigh(macos_input->display); + BOOST_LOG(debug) << "Display "sv << macos_input->display << ", pixel dimension: " << CGDisplayPixelsWide(macos_input->display) << "x"sv << CGDisplayPixelsHigh(macos_input->display); return result; } @@ -492,4 +570,13 @@ const KeyCodeMap kKeyCodesMap[] = { return gamepads; } + + /** + * @brief Returns the supported platform capabilities to advertise to the client. + * @return Capability flags. + */ + platform_caps::caps_t + get_capabilities() { + return 0; + } } // namespace platf diff --git a/src/platform/macos/microphone.mm b/src/platform/macos/microphone.mm index 854ca6faffe..836f134a482 100644 --- a/src/platform/macos/microphone.mm +++ b/src/platform/macos/microphone.mm @@ -6,15 +6,15 @@ #include "src/platform/macos/av_audio.h" #include "src/config.h" -#include "src/main.h" +#include "src/logging.h" namespace platf { using namespace std::literals; struct av_mic_t: public mic_t { - AVAudio *av_audio_capture; + AVAudio *av_audio_capture {}; - ~av_mic_t() { + ~av_mic_t() override { [av_audio_capture release]; } @@ -42,7 +42,7 @@ }; struct macos_audio_control_t: public audio_control_t { - AVCaptureDevice *audio_capture_device; + AVCaptureDevice *audio_capture_device {}; public: int diff --git a/src/platform/macos/misc.h b/src/platform/macos/misc.h index a6fb1df3244..ca74f0ea478 100644 --- a/src/platform/macos/misc.h +++ b/src/platform/macos/misc.h @@ -9,7 +9,7 @@ #include namespace dyn { - typedef void (*apiproc)(void); + typedef void (*apiproc)(); int load(void *handle, const std::vector> &funcs, bool strict = true); diff --git a/src/platform/macos/misc.mm b/src/platform/macos/misc.mm index fb2b41b2b39..20c2247e049 100644 --- a/src/platform/macos/misc.mm +++ b/src/platform/macos/misc.mm @@ -2,6 +2,12 @@ * @file src/platform/macos/misc.mm * @brief todo */ + +// Required for IPV6_PKTINFO with Darwin headers +#ifndef __APPLE_USE_RFC_3542 // NOLINT(bugprone-reserved-identifier) + #define __APPLE_USE_RFC_3542 1 +#endif + #include #include #include @@ -12,9 +18,11 @@ #include #include "misc.h" -#include "src/main.h" +#include "src/entry_handler.h" +#include "src/logging.h" #include "src/platform/common.h" +#include #include using namespace std::literals; @@ -46,7 +54,7 @@ // Xcode 12.2 and later, these functions are not weakly linked and will never // be null, and therefore generate this warning. Since we are weakly linking // when compiling with earlier Xcode versions, the check for null is - // necessary and so we ignore the warning. + // necessary, and so we ignore the warning. #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunguarded-availability-new" #pragma clang diagnostic ignored "-Wtautological-pointer-compare" @@ -134,7 +142,7 @@ std::string mac_address; if (getifaddrs(&ifap) == 0) { - for (ifaptr = ifap; ifaptr != NULL; ifaptr = (ifaptr)->ifa_next) { + for (ifaptr = ifap; ifaptr != nullptr; ifaptr = (ifaptr)->ifa_next) { if (!strcmp((ifaptr)->ifa_name, pos->ifa_name) && (((ifaptr)->ifa_addr)->sa_family == AF_LINK)) { ptr = (unsigned char *) LLADDR((struct sockaddr_dl *) (ifaptr)->ifa_addr); char buff[100]; @@ -148,7 +156,7 @@ freeifaddrs(ifap); - if (ifaptr != NULL) { + if (ifaptr != nullptr) { BOOST_LOG(verbose) << "Found MAC of "sv << pos->ifa_name << ": "sv << mac_address; return mac_address; } @@ -161,7 +169,7 @@ } bp::child - run_command(bool elevated, bool interactive, const std::string &cmd, boost::filesystem::path &working_dir, bp::environment &env, FILE *file, std::error_code &ec, bp::group *group) { + run_command(bool elevated, bool interactive, const std::string &cmd, boost::filesystem::path &working_dir, const bp::environment &env, FILE *file, std::error_code &ec, bp::group *group) { if (!group) { if (!file) { return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_out > bp::null, bp::std_err > bp::null, ec); @@ -245,18 +253,268 @@ lifetime::exit_sunshine(0, true); } + /** + * @brief Attempt to gracefully terminate a process group. + * @param native_handle The process group ID. + * @return true if termination was successfully requested. + */ + bool + request_process_group_exit(std::uintptr_t native_handle) { + if (killpg((pid_t) native_handle, SIGTERM) == 0 || errno == ESRCH) { + BOOST_LOG(debug) << "Successfully sent SIGTERM to process group: "sv << native_handle; + return true; + } + else { + BOOST_LOG(warning) << "Unable to send SIGTERM to process group ["sv << native_handle << "]: "sv << errno; + return false; + } + } + + /** + * @brief Checks if a process group still has running children. + * @param native_handle The process group ID. + * @return true if processes are still running. + */ + bool + process_group_running(std::uintptr_t native_handle) { + return waitpid(-((pid_t) native_handle), nullptr, WNOHANG) >= 0; + } + + struct sockaddr_in + to_sockaddr(boost::asio::ip::address_v4 address, uint16_t port) { + struct sockaddr_in saddr_v4 = {}; + + saddr_v4.sin_family = AF_INET; + saddr_v4.sin_port = htons(port); + + auto addr_bytes = address.to_bytes(); + memcpy(&saddr_v4.sin_addr, addr_bytes.data(), sizeof(saddr_v4.sin_addr)); + + return saddr_v4; + } + + struct sockaddr_in6 + to_sockaddr(boost::asio::ip::address_v6 address, uint16_t port) { + struct sockaddr_in6 saddr_v6 = {}; + + saddr_v6.sin6_family = AF_INET6; + saddr_v6.sin6_port = htons(port); + saddr_v6.sin6_scope_id = address.scope_id(); + + auto addr_bytes = address.to_bytes(); + memcpy(&saddr_v6.sin6_addr, addr_bytes.data(), sizeof(saddr_v6.sin6_addr)); + + return saddr_v6; + } + bool send_batch(batched_send_info_t &send_info) { // Fall back to unbatched send calls return false; } + bool + send(send_info_t &send_info) { + auto sockfd = (int) send_info.native_socket; + struct msghdr msg = {}; + + // Convert the target address into a sockaddr + struct sockaddr_in taddr_v4 = {}; + struct sockaddr_in6 taddr_v6 = {}; + if (send_info.target_address.is_v6()) { + taddr_v6 = to_sockaddr(send_info.target_address.to_v6(), send_info.target_port); + + msg.msg_name = (struct sockaddr *) &taddr_v6; + msg.msg_namelen = sizeof(taddr_v6); + } + else { + taddr_v4 = to_sockaddr(send_info.target_address.to_v4(), send_info.target_port); + + msg.msg_name = (struct sockaddr *) &taddr_v4; + msg.msg_namelen = sizeof(taddr_v4); + } + + union { + char buf[std::max(CMSG_SPACE(sizeof(struct in_pktinfo)), CMSG_SPACE(sizeof(struct in6_pktinfo)))]; + struct cmsghdr alignment; + } cmbuf {}; + socklen_t cmbuflen = 0; + + msg.msg_control = cmbuf.buf; + msg.msg_controllen = sizeof(cmbuf.buf); + + auto pktinfo_cm = CMSG_FIRSTHDR(&msg); + if (send_info.source_address.is_v6()) { + struct in6_pktinfo pktInfo {}; + + struct sockaddr_in6 saddr_v6 = to_sockaddr(send_info.source_address.to_v6(), 0); + pktInfo.ipi6_addr = saddr_v6.sin6_addr; + pktInfo.ipi6_ifindex = 0; + + cmbuflen += CMSG_SPACE(sizeof(pktInfo)); + + pktinfo_cm->cmsg_level = IPPROTO_IPV6; + pktinfo_cm->cmsg_type = IPV6_PKTINFO; + pktinfo_cm->cmsg_len = CMSG_LEN(sizeof(pktInfo)); + memcpy(CMSG_DATA(pktinfo_cm), &pktInfo, sizeof(pktInfo)); + } + else { + struct in_pktinfo pktInfo {}; + + struct sockaddr_in saddr_v4 = to_sockaddr(send_info.source_address.to_v4(), 0); + pktInfo.ipi_spec_dst = saddr_v4.sin_addr; + pktInfo.ipi_ifindex = 0; + + cmbuflen += CMSG_SPACE(sizeof(pktInfo)); + + pktinfo_cm->cmsg_level = IPPROTO_IP; + pktinfo_cm->cmsg_type = IP_PKTINFO; + pktinfo_cm->cmsg_len = CMSG_LEN(sizeof(pktInfo)); + memcpy(CMSG_DATA(pktinfo_cm), &pktInfo, sizeof(pktInfo)); + } + + struct iovec iov = {}; + iov.iov_base = (void *) send_info.buffer; + iov.iov_len = send_info.size; + + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + + msg.msg_controllen = cmbuflen; + + auto bytes_sent = sendmsg(sockfd, &msg, 0); + + // If there's no send buffer space, wait for some to be available + while (bytes_sent < 0 && errno == EAGAIN) { + struct pollfd pfd; + + pfd.fd = sockfd; + pfd.events = POLLOUT; + + if (poll(&pfd, 1, -1) != 1) { + BOOST_LOG(warning) << "poll() failed: "sv << errno; + break; + } + + // Try to send again + bytes_sent = sendmsg(sockfd, &msg, 0); + } + + if (bytes_sent < 0) { + BOOST_LOG(warning) << "sendmsg() failed: "sv << errno; + return false; + } + + return true; + } + + // We can't track QoS state separately for each destination on this OS, + // so we keep a ref count to only disable QoS options when all clients + // are disconnected. + static std::atomic qos_ref_count = 0; + + class qos_t: public deinit_t { + public: + qos_t(int sockfd, std::vector> options): + sockfd(sockfd), options(options) { + qos_ref_count++; + } + + virtual ~qos_t() { + if (--qos_ref_count == 0) { + for (const auto &tuple : options) { + auto reset_val = std::get<2>(tuple); + if (setsockopt(sockfd, std::get<0>(tuple), std::get<1>(tuple), &reset_val, sizeof(reset_val)) < 0) { + BOOST_LOG(warning) << "Failed to reset option: "sv << errno; + } + } + } + } + + private: + int sockfd; + std::vector> options; + }; + + /** + * @brief Enables QoS on the given socket for traffic to the specified destination. + * @param native_socket The native socket handle. + * @param address The destination address for traffic sent on this socket. + * @param port The destination port for traffic sent on this socket. + * @param data_type The type of traffic sent on this socket. + * @param dscp_tagging Specifies whether to enable DSCP tagging on outgoing traffic. + */ std::unique_ptr - enable_socket_qos(uintptr_t native_socket, boost::asio::ip::address &address, uint16_t port, qos_data_type_e data_type) { - // Unimplemented - // - // NB: When implementing, remember to consider that some routes can drop DSCP-tagged packets completely! - return nullptr; + enable_socket_qos(uintptr_t native_socket, boost::asio::ip::address &address, uint16_t port, qos_data_type_e data_type, bool dscp_tagging) { + int sockfd = (int) native_socket; + std::vector> reset_options; + + // We can use SO_NET_SERVICE_TYPE to set link-layer prioritization without DSCP tagging + int service_type = 0; + switch (data_type) { + case qos_data_type_e::video: + service_type = NET_SERVICE_TYPE_VI; + break; + case qos_data_type_e::audio: + service_type = NET_SERVICE_TYPE_VO; + break; + default: + BOOST_LOG(error) << "Unknown traffic type: "sv << (int) data_type; + break; + } + + if (service_type) { + if (setsockopt(sockfd, SOL_SOCKET, SO_NET_SERVICE_TYPE, &service_type, sizeof(service_type)) == 0) { + // Reset SO_NET_SERVICE_TYPE to best-effort when QoS is disabled + reset_options.emplace_back(std::make_tuple(SOL_SOCKET, SO_NET_SERVICE_TYPE, NET_SERVICE_TYPE_BE)); + } + else { + BOOST_LOG(error) << "Failed to set SO_NET_SERVICE_TYPE: "sv << errno; + } + } + + if (dscp_tagging) { + int level; + int option; + if (address.is_v6()) { + level = IPPROTO_IPV6; + option = IPV6_TCLASS; + } + else { + level = IPPROTO_IP; + option = IP_TOS; + } + + // The specific DSCP values here are chosen to be consistent with Windows, + // except that we use CS6 instead of CS7 for audio traffic. + int dscp = 0; + switch (data_type) { + case qos_data_type_e::video: + dscp = 40; + break; + case qos_data_type_e::audio: + dscp = 48; + break; + default: + BOOST_LOG(error) << "Unknown traffic type: "sv << (int) data_type; + break; + } + + if (dscp) { + // Shift to put the DSCP value in the correct position in the TOS field + dscp <<= 2; + + if (setsockopt(sockfd, level, option, &dscp, sizeof(dscp)) == 0) { + // Reset TOS to -1 when QoS is disabled + reset_options.emplace_back(std::make_tuple(level, option, -1)); + } + else { + BOOST_LOG(error) << "Failed to set TOS/TCLASS: "sv << errno; + } + } + } + + return std::make_unique(sockfd, reset_options); } } // namespace platf diff --git a/src/platform/macos/nv12_zero_device.cpp b/src/platform/macos/nv12_zero_device.cpp index 21046be3054..ab0c478eb74 100644 --- a/src/platform/macos/nv12_zero_device.cpp +++ b/src/platform/macos/nv12_zero_device.cpp @@ -2,8 +2,10 @@ * @file src/platform/macos/nv12_zero_device.cpp * @brief todo */ -#include "src/platform/macos/nv12_zero_device.h" +#include + #include "src/platform/macos/av_img_t.h" +#include "src/platform/macos/nv12_zero_device.h" #include "src/video.h" @@ -18,45 +20,31 @@ namespace platf { av_frame_free(&frame); } + void + free_buffer(void *opaque, uint8_t *data) { + CVPixelBufferRelease((CVPixelBufferRef) data); + } + util::safe_ptr av_frame; int nv12_zero_device::convert(platf::img_t &img) { - av_frame_make_writable(av_frame.get()); - - av_img_t *av_img = (av_img_t *) &img; - - size_t left_pad, right_pad, top_pad, bottom_pad; - CVPixelBufferGetExtendedPixels(av_img->pixel_buffer, &left_pad, &right_pad, &top_pad, &bottom_pad); + auto *av_img = (av_img_t *) &img; - const uint8_t *data = (const uint8_t *) CVPixelBufferGetBaseAddressOfPlane(av_img->pixel_buffer, 0) - left_pad - (top_pad * img.width); + // Release any existing CVPixelBuffer previously retained for encoding + av_buffer_unref(&av_frame->buf[0]); - int result = av_image_fill_arrays(av_frame->data, av_frame->linesize, data, (AVPixelFormat) av_frame->format, img.width, img.height, 32); - - // We will create the black bars for the padding top/bottom or left/right here in very cheap way. - // The luminance is 0, therefore, we simply need to set the chroma values to 128 for each pixel - // for black bars (instead of green with chroma 0). However, this only works 100% correct, when - // the resolution is devisable by 32. This could be improved by calculating the chroma values for - // the outer content pixels, which should introduce only a minor performance hit. + // Attach an AVBufferRef to this frame which will retain ownership of the CVPixelBuffer + // until av_buffer_unref() is called (above) or the frame is freed with av_frame_free(). // - // XXX: Improve the algorithm to take into account the outer pixels - - size_t uv_plane_height = CVPixelBufferGetHeightOfPlane(av_img->pixel_buffer, 1); + // The presence of the AVBufferRef allows FFmpeg to simply add a reference to the buffer + // rather than having to perform a deep copy of the data buffers in avcodec_send_frame(). + av_frame->buf[0] = av_buffer_create((uint8_t *) CFRetain(av_img->pixel_buffer->buf), 0, free_buffer, nullptr, 0); - if (left_pad || right_pad) { - for (int l = 0; l < uv_plane_height + (top_pad / 2); l++) { - int line = l * av_frame->linesize[1]; - memset((void *) &av_frame->data[1][line], 128, (size_t) left_pad); - memset((void *) &av_frame->data[1][line + img.width - right_pad], 128, right_pad); - } - } + // Place a CVPixelBufferRef at data[3] as required by AV_PIX_FMT_VIDEOTOOLBOX + av_frame->data[3] = (uint8_t *) av_img->pixel_buffer->buf; - if (top_pad || bottom_pad) { - memset((void *) &av_frame->data[1][0], 128, (top_pad / 2) * av_frame->linesize[1]); - memset((void *) &av_frame->data[1][((top_pad / 2) + uv_plane_height) * av_frame->linesize[1]], 128, bottom_pad / 2 * av_frame->linesize[1]); - } - - return result > 0 ? 0 : -1; + return 0; } int @@ -70,19 +58,17 @@ namespace platf { return 0; } - void - nv12_zero_device::set_colorspace(std::uint32_t colorspace, std::uint32_t color_range) { - } - int - nv12_zero_device::init(void *display, resolution_fn_t resolution_fn, pixel_format_fn_t pixel_format_fn) { - pixel_format_fn(display, '420v'); + nv12_zero_device::init(void *display, pix_fmt_e pix_fmt, resolution_fn_t resolution_fn, const pixel_format_fn_t &pixel_format_fn) { + pixel_format_fn(display, pix_fmt == pix_fmt_e::nv12 ? + kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange : + kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange); this->display = display; - this->resolution_fn = resolution_fn; + this->resolution_fn = std::move(resolution_fn); - // we never use this pointer but it's existence is checked/used - // by the platform independed code + // we never use this pointer, but its existence is checked/used + // by the platform independent code data = this; return 0; diff --git a/src/platform/macos/nv12_zero_device.h b/src/platform/macos/nv12_zero_device.h index 059896ea156..e0f3230f924 100644 --- a/src/platform/macos/nv12_zero_device.h +++ b/src/platform/macos/nv12_zero_device.h @@ -6,9 +6,13 @@ #include "src/platform/common.h" +struct AVFrame; + namespace platf { + void + free_frame(AVFrame *frame); - class nv12_zero_device: public hwdevice_t { + class nv12_zero_device: public avcodec_encode_device_t { // display holds a pointer to an av_video object. Since the namespaces of AVFoundation // and FFMPEG collide, we need this opaque pointer and cannot use the definition void *display; @@ -21,14 +25,15 @@ namespace platf { using pixel_format_fn_t = std::function; int - init(void *display, resolution_fn_t resolution_fn, pixel_format_fn_t pixel_format_fn); + init(void *display, pix_fmt_e pix_fmt, resolution_fn_t resolution_fn, const pixel_format_fn_t &pixel_format_fn); int - convert(img_t &img); + convert(img_t &img) override; int - set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx); - void - set_colorspace(std::uint32_t colorspace, std::uint32_t color_range); + set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) override; + + private: + util::safe_ptr av_frame; }; } // namespace platf diff --git a/src/platform/macos/publish.cpp b/src/platform/macos/publish.cpp index 4cb80b8dbdb..eb8823e5020 100644 --- a/src/platform/macos/publish.cpp +++ b/src/platform/macos/publish.cpp @@ -7,7 +7,8 @@ #include #include "misc.h" -#include "src/main.h" +#include "src/logging.h" +#include "src/network.h" #include "src/nvhttp.h" #include "src/platform/common.h" #include "src/utility.h" @@ -55,10 +56,10 @@ namespace avahi { ERR_NOT_FOUND = -30, /**< Not found */ ERR_INVALID_CONFIG = -31, /**< Configuration error */ - ERR_VERSION_MISMATCH = -32, /**< Verson mismatch */ + ERR_VERSION_MISMATCH = -32, /**< Version mismatch */ ERR_INVALID_SERVICE_SUBTYPE = -33, /**< Invalid service subtype */ ERR_INVALID_PACKET = -34, /**< Invalid packet */ - ERR_INVALID_DNS_ERROR = -35, /**< Invlaid DNS return code */ + ERR_INVALID_DNS_ERROR = -35, /**< Invalid DNS return code */ ERR_DNS_FORMERR = -36, /**< DNS Error: Form error */ ERR_DNS_SERVFAIL = -37, /**< DNS Error: Server Failure */ ERR_DNS_NXDOMAIN = -38, /**< DNS Error: No such domain */ @@ -107,7 +108,7 @@ namespace avahi { }; enum EntryGroupState { - ENTRY_GROUP_UNCOMMITED, /**< The group has not yet been commited, the user must still call avahi_entry_group_commit() */ + ENTRY_GROUP_UNCOMMITED, /**< The group has not yet been committed, the user must still call avahi_entry_group_commit() */ ENTRY_GROUP_REGISTERING, /**< The entries of the group are currently being registered */ ENTRY_GROUP_ESTABLISHED, /**< The entries have successfully been established */ ENTRY_GROUP_COLLISION, /**< A name collision for one of the entries in the group has been detected, the entries have been withdrawn */ @@ -348,7 +349,7 @@ namespace platf::publish { name.get(), SERVICE_TYPE, nullptr, nullptr, - map_port(nvhttp::PORT_HTTP), + net::map_port(nvhttp::PORT_HTTP), nullptr); if (ret < 0) { @@ -402,7 +403,7 @@ namespace platf::publish { public: std::thread poll_thread; - deinit_t(std::thread poll_thread): + explicit deinit_t(std::thread poll_thread): poll_thread { std::move(poll_thread) } {} ~deinit_t() override { diff --git a/src/platform/windows/PolicyConfig.h b/src/platform/windows/PolicyConfig.h index 21772227c19..087f85fa84f 100644 --- a/src/platform/windows/PolicyConfig.h +++ b/src/platform/windows/PolicyConfig.h @@ -3,7 +3,7 @@ * @brief Undocumented COM-interface IPolicyConfig. * @details Use for setting default audio render endpoint. * @author EreTIk - * @see http://eretik.omegahg.com/ + * @see https://kitere.github.io/ */ #pragma once diff --git a/src/platform/windows/audio.cpp b/src/platform/windows/audio.cpp index eac55e89e22..3e9267b32aa 100644 --- a/src/platform/windows/audio.cpp +++ b/src/platform/windows/audio.cpp @@ -2,24 +2,23 @@ * @file src/platform/windows/audio.cpp * @brief todo */ +#define INITGUID #include #include #include -#include - #include #include -#define INITGUID -#include -#undef INITGUID +#include #include "src/config.h" -#include "src/main.h" +#include "src/logging.h" #include "src/platform/common.h" +#include "misc.h" + // Must be the last included file // clang-format off #include "PolicyConfig.h" @@ -29,11 +28,6 @@ DEFINE_PROPERTYKEY(PKEY_Device_DeviceDesc, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x2 DEFINE_PROPERTYKEY(PKEY_Device_FriendlyName, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 14); // DEVPROP_TYPE_STRING DEFINE_PROPERTYKEY(PKEY_DeviceInterface_FriendlyName, 0x026e516e, 0xb814, 0x414b, 0x83, 0xcd, 0x85, 0x6d, 0x6f, 0xef, 0x48, 0x22, 2); -const CLSID CLSID_MMDeviceEnumerator = __uuidof(MMDeviceEnumerator); -const IID IID_IMMDeviceEnumerator = __uuidof(IMMDeviceEnumerator); -const IID IID_IAudioClient = __uuidof(IAudioClient); -const IID IID_IAudioCaptureClient = __uuidof(IAudioCaptureClient); - #if defined(__x86_64) || defined(_M_AMD64) #define STEAM_DRIVER_SUBDIR L"x64" #elif defined(__i386) || defined(_M_IX86) @@ -95,7 +89,6 @@ namespace platf::audio { PROPVARIANT prop; }; - static std::wstring_convert, wchar_t> converter; struct format_t { enum type_e : int { none, @@ -162,7 +155,7 @@ namespace platf::audio { wave_format.Format.wBitsPerSample = 16; wave_format.Format.nBlockAlign = wave_format.Format.nChannels * wave_format.Format.wBitsPerSample / 8; wave_format.Format.nAvgBytesPerSec = wave_format.Format.nSamplesPerSec * wave_format.Format.nBlockAlign; - wave_format.Format.cbSize = sizeof(wave_format); + wave_format.Format.cbSize = sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX); wave_format.Samples.wValidBitsPerSample = 16; wave_format.dwChannelMask = format.channel_mask; @@ -457,6 +450,14 @@ namespace platf::audio { return -1; } + { + DWORD task_index = 0; + mmcss_task_handle = AvSetMmThreadCharacteristics("Pro Audio", &task_index); + if (!mmcss_task_handle) { + BOOST_LOG(error) << "Couldn't associate audio capture thread with Pro Audio MMCSS task [0x" << util::hex(GetLastError()).to_string_view() << ']'; + } + } + status = audio_client->Start(); if (FAILED(status)) { BOOST_LOG(error) << "Couldn't start recording [0x"sv << util::hex(status).to_string_view() << ']'; @@ -475,6 +476,10 @@ namespace platf::audio { if (audio_client) { audio_client->Stop(); } + + if (mmcss_task_handle) { + AvRevertMmThreadCharacteristics(mmcss_task_handle); + } } private: @@ -537,9 +542,17 @@ namespace platf::audio { return capture_e::error; } + if (buffer_flags & AUDCLNT_BUFFERFLAGS_DATA_DISCONTINUITY) { + BOOST_LOG(debug) << "Audio capture signaled buffer discontinuity"; + } + sample_aligned.uninitialized = std::end(sample_buf) - sample_buf_pos; auto n = std::min(sample_aligned.uninitialized, block_aligned.audio_sample_size * channels); + if (n < block_aligned.audio_sample_size * channels) { + BOOST_LOG(warning) << "Audio capture buffer overflow"; + } + if (buffer_flags & AUDCLNT_BUFFERFLAGS_SILENT) { std::fill_n(sample_buf_pos, n, 0); } @@ -579,6 +592,8 @@ namespace platf::audio { util::buffer_t sample_buf; std::int16_t *sample_buf_pos; int channels; + + HANDLE mmcss_task_handle = NULL; }; class audio_control_t: public ::platf::audio_control_t { @@ -597,7 +612,7 @@ namespace platf::audio { audio::wstring_t wstring; device->GetId(&wstring); - sink.host = converter.to_bytes(wstring.get()); + sink.host = to_utf8(wstring.get()); collection_t collection; auto status = device_enum->EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE, &collection); @@ -611,7 +626,7 @@ namespace platf::audio { collection->GetCount(&count); // If the sink isn't a device name, we'll assume it's a device ID - auto virtual_device_id = find_device_id_by_name(config::audio.virtual_sink).value_or(converter.from_bytes(config::audio.virtual_sink)); + auto virtual_device_id = find_device_id_by_name(config::audio.virtual_sink).value_or(from_utf8(config::audio.virtual_sink)); auto virtual_device_found = false; for (auto x = 0; x < count; ++x) { @@ -658,7 +673,7 @@ namespace platf::audio { } if (virtual_device_found) { - auto name_suffix = converter.to_bytes(virtual_device_id); + auto name_suffix = to_utf8(virtual_device_id); sink.null = std::make_optional(sink_t::null_t { "virtual-"s.append(formats[format_t::stereo - 1].name) + name_suffix, "virtual-"s.append(formats[format_t::surr51 - 1].name) + name_suffix, @@ -733,7 +748,7 @@ namespace platf::audio { auto sink_info = get_sink_info(sink); // If the sink isn't a device name, we'll assume it's a device ID - auto wstring_device_id = find_device_id_by_name(sink).value_or(converter.from_bytes(sink_info.second.data())); + auto wstring_device_id = find_device_id_by_name(sink).value_or(from_utf8(sink_info.second.data())); if (sink_info.first == format_t::none) { // wstring_device_id does not contain virtual-(format name) @@ -823,7 +838,7 @@ namespace platf::audio { UINT count; collection->GetCount(&count); - auto wstring_name = converter.from_bytes(name.data()); + auto wstring_name = from_utf8(name.data()); for (auto x = 0; x < count; ++x) { audio::device_t device; diff --git a/src/platform/windows/display.h b/src/platform/windows/display.h index 2496cd3f55e..1a3e7b5817f 100644 --- a/src/platform/windows/display.h +++ b/src/platform/windows/display.h @@ -11,8 +11,12 @@ #include #include +#include +#include + #include "src/platform/common.h" #include "src/utility.h" +#include "src/video.h" namespace platf::dxgi { extern const char *format_str[]; @@ -80,45 +84,76 @@ namespace platf::dxgi { public: gpu_cursor_t(): cursor_view { 0, 0, 0, 0, 0.0f, 1.0f } {}; - void - set_pos(LONG rel_x, LONG rel_y, bool visible) { - cursor_view.TopLeftX = rel_x; - cursor_view.TopLeftY = rel_y; + void + set_pos(LONG topleft_x, LONG topleft_y, LONG display_width, LONG display_height, DXGI_MODE_ROTATION display_rotation, bool visible) { + this->topleft_x = topleft_x; + this->topleft_y = topleft_y; + this->display_width = display_width; + this->display_height = display_height; + this->display_rotation = display_rotation; this->visible = visible; + update_viewport(); } void - set_texture(LONG width, LONG height, texture2d_t &&texture) { - cursor_view.Width = width; - cursor_view.Height = height; - + set_texture(LONG texture_width, LONG texture_height, texture2d_t &&texture) { this->texture = std::move(texture); + this->texture_width = texture_width; + this->texture_height = texture_height; + update_viewport(); + } + + void + update_viewport() { + switch (display_rotation) { + case DXGI_MODE_ROTATION_UNSPECIFIED: + case DXGI_MODE_ROTATION_IDENTITY: + cursor_view.TopLeftX = topleft_x; + cursor_view.TopLeftY = topleft_y; + cursor_view.Width = texture_width; + cursor_view.Height = texture_height; + break; + + case DXGI_MODE_ROTATION_ROTATE90: + cursor_view.TopLeftX = topleft_y; + cursor_view.TopLeftY = display_width - texture_width - topleft_x; + cursor_view.Width = texture_height; + cursor_view.Height = texture_width; + break; + + case DXGI_MODE_ROTATION_ROTATE180: + cursor_view.TopLeftX = display_width - texture_width - topleft_x; + cursor_view.TopLeftY = display_height - texture_height - topleft_y; + cursor_view.Width = texture_width; + cursor_view.Height = texture_height; + break; + + case DXGI_MODE_ROTATION_ROTATE270: + cursor_view.TopLeftX = display_height - texture_height - topleft_y; + cursor_view.TopLeftY = topleft_x; + cursor_view.Width = texture_height; + cursor_view.Height = texture_width; + break; + } } texture2d_t texture; - shader_res_t input_res; + LONG texture_width; + LONG texture_height; - D3D11_VIEWPORT cursor_view; + LONG topleft_x; + LONG topleft_y; - bool visible; - }; + LONG display_width; + LONG display_height; + DXGI_MODE_ROTATION display_rotation; - class duplication_t { - public: - dup_t dup; - bool has_frame {}; - bool use_dwmflush {}; - std::chrono::steady_clock::time_point last_protected_content_warning_time {}; + shader_res_t input_res; - capture_e - next_frame(DXGI_OUTDUPL_FRAME_INFO &frame_info, std::chrono::milliseconds timeout, resource_t::pointer *res_p); - capture_e - reset(dup_t::pointer dup_p = dup_t::pointer()); - capture_e - release_frame(); + D3D11_VIEWPORT cursor_view; - ~duplication_t(); + bool visible; }; class display_base_t: public display_t { @@ -126,21 +161,31 @@ namespace platf::dxgi { int init(const ::video::config_t &config, const std::string &display_name); + void + high_precision_sleep(std::chrono::nanoseconds duration); + capture_e capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override; - std::chrono::nanoseconds delay; - factory1_t factory; adapter_t adapter; output_t output; device_t device; device_ctx_t device_ctx; - duplication_t dup; + DXGI_RATIONAL display_refresh_rate; + int display_refresh_rate_rounded; + + DXGI_MODE_ROTATION display_rotation = DXGI_MODE_ROTATION_UNSPECIFIED; + int width_before_rotation; + int height_before_rotation; + + int client_frame_rate; DXGI_FORMAT capture_format; D3D_FEATURE_LEVEL feature_level; + util::safe_ptr_v2, BOOL, CloseHandle> timer; + typedef enum _D3DKMT_SCHEDULINGPRIORITYCLASS { D3DKMT_SCHEDULINGPRIORITYCLASS_IDLE, D3DKMT_SCHEDULINGPRIORITYCLASS_BELOW_NORMAL, @@ -150,37 +195,76 @@ namespace platf::dxgi { D3DKMT_SCHEDULINGPRIORITYCLASS_REALTIME } D3DKMT_SCHEDULINGPRIORITYCLASS; - typedef NTSTATUS WINAPI (*PD3DKMTSetProcessSchedulingPriorityClass)(HANDLE, D3DKMT_SCHEDULINGPRIORITYCLASS); + typedef UINT D3DKMT_HANDLE; + + typedef struct _D3DKMT_OPENADAPTERFROMLUID { + LUID AdapterLuid; + D3DKMT_HANDLE hAdapter; + } D3DKMT_OPENADAPTERFROMLUID; + + typedef struct _D3DKMT_WDDM_2_7_CAPS { + union { + struct + { + UINT HwSchSupported : 1; + UINT HwSchEnabled : 1; + UINT HwSchEnabledByDefault : 1; + UINT IndependentVidPnVSyncControl : 1; + UINT Reserved : 28; + }; + UINT Value; + }; + } D3DKMT_WDDM_2_7_CAPS; + + typedef struct _D3DKMT_QUERYADAPTERINFO { + D3DKMT_HANDLE hAdapter; + UINT Type; + VOID *pPrivateDriverData; + UINT PrivateDriverDataSize; + } D3DKMT_QUERYADAPTERINFO; + + const UINT KMTQAITYPE_WDDM_2_7_CAPS = 70; + + typedef struct _D3DKMT_CLOSEADAPTER { + D3DKMT_HANDLE hAdapter; + } D3DKMT_CLOSEADAPTER; + + typedef NTSTATUS(WINAPI *PD3DKMTSetProcessSchedulingPriorityClass)(HANDLE, D3DKMT_SCHEDULINGPRIORITYCLASS); + typedef NTSTATUS(WINAPI *PD3DKMTOpenAdapterFromLuid)(D3DKMT_OPENADAPTERFROMLUID *); + typedef NTSTATUS(WINAPI *PD3DKMTQueryAdapterInfo)(D3DKMT_QUERYADAPTERINFO *); + typedef NTSTATUS(WINAPI *PD3DKMTCloseAdapter)(D3DKMT_CLOSEADAPTER *); virtual bool is_hdr() override; virtual bool get_hdr_metadata(SS_HDR_METADATA &metadata) override; + const char * + dxgi_format_to_string(DXGI_FORMAT format); + const char * + colorspace_to_string(DXGI_COLOR_SPACE_TYPE type); + virtual std::vector + get_supported_capture_formats() = 0; + protected: int get_pixel_pitch() { return (capture_format == DXGI_FORMAT_R16G16B16A16_FLOAT) ? 8 : 4; } - const char * - dxgi_format_to_string(DXGI_FORMAT format); - const char * - colorspace_to_string(DXGI_COLOR_SPACE_TYPE type); - virtual capture_e snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor_visible) = 0; + virtual capture_e + release_snapshot() = 0; virtual int complete_img(img_t *img, bool dummy) = 0; - virtual std::vector - get_supported_capture_formats() = 0; }; + /** + * Display component for devices that use software encoders. + */ class display_ram_t: public display_base_t { public: - virtual capture_e - snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor_visible) override; - std::shared_ptr alloc_img() override; int @@ -190,19 +274,18 @@ namespace platf::dxgi { std::vector get_supported_capture_formats() override; - int - init(const ::video::config_t &config, const std::string &display_name); + std::unique_ptr + make_avcodec_encode_device(pix_fmt_e pix_fmt) override; - cursor_t cursor; D3D11_MAPPED_SUBRESOURCE img_info; texture2d_t texture; }; + /** + * Display component for devices that use hardware encoders. + */ class display_vram_t: public display_base_t, public std::enable_shared_from_this { public: - virtual capture_e - snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor_visible) override; - std::shared_ptr alloc_img() override; int @@ -212,20 +295,76 @@ namespace platf::dxgi { std::vector get_supported_capture_formats() override; + bool + is_codec_supported(std::string_view name, const ::video::config_t &config) override; + + std::unique_ptr + make_avcodec_encode_device(pix_fmt_e pix_fmt) override; + + std::unique_ptr + make_nvenc_encode_device(pix_fmt_e pix_fmt) override; + + std::atomic next_image_id; + }; + + /** + * Display duplicator that uses the DirectX Desktop Duplication API. + */ + class duplication_t { + public: + dup_t dup; + bool has_frame {}; + std::chrono::steady_clock::time_point last_protected_content_warning_time {}; + + int + init(display_base_t *display, const ::video::config_t &config); + capture_e + next_frame(DXGI_OUTDUPL_FRAME_INFO &frame_info, std::chrono::milliseconds timeout, resource_t::pointer *res_p); + capture_e + reset(dup_t::pointer dup_p = dup_t::pointer()); + capture_e + release_frame(); + + ~duplication_t(); + }; + + /** + * Display backend that uses DDAPI with a software encoder. + */ + class display_ddup_ram_t: public display_ram_t { + public: int init(const ::video::config_t &config, const std::string &display_name); + capture_e + snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor_visible) override; + capture_e + release_snapshot() override; - std::shared_ptr - make_hwdevice(pix_fmt_e pix_fmt) override; + duplication_t dup; + cursor_t cursor; + }; + + /** + * Display backend that uses DDAPI with a hardware encoder. + */ + class display_ddup_vram_t: public display_vram_t { + public: + int + init(const ::video::config_t &config, const std::string &display_name); + capture_e + snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor_visible) override; + capture_e + release_snapshot() override; + duplication_t dup; sampler_state_t sampler_linear; blend_t blend_alpha; blend_t blend_invert; blend_t blend_disable; - ps_t scene_ps; - vs_t scene_vs; + ps_t cursor_ps; + vs_t cursor_vs; gpu_cursor_t cursor_alpha; gpu_cursor_t cursor_xor; @@ -233,7 +372,64 @@ namespace platf::dxgi { texture2d_t old_surface_delayed_destruction; std::chrono::steady_clock::time_point old_surface_timestamp; std::variant> last_frame_variant; + }; - std::atomic next_image_id; + /** + * Display duplicator that uses the Windows.Graphics.Capture API. + */ + class wgc_capture_t { + winrt::Windows::Graphics::DirectX::Direct3D11::IDirect3DDevice uwp_device { nullptr }; + winrt::Windows::Graphics::Capture::GraphicsCaptureItem item { nullptr }; + winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool frame_pool { nullptr }; + winrt::Windows::Graphics::Capture::GraphicsCaptureSession capture_session { nullptr }; + winrt::Windows::Graphics::Capture::Direct3D11CaptureFrame produced_frame { nullptr }, consumed_frame { nullptr }; + SRWLOCK frame_lock = SRWLOCK_INIT; + CONDITION_VARIABLE frame_present_cv; + + void + on_frame_arrived(winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool const &sender, winrt::Windows::Foundation::IInspectable const &); + + public: + wgc_capture_t(); + ~wgc_capture_t(); + + int + init(display_base_t *display, const ::video::config_t &config); + capture_e + next_frame(std::chrono::milliseconds timeout, ID3D11Texture2D **out, uint64_t &out_time); + capture_e + release_frame(); + int + set_cursor_visible(bool); + }; + + /** + * Display backend that uses Windows.Graphics.Capture with a software encoder. + */ + class display_wgc_ram_t: public display_ram_t { + wgc_capture_t dup; + + public: + int + init(const ::video::config_t &config, const std::string &display_name); + capture_e + snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor_visible) override; + capture_e + release_snapshot() override; + }; + + /** + * Display backend that uses Windows.Graphics.Capture with a hardware encoder. + */ + class display_wgc_vram_t: public display_vram_t { + wgc_capture_t dup; + + public: + int + init(const ::video::config_t &config, const std::string &display_name); + capture_e + snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor_visible) override; + capture_e + release_snapshot() override; }; } // namespace platf::dxgi diff --git a/src/platform/windows/display_base.cpp b/src/platform/windows/display_base.cpp index e4483ae77d1..8d7eb36bbcb 100644 --- a/src/platform/windows/display_base.cpp +++ b/src/platform/windows/display_base.cpp @@ -3,8 +3,8 @@ * @brief todo */ #include -#include #include +#include #include @@ -15,8 +15,9 @@ typedef long NTSTATUS; #include "display.h" #include "misc.h" #include "src/config.h" -#include "src/main.h" +#include "src/logging.h" #include "src/platform/common.h" +#include "src/stat_trackers.h" #include "src/video.h" namespace platf { @@ -25,6 +26,91 @@ namespace platf { namespace platf::dxgi { namespace bp = boost::process; + /** + * DDAPI-specific initialization goes here. + */ + int + duplication_t::init(display_base_t *display, const ::video::config_t &config) { + HRESULT status; + + // Capture format will be determined from the first call to AcquireNextFrame() + display->capture_format = DXGI_FORMAT_UNKNOWN; + + // FIXME: Duplicate output on RX580 in combination with DOOM (2016) --> BSOD + { + // IDXGIOutput5 is optional, but can provide improved performance and wide color support + dxgi::output5_t output5 {}; + status = display->output->QueryInterface(IID_IDXGIOutput5, (void **) &output5); + if (SUCCEEDED(status)) { + // Ask the display implementation which formats it supports + auto supported_formats = display->get_supported_capture_formats(); + if (supported_formats.empty()) { + BOOST_LOG(warning) << "No compatible capture formats for this encoder"sv; + return -1; + } + + // We try this twice, in case we still get an error on reinitialization + for (int x = 0; x < 2; ++x) { + // Ensure we can duplicate the current display + syncThreadDesktop(); + + status = output5->DuplicateOutput1((IUnknown *) display->device.get(), 0, supported_formats.size(), supported_formats.data(), &dup); + if (SUCCEEDED(status)) { + break; + } + std::this_thread::sleep_for(200ms); + } + + // We don't retry with DuplicateOutput() because we can hit this codepath when we're racing + // with mode changes and we don't want to accidentally fall back to suboptimal capture if + // we get unlucky and succeed below. + if (FAILED(status)) { + BOOST_LOG(warning) << "DuplicateOutput1 Failed [0x"sv << util::hex(status).to_string_view() << ']'; + return -1; + } + } + else { + BOOST_LOG(warning) << "IDXGIOutput5 is not supported by your OS. Capture performance may be reduced."sv; + + dxgi::output1_t output1 {}; + status = display->output->QueryInterface(IID_IDXGIOutput1, (void **) &output1); + if (FAILED(status)) { + BOOST_LOG(error) << "Failed to query IDXGIOutput1 from the output"sv; + return -1; + } + + for (int x = 0; x < 2; ++x) { + // Ensure we can duplicate the current display + syncThreadDesktop(); + + status = output1->DuplicateOutput((IUnknown *) display->device.get(), &dup); + if (SUCCEEDED(status)) { + break; + } + std::this_thread::sleep_for(200ms); + } + + if (FAILED(status)) { + BOOST_LOG(error) << "DuplicateOutput Failed [0x"sv << util::hex(status).to_string_view() << ']'; + return -1; + } + } + } + + DXGI_OUTDUPL_DESC dup_desc; + dup->GetDesc(&dup_desc); + + BOOST_LOG(info) << "Desktop resolution ["sv << dup_desc.ModeDesc.Width << 'x' << dup_desc.ModeDesc.Height << ']'; + BOOST_LOG(info) << "Desktop format ["sv << display->dxgi_format_to_string(dup_desc.ModeDesc.Format) << ']'; + + display->display_refresh_rate = dup_desc.ModeDesc.RefreshRate; + double display_refresh_rate_decimal = (double) display->display_refresh_rate.Numerator / display->display_refresh_rate.Denominator; + BOOST_LOG(info) << "Display refresh rate [" << display_refresh_rate_decimal << "Hz]"; + BOOST_LOG(info) << "Requested frame rate [" << display->client_frame_rate << "fps]"; + display->display_refresh_rate_rounded = lround(display_refresh_rate_decimal); + return 0; + } + capture_e duplication_t::next_frame(DXGI_OUTDUPL_FRAME_INFO &frame_info, std::chrono::milliseconds timeout, resource_t::pointer *res_p) { auto capture_status = release_frame(); @@ -32,10 +118,6 @@ namespace platf::dxgi { return capture_status; } - if (use_dwmflush) { - DwmFlush(); - } - auto status = dup->AcquireNextFrame(timeout.count(), &frame_info, res_p); switch (status) { @@ -78,19 +160,20 @@ namespace platf::dxgi { } auto status = dup->ReleaseFrame(); + has_frame = false; switch (status) { case S_OK: - has_frame = false; return capture_e::ok; - case DXGI_ERROR_WAIT_TIMEOUT: - return capture_e::timeout; - case WAIT_ABANDONED: + + case DXGI_ERROR_INVALID_CALL: + BOOST_LOG(warning) << "Duplication frame already released"; + return capture_e::ok; + case DXGI_ERROR_ACCESS_LOST: - case DXGI_ERROR_ACCESS_DENIED: - has_frame = false; return capture_e::reinit; + default: - BOOST_LOG(error) << "Couldn't release frame [0x"sv << util::hex(status).to_string_view(); + BOOST_LOG(error) << "Error while releasing duplication frame [0x"sv << util::hex(status).to_string_view(); return capture_e::error; } } @@ -99,24 +182,53 @@ namespace platf::dxgi { release_frame(); } + void + display_base_t::high_precision_sleep(std::chrono::nanoseconds duration) { + if (!timer) { + BOOST_LOG(error) << "Attempting high_precision_sleep() with uninitialized timer"; + return; + } + if (duration < 0s) { + BOOST_LOG(error) << "Attempting high_precision_sleep() with negative duration"; + return; + } + if (duration > 5s) { + BOOST_LOG(error) << "Attempting high_precision_sleep() with unexpectedly large duration (>5s)"; + return; + } + + LARGE_INTEGER due_time; + due_time.QuadPart = duration.count() / -100; + SetWaitableTimer(timer.get(), &due_time, 0, nullptr, nullptr, false); + WaitForSingleObject(timer.get(), INFINITE); + } + capture_e display_base_t::capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) { - auto next_frame = std::chrono::steady_clock::now(); - - // Use CREATE_WAITABLE_TIMER_HIGH_RESOLUTION if supported (Windows 10 1809+) - HANDLE timer = CreateWaitableTimerEx(nullptr, nullptr, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS); - if (!timer) { - timer = CreateWaitableTimerEx(nullptr, nullptr, 0, TIMER_ALL_ACCESS); - if (!timer) { - auto winerr = GetLastError(); - BOOST_LOG(error) << "Failed to create timer: "sv << winerr; - return capture_e::error; + auto adjust_client_frame_rate = [&]() -> DXGI_RATIONAL { + // Adjust capture frame interval when display refresh rate is not integral but very close to requested fps. + if (display_refresh_rate.Denominator > 1) { + DXGI_RATIONAL candidate = display_refresh_rate; + if (client_frame_rate % display_refresh_rate_rounded == 0) { + candidate.Numerator *= client_frame_rate / display_refresh_rate_rounded; + } + else if (display_refresh_rate_rounded % client_frame_rate == 0) { + candidate.Denominator *= display_refresh_rate_rounded / client_frame_rate; + } + double candidate_rate = (double) candidate.Numerator / candidate.Denominator; + // Can only decrease requested fps, otherwise client may start accumulating frames and suffer increased latency. + if (client_frame_rate > candidate_rate && candidate_rate / client_frame_rate > 0.99) { + BOOST_LOG(info) << "Adjusted capture rate to " << candidate_rate << "fps to better match display"; + return candidate; + } } - } - auto close_timer = util::fail_guard([timer]() { - CloseHandle(timer); - }); + return { (uint32_t) client_frame_rate, 1 }; + }; + + DXGI_RATIONAL client_frame_rate_adjusted = adjust_client_frame_rate(); + std::optional frame_pacing_group_start; + uint32_t frame_pacing_group_frames = 0; // Keep the display awake during capture. If the display goes to sleep during // capture, best case is that capture stops until it powers back on. However, @@ -127,6 +239,8 @@ namespace platf::dxgi { SetThreadExecutionState(ES_CONTINUOUS); }); + sleep_overshoot_tracker.reset(); + while (true) { // This will return false if the HDR state changes or for any number of other // display or GPU changes. We should reinit to examine the updated state of @@ -135,25 +249,77 @@ namespace platf::dxgi { return platf::capture_e::reinit; } - // If the wait time is between 1 us and 1 second, wait the specified time - // and offset the next frame time from the exact current frame time target. - auto wait_time_us = std::chrono::duration_cast(next_frame - std::chrono::steady_clock::now()).count(); - if (wait_time_us > 0 && wait_time_us < 1000000) { - LARGE_INTEGER due_time { .QuadPart = -10LL * wait_time_us }; - SetWaitableTimer(timer, &due_time, 0, nullptr, nullptr, false); - WaitForSingleObject(timer, INFINITE); - next_frame += delay; + platf::capture_e status = capture_e::ok; + std::shared_ptr img_out; + + // Try to continue frame pacing group, snapshot() is called with zero timeout after waiting for client frame interval + if (frame_pacing_group_start) { + const uint32_t seconds = (uint64_t) frame_pacing_group_frames * client_frame_rate_adjusted.Denominator / client_frame_rate_adjusted.Numerator; + const uint32_t remainder = (uint64_t) frame_pacing_group_frames * client_frame_rate_adjusted.Denominator % client_frame_rate_adjusted.Numerator; + const auto sleep_target = *frame_pacing_group_start + + std::chrono::nanoseconds(1s) * seconds + + std::chrono::nanoseconds(1s) * remainder / client_frame_rate_adjusted.Numerator; + const auto sleep_period = sleep_target - std::chrono::steady_clock::now(); + + if (sleep_period <= 0ns) { + // We missed next frame time, invalidating current frame pacing group + frame_pacing_group_start = std::nullopt; + frame_pacing_group_frames = 0; + status = capture_e::timeout; + } + else { + high_precision_sleep(sleep_period); + std::chrono::nanoseconds overshoot_ns = std::chrono::steady_clock::now() - sleep_target; + log_sleep_overshoot(overshoot_ns); + + status = snapshot(pull_free_image_cb, img_out, 0ms, *cursor); + + if (status == capture_e::ok && img_out) { + frame_pacing_group_frames += 1; + } + else { + frame_pacing_group_start = std::nullopt; + frame_pacing_group_frames = 0; + } + } } - else { - // If the wait time is negative (meaning the frame is past due) or the - // computed wait time is beyond a second (meaning possible clock issues), - // just capture the frame now and resynchronize the frame interval with - // the current time. - next_frame = std::chrono::steady_clock::now() + delay; + + // Start new frame pacing group if necessary, snapshot() is called with non-zero timeout + if (status == capture_e::timeout || (status == capture_e::ok && !frame_pacing_group_start)) { + status = snapshot(pull_free_image_cb, img_out, 200ms, *cursor); + + if (status == capture_e::ok && img_out) { + frame_pacing_group_start = img_out->frame_timestamp; + + if (!frame_pacing_group_start) { + BOOST_LOG(warning) << "snapshot() provided image without timestamp"; + frame_pacing_group_start = std::chrono::steady_clock::now(); + } + + frame_pacing_group_frames = 1; + } + else if (status == platf::capture_e::timeout) { + // The D3D11 device is protected by an unfair lock that is held the entire time that + // IDXGIOutputDuplication::AcquireNextFrame() is running. This is normally harmless, + // however sometimes the encoding thread needs to interact with our ID3D11Device to + // create dummy images or initialize the shared state that is used to pass textures + // between the capture and encoding ID3D11Devices. + // + // When we're in a state where we're not actively receiving frames regularly, we will + // spend almost 100% of our time in AcquireNextFrame() holding that critical lock. + // Worse still, since it's unfair, we can monopolize it while the encoding thread + // is starved. The encoding thread may acquire it for a few moments across a few + // ID3D11Device calls before losing it again to us for another long time waiting in + // AcquireNextFrame(). The starvation caused by this lock contention causes encoder + // reinitialization to take several seconds instead of a fraction of a second. + // + // To avoid starving the encoding thread, sleep without the lock held for a little + // while each time we reach our max frame timeout. This will only happen when nothing + // is updating the display, so no visible stutter should be introduced by the sleep. + std::this_thread::sleep_for(10ms); + } } - std::shared_ptr img_out; - auto status = snapshot(pull_free_image_cb, img_out, 1000ms, *cursor); switch (status) { case platf::capture_e::reinit: case platf::capture_e::error: @@ -173,6 +339,11 @@ namespace platf::dxgi { BOOST_LOG(error) << "Unrecognized capture status ["sv << (int) status << ']'; return status; } + + status = release_snapshot(); + if (status != platf::capture_e::ok) { + return status; + } } return capture_e::ok; @@ -264,8 +435,15 @@ namespace platf::dxgi { return false; } + /** + * @brief Tests to determine if the Desktop Duplication API can capture the given output. + * @details When testing for enumeration only, we avoid resyncing the thread desktop. + * @param adapter The DXGI adapter to use for capture. + * @param output The DXGI output to capture. + * @param enumeration_only Specifies whether this test is occurring for display enumeration. + */ bool - test_dxgi_duplication(adapter_t &adapter, output_t &output) { + test_dxgi_duplication(adapter_t &adapter, output_t &output, bool enumeration_only) { D3D_FEATURE_LEVEL featureLevels[] { D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0, @@ -302,11 +480,27 @@ namespace platf::dxgi { // Check if we can use the Desktop Duplication API on this output for (int x = 0; x < 2; ++x) { dup_t dup; + + // Only resynchronize the thread desktop when not enumerating displays. + // During enumeration, the caller will do this only once to ensure + // a consistent view of available outputs. + if (!enumeration_only) { + syncThreadDesktop(); + } + status = output1->DuplicateOutput((IUnknown *) device.get(), &dup); if (SUCCEEDED(status)) { return true; } - Sleep(200); + + // If we're not resyncing the thread desktop and we don't have permission to + // capture the current desktop, just bail immediately. Retrying won't help. + if (enumeration_only && status == E_ACCESSDENIED) { + break; + } + else { + std::this_thread::sleep_for(200ms); + } } BOOST_LOG(error) << "DuplicateOutput() test failed [0x"sv << util::hex(status).to_string_view() << ']'; @@ -331,11 +525,6 @@ namespace platf::dxgi { FreeLibrary(user32); }); - // Ensure we can duplicate the current display - syncThreadDesktop(); - - delay = std::chrono::nanoseconds { 1s } / config.framerate; - // Get rectangle of full desktop for absolute mouse coordinates env_width = GetSystemMetrics(SM_CXVIRTUALSCREEN); env_height = GetSystemMetrics(SM_CYVIRTUALSCREEN); @@ -353,10 +542,8 @@ namespace platf::dxgi { return -1; } - std::wstring_convert, wchar_t> converter; - - auto adapter_name = converter.from_bytes(config::video.adapter_name); - auto output_name = converter.from_bytes(display_name); + auto adapter_name = from_utf8(config::video.adapter_name); + auto output_name = from_utf8(display_name); adapter_t::pointer adapter_p; for (int tries = 0; tries < 2; ++tries) { @@ -381,7 +568,7 @@ namespace platf::dxgi { continue; } - if (desc.AttachedToDesktop && test_dxgi_duplication(adapter_tmp, output_tmp)) { + if (desc.AttachedToDesktop && test_dxgi_duplication(adapter_tmp, output_tmp, false)) { output = std::move(output_tmp); offset_x = desc.DesktopCoordinates.left; @@ -389,6 +576,17 @@ namespace platf::dxgi { width = desc.DesktopCoordinates.right - offset_x; height = desc.DesktopCoordinates.bottom - offset_y; + display_rotation = desc.Rotation; + if (display_rotation == DXGI_MODE_ROTATION_ROTATE90 || + display_rotation == DXGI_MODE_ROTATION_ROTATE270) { + width_before_rotation = height; + height_before_rotation = width; + } + else { + width_before_rotation = width; + height_before_rotation = height; + } + // left and bottom may be negative, yet absolute mouse coordinates start at 0x0 // Ensure offset starts at 0x0 offset_x -= GetSystemMetrics(SM_XVIRTUALSCREEN); @@ -456,7 +654,7 @@ namespace platf::dxgi { DXGI_ADAPTER_DESC adapter_desc; adapter->GetDesc(&adapter_desc); - auto description = converter.to_bytes(adapter_desc.Description); + auto description = to_utf8(adapter_desc.Description); BOOST_LOG(info) << std::endl << "Device Description : " << description << std::endl @@ -470,21 +668,6 @@ namespace platf::dxgi { << "Offset : "sv << offset_x << 'x' << offset_y << std::endl << "Virtual Desktop : "sv << env_width << 'x' << env_height; - // Enable DwmFlush() only if the current refresh rate can match the client framerate. - auto refresh_rate = config.framerate; - DWM_TIMING_INFO timing_info; - timing_info.cbSize = sizeof(timing_info); - - status = DwmGetCompositionTimingInfo(NULL, &timing_info); - if (FAILED(status)) { - BOOST_LOG(warning) << "Failed to detect active refresh rate."; - } - else { - refresh_rate = std::round((double) timing_info.rateRefresh.uiNumerator / (double) timing_info.rateRefresh.uiDenominator); - } - - dup.use_dwmflush = config::video.dwmflush && !(config.framerate > refresh_rate) ? true : false; - // Bump up thread priority { const DWORD flags = TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY; @@ -507,14 +690,65 @@ namespace platf::dxgi { HMODULE gdi32 = GetModuleHandleA("GDI32"); if (gdi32) { - PD3DKMTSetProcessSchedulingPriorityClass fn = - (PD3DKMTSetProcessSchedulingPriorityClass) GetProcAddress(gdi32, "D3DKMTSetProcessSchedulingPriorityClass"); - if (fn) { - status = fn(GetCurrentProcess(), D3DKMT_SCHEDULINGPRIORITYCLASS_REALTIME); - if (FAILED(status)) { - BOOST_LOG(warning) << "Failed to set realtime GPU priority. Please run application as administrator for optimal performance."; + auto check_hags = [&](const LUID &adapter) -> bool { + auto d3dkmt_open_adapter = (PD3DKMTOpenAdapterFromLuid) GetProcAddress(gdi32, "D3DKMTOpenAdapterFromLuid"); + auto d3dkmt_query_adapter_info = (PD3DKMTQueryAdapterInfo) GetProcAddress(gdi32, "D3DKMTQueryAdapterInfo"); + auto d3dkmt_close_adapter = (PD3DKMTCloseAdapter) GetProcAddress(gdi32, "D3DKMTCloseAdapter"); + if (!d3dkmt_open_adapter || !d3dkmt_query_adapter_info || !d3dkmt_close_adapter) { + BOOST_LOG(error) << "Couldn't load d3dkmt functions from gdi32.dll to determine GPU HAGS status"; + return false; + } + + D3DKMT_OPENADAPTERFROMLUID d3dkmt_adapter = { adapter }; + if (FAILED(d3dkmt_open_adapter(&d3dkmt_adapter))) { + BOOST_LOG(error) << "D3DKMTOpenAdapterFromLuid() failed while trying to determine GPU HAGS status"; + return false; + } + + bool result; + + D3DKMT_WDDM_2_7_CAPS d3dkmt_adapter_caps = {}; + D3DKMT_QUERYADAPTERINFO d3dkmt_adapter_info = {}; + d3dkmt_adapter_info.hAdapter = d3dkmt_adapter.hAdapter; + d3dkmt_adapter_info.Type = KMTQAITYPE_WDDM_2_7_CAPS; + d3dkmt_adapter_info.pPrivateDriverData = &d3dkmt_adapter_caps; + d3dkmt_adapter_info.PrivateDriverDataSize = sizeof(d3dkmt_adapter_caps); + + if (SUCCEEDED(d3dkmt_query_adapter_info(&d3dkmt_adapter_info))) { + result = d3dkmt_adapter_caps.HwSchEnabled; + } + else { + BOOST_LOG(warning) << "D3DKMTQueryAdapterInfo() failed while trying to determine GPU HAGS status"; + result = false; + } + + D3DKMT_CLOSEADAPTER d3dkmt_close_adapter_wrap = { d3dkmt_adapter.hAdapter }; + if (FAILED(d3dkmt_close_adapter(&d3dkmt_close_adapter_wrap))) { + BOOST_LOG(error) << "D3DKMTCloseAdapter() failed while trying to determine GPU HAGS status"; + } + + return result; + }; + + auto d3dkmt_set_process_priority = (PD3DKMTSetProcessSchedulingPriorityClass) GetProcAddress(gdi32, "D3DKMTSetProcessSchedulingPriorityClass"); + if (d3dkmt_set_process_priority) { + auto priority = D3DKMT_SCHEDULINGPRIORITYCLASS_REALTIME; + bool hags_enabled = check_hags(adapter_desc.AdapterLuid); + if (adapter_desc.VendorId == 0x10DE) { + // As of 2023.07, NVIDIA driver has unfixed bug(s) where "realtime" can cause unrecoverable encoding freeze or outright driver crash + // This issue happens more frequently with HAGS, in DX12 games or when VRAM is filled close to max capacity + // Track OBS to see if they find better workaround or NVIDIA fixes it on their end, they seem to be in communication + if (hags_enabled && !config::video.nv_realtime_hags) priority = D3DKMT_SCHEDULINGPRIORITYCLASS_HIGH; + } + BOOST_LOG(info) << "Active GPU has HAGS " << (hags_enabled ? "enabled" : "disabled"); + BOOST_LOG(info) << "Using " << (priority == D3DKMT_SCHEDULINGPRIORITYCLASS_HIGH ? "high" : "realtime") << " GPU priority"; + if (FAILED(d3dkmt_set_process_priority(GetCurrentProcess(), priority))) { + BOOST_LOG(warning) << "Failed to adjust GPU priority. Please run application as administrator for optimal performance."; } } + else { + BOOST_LOG(error) << "Couldn't load D3DKMTSetProcessSchedulingPriorityClass function from gdi32.dll to adjust GPU priority"; + } } dxgi::dxgi_t dxgi; @@ -545,67 +779,7 @@ namespace platf::dxgi { } } - // FIXME: Duplicate output on RX580 in combination with DOOM (2016) --> BSOD - { - // IDXGIOutput5 is optional, but can provide improved performance and wide color support - dxgi::output5_t output5 {}; - status = output->QueryInterface(IID_IDXGIOutput5, (void **) &output5); - if (SUCCEEDED(status)) { - // Ask the display implementation which formats it supports - auto supported_formats = get_supported_capture_formats(); - if (supported_formats.empty()) { - BOOST_LOG(warning) << "No compatible capture formats for this encoder"sv; - return -1; - } - - // We try this twice, in case we still get an error on reinitialization - for (int x = 0; x < 2; ++x) { - status = output5->DuplicateOutput1((IUnknown *) device.get(), 0, supported_formats.size(), supported_formats.data(), &dup.dup); - if (SUCCEEDED(status)) { - break; - } - std::this_thread::sleep_for(200ms); - } - - // We don't retry with DuplicateOutput() because we can hit this codepath when we're racing - // with mode changes and we don't want to accidentally fall back to suboptimal capture if - // we get unlucky and succeed below. - if (FAILED(status)) { - BOOST_LOG(warning) << "DuplicateOutput1 Failed [0x"sv << util::hex(status).to_string_view() << ']'; - return -1; - } - } - else { - BOOST_LOG(warning) << "IDXGIOutput5 is not supported by your OS. Capture performance may be reduced."sv; - - dxgi::output1_t output1 {}; - status = output->QueryInterface(IID_IDXGIOutput1, (void **) &output1); - if (FAILED(status)) { - BOOST_LOG(error) << "Failed to query IDXGIOutput1 from the output"sv; - return -1; - } - - for (int x = 0; x < 2; ++x) { - status = output1->DuplicateOutput((IUnknown *) device.get(), &dup.dup); - if (SUCCEEDED(status)) { - break; - } - std::this_thread::sleep_for(200ms); - } - - if (FAILED(status)) { - BOOST_LOG(error) << "DuplicateOutput Failed [0x"sv << util::hex(status).to_string_view() << ']'; - return -1; - } - } - } - - DXGI_OUTDUPL_DESC dup_desc; - dup.dup->GetDesc(&dup_desc); - - BOOST_LOG(info) << "Desktop resolution ["sv << dup_desc.ModeDesc.Width << 'x' << dup_desc.ModeDesc.Height << ']'; - BOOST_LOG(info) << "Desktop format ["sv << dxgi_format_to_string(dup_desc.ModeDesc.Format) << ']'; - + client_frame_rate = config.framerate; dxgi::output6_t output6 {}; status = output->QueryInterface(IID_IDXGIOutput6, (void **) &output6); if (SUCCEEDED(status)) { @@ -625,8 +799,16 @@ namespace platf::dxgi { << "Max Full Luminance : "sv << desc1.MaxFullFrameLuminance << " nits"sv; } - // Capture format will be determined from the first call to AcquireNextFrame() - capture_format = DXGI_FORMAT_UNKNOWN; + // Use CREATE_WAITABLE_TIMER_HIGH_RESOLUTION if supported (Windows 10 1809+) + timer.reset(CreateWaitableTimerEx(nullptr, nullptr, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS)); + if (!timer) { + timer.reset(CreateWaitableTimerEx(nullptr, nullptr, 0, TIMER_ALL_ACCESS)); + if (!timer) { + auto winerr = GetLastError(); + BOOST_LOG(error) << "Failed to create timer: "sv << winerr; + return -1; + } + } return 0; } @@ -872,23 +1054,47 @@ namespace platf::dxgi { } // namespace platf::dxgi namespace platf { + /** + * Pick a display adapter and capture method. + * @param hwdevice_type enables possible use of hardware encoder + */ std::shared_ptr display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config) { - if (hwdevice_type == mem_type_e::dxgi) { - auto disp = std::make_shared(); + if (config::video.capture == "ddx" || config::video.capture.empty()) { + if (hwdevice_type == mem_type_e::dxgi) { + auto disp = std::make_shared(); - if (!disp->init(config, display_name)) { - return disp; + if (!disp->init(config, display_name)) { + return disp; + } + } + else if (hwdevice_type == mem_type_e::system) { + auto disp = std::make_shared(); + + if (!disp->init(config, display_name)) { + return disp; + } } } - else if (hwdevice_type == mem_type_e::system) { - auto disp = std::make_shared(); - if (!disp->init(config, display_name)) { - return disp; + if (config::video.capture == "wgc" || config::video.capture.empty()) { + if (hwdevice_type == mem_type_e::dxgi) { + auto disp = std::make_shared(); + + if (!disp->init(config, display_name)) { + return disp; + } + } + else if (hwdevice_type == mem_type_e::system) { + auto disp = std::make_shared(); + + if (!disp->init(config, display_name)) { + return disp; + } } } + // ddx and wgc failed return nullptr; } @@ -900,13 +1106,18 @@ namespace platf { BOOST_LOG(debug) << "Detecting monitors..."sv; - std::wstring_convert, wchar_t> converter; - // We must set the GPU preference before calling any DXGI APIs! if (!dxgi::probe_for_gpu_preference(config::video.output_name)) { BOOST_LOG(warning) << "Failed to set GPU preference. Capture may not work!"sv; } + // We sync the thread desktop once before we start the enumeration process + // to ensure test_dxgi_duplication() returns consistent results for all GPUs + // even if the current desktop changes during our enumeration process. + // It is critical that we either fully succeed in enumeration or fully fail, + // otherwise it can lead to the capture code switching monitors unexpectedly. + syncThreadDesktop(); + dxgi::factory1_t factory; status = CreateDXGIFactory1(IID_IDXGIFactory1, (void **) &factory); if (FAILED(status)) { @@ -922,7 +1133,7 @@ namespace platf { BOOST_LOG(debug) << std::endl << "====== ADAPTER ====="sv << std::endl - << "Device Name : "sv << converter.to_bytes(adapter_desc.Description) << std::endl + << "Device Name : "sv << to_utf8(adapter_desc.Description) << std::endl << "Device Vendor ID : 0x"sv << util::hex(adapter_desc.VendorId).to_string_view() << std::endl << "Device Device ID : 0x"sv << util::hex(adapter_desc.DeviceId).to_string_view() << std::endl << "Device Video Mem : "sv << adapter_desc.DedicatedVideoMemory / 1048576 << " MiB"sv << std::endl @@ -938,7 +1149,7 @@ namespace platf { DXGI_OUTPUT_DESC desc; output->GetDesc(&desc); - auto device_name = converter.to_bytes(desc.DeviceName); + auto device_name = to_utf8(desc.DeviceName); auto width = desc.DesktopCoordinates.right - desc.DesktopCoordinates.left; auto height = desc.DesktopCoordinates.bottom - desc.DesktopCoordinates.top; @@ -950,7 +1161,7 @@ namespace platf { << std::endl; // Don't include the display in the list if we can't actually capture it - if (desc.AttachedToDesktop && dxgi::test_dxgi_duplication(adapter, output)) { + if (desc.AttachedToDesktop && dxgi::test_dxgi_duplication(adapter, output, true)) { display_names.emplace_back(std::move(device_name)); } } @@ -959,4 +1170,35 @@ namespace platf { return display_names; } + /** + * @brief Returns if GPUs/drivers have changed since the last call to this function. + * @return `true` if a change has occurred or if it is unknown whether a change occurred. + */ + bool + needs_encoder_reenumeration() { + // Serialize access to the static DXGI factory + static std::mutex reenumeration_state_lock; + auto lg = std::lock_guard(reenumeration_state_lock); + + // Keep a reference to the DXGI factory, which will keep track of changes internally. + static dxgi::factory1_t factory; + if (!factory || !factory->IsCurrent()) { + factory.reset(); + + auto status = CreateDXGIFactory1(IID_IDXGIFactory1, (void **) &factory); + if (FAILED(status)) { + BOOST_LOG(error) << "Failed to create DXGIFactory1 [0x"sv << util::hex(status).to_string_view() << ']'; + factory.release(); + } + + // Always request reenumeration on the first streaming session just to ensure we + // can deal with any initialization races that may occur when the system is booting. + BOOST_LOG(info) << "Encoder reenumeration is required"sv; + return true; + } + else { + // The DXGI factory from last time is still current, so no encoder changes have occurred. + return false; + } + } } // namespace platf diff --git a/src/platform/windows/display_ram.cpp b/src/platform/windows/display_ram.cpp index 631abce7be8..0a8e1a8b8f6 100644 --- a/src/platform/windows/display_ram.cpp +++ b/src/platform/windows/display_ram.cpp @@ -5,7 +5,7 @@ #include "display.h" #include "misc.h" -#include "src/main.h" +#include "src/logging.h" namespace platf { using namespace std::literals; @@ -177,9 +177,8 @@ namespace platf::dxgi { } capture_e - display_ram_t::snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor_visible) { + display_ddup_ram_t::snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor_visible) { HRESULT status; - DXGI_OUTDUPL_FRAME_INFO frame_info; resource_t::pointer res_p {}; @@ -326,6 +325,11 @@ namespace platf::dxgi { return capture_e::ok; } + capture_e + display_ddup_ram_t::release_snapshot() { + return dup.release_frame(); + } + std::shared_ptr display_ram_t::alloc_img() { auto img = std::make_shared(); @@ -382,11 +386,17 @@ namespace platf::dxgi { } int - display_ram_t::init(const ::video::config_t &config, const std::string &display_name) { - if (display_base_t::init(config, display_name)) { + display_ddup_ram_t::init(const ::video::config_t &config, const std::string &display_name) { + if (display_base_t::init(config, display_name) || dup.init(this, config)) { return -1; } return 0; } + + std::unique_ptr + display_ram_t::make_avcodec_encode_device(pix_fmt_e pix_fmt) { + return std::make_unique(); + } + } // namespace platf::dxgi diff --git a/src/platform/windows/display_vram.cpp b/src/platform/windows/display_vram.cpp index 376a58521da..ae0e6407347 100644 --- a/src/platform/windows/display_vram.cpp +++ b/src/platform/windows/display_vram.cpp @@ -4,8 +4,6 @@ */ #include -#include - #include #include @@ -16,10 +14,20 @@ extern "C" { #include "display.h" #include "misc.h" -#include "src/main.h" +#include "src/config.h" +#include "src/logging.h" +#include "src/nvenc/nvenc_config.h" +#include "src/nvenc/nvenc_d3d11.h" +#include "src/nvenc/nvenc_utils.h" #include "src/video.h" -#define SUNSHINE_SHADERS_DIR SUNSHINE_ASSETS_DIR "/shaders/directx" +#include + +#include + +#if !defined(SUNSHINE_SHADERS_DIR) // for testing this needs to be defined in cmake as we don't do an install + #define SUNSHINE_SHADERS_DIR SUNSHINE_ASSETS_DIR "/shaders/directx" +#endif namespace platf { using namespace std::literals; } @@ -94,16 +102,17 @@ namespace platf::dxgi { return blend; } - blob_t convert_UV_vs_hlsl; - blob_t convert_UV_ps_hlsl; - blob_t convert_UV_linear_ps_hlsl; - blob_t convert_UV_PQ_ps_hlsl; - blob_t scene_vs_hlsl; - blob_t convert_Y_ps_hlsl; - blob_t convert_Y_linear_ps_hlsl; - blob_t convert_Y_PQ_ps_hlsl; - blob_t scene_ps_hlsl; - blob_t scene_NW_ps_hlsl; + blob_t convert_yuv420_packed_uv_type0_ps_hlsl; + blob_t convert_yuv420_packed_uv_type0_ps_linear_hlsl; + blob_t convert_yuv420_packed_uv_type0_ps_perceptual_quantizer_hlsl; + blob_t convert_yuv420_packed_uv_type0_vs_hlsl; + blob_t convert_yuv420_planar_y_ps_hlsl; + blob_t convert_yuv420_planar_y_ps_linear_hlsl; + blob_t convert_yuv420_planar_y_ps_perceptual_quantizer_hlsl; + blob_t convert_yuv420_planar_y_vs_hlsl; + blob_t cursor_ps_hlsl; + blob_t cursor_ps_normalize_white_hlsl; + blob_t cursor_vs_hlsl; struct img_d3d_t: public platf::img_t { // These objects are owned by the display_t's ID3D11Device @@ -118,6 +127,9 @@ namespace platf::dxgi { // the first successful capture of a desktop frame bool dummy = false; + // Set to true if the image is blank (contains no content at all, including a cursor) + bool blank = true; + // Unique identifier for this image uint32_t id = 0; @@ -224,7 +236,7 @@ namespace platf::dxgi { auto xor_mask = std::begin(img_data) + bytes; for (auto x = 0; x < bytes; ++x) { - for (auto c = 7; c >= 0; --c) { + for (auto c = 7; c >= 0 && ((std::uint8_t *) pixel_data) != std::end(cursor_img); --c) { auto bit = 1 << c; auto color_type = ((*and_mask & bit) ? 1 : 0) + ((*xor_mask & bit) ? 2 : 0); @@ -297,7 +309,7 @@ namespace platf::dxgi { auto xor_mask = std::begin(img_data) + bytes; for (auto x = 0; x < bytes; ++x) { - for (auto c = 7; c >= 0; --c) { + for (auto c = 7; c >= 0 && ((std::uint8_t *) pixel_data) != std::end(cursor_img); --c) { auto bit = 1 << c; auto color_type = ((*and_mask & bit) ? 1 : 0) + ((*xor_mask & bit) ? 2 : 0); @@ -333,10 +345,9 @@ namespace platf::dxgi { #ifndef NDEBUG flags |= D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION; #endif - std::wstring_convert, wchar_t> converter; - auto wFile = converter.from_bytes(file); - auto status = D3DCompileFromFile(wFile.c_str(), nullptr, nullptr, entrypoint, shader_model, flags, 0, &compiled_p, &msg_p); + auto wFile = from_utf8(file); + auto status = D3DCompileFromFile(wFile.c_str(), nullptr, D3D_COMPILE_STANDARD_FILE_INCLUDE, entrypoint, shader_model, flags, 0, &compiled_p, &msg_p); if (msg_p) { BOOST_LOG(warning) << std::string_view { (const char *) msg_p->GetBufferPointer(), msg_p->GetBufferSize() - 1 }; @@ -361,10 +372,10 @@ namespace platf::dxgi { return compile_shader(file, "main_vs", "vs_5_0"); } - class hwdevice_t: public platf::hwdevice_t { + class d3d_base_encode_device final { public: int - convert(platf::img_t &img_base) override { + convert(platf::img_t &img_base) { // Garbage collect mapped capture images whose weak references have expired for (auto it = img_ctx_map.begin(); it != img_ctx_map.end();) { if (it->second.img_weak.expired()) { @@ -376,147 +387,71 @@ namespace platf::dxgi { } auto &img = (img_d3d_t &) img_base; - auto &img_ctx = img_ctx_map[img.id]; + if (!img.blank) { + auto &img_ctx = img_ctx_map[img.id]; - // Open the shared capture texture with our ID3D11Device - if (initialize_image_context(img, img_ctx)) { - return -1; - } + // Open the shared capture texture with our ID3D11Device + if (initialize_image_context(img, img_ctx)) { + return -1; + } - // Acquire encoder mutex to synchronize with capture code - auto status = img_ctx.encoder_mutex->AcquireSync(0, INFINITE); - if (status != S_OK) { - BOOST_LOG(error) << "Failed to acquire encoder mutex [0x"sv << util::hex(status).to_string_view() << ']'; - return -1; - } + // Acquire encoder mutex to synchronize with capture code + auto status = img_ctx.encoder_mutex->AcquireSync(0, INFINITE); + if (status != S_OK) { + BOOST_LOG(error) << "Failed to acquire encoder mutex [0x"sv << util::hex(status).to_string_view() << ']'; + return -1; + } - device_ctx->OMSetRenderTargets(1, &nv12_Y_rt, nullptr); - device_ctx->VSSetShader(scene_vs.get(), nullptr, 0); - device_ctx->PSSetShader(img.format == DXGI_FORMAT_R16G16B16A16_FLOAT ? convert_Y_fp16_ps.get() : convert_Y_ps.get(), nullptr, 0); - device_ctx->RSSetViewports(1, &outY_view); - device_ctx->PSSetShaderResources(0, 1, &img_ctx.encoder_input_res); - device_ctx->Draw(3, 0); + device_ctx->OMSetRenderTargets(1, &nv12_Y_rt, nullptr); + device_ctx->VSSetShader(scene_vs.get(), nullptr, 0); + device_ctx->PSSetShader(img.format == DXGI_FORMAT_R16G16B16A16_FLOAT ? convert_Y_fp16_ps.get() : convert_Y_ps.get(), nullptr, 0); + device_ctx->RSSetViewports(1, &outY_view); + device_ctx->PSSetShaderResources(0, 1, &img_ctx.encoder_input_res); + device_ctx->Draw(3, 0); - device_ctx->OMSetRenderTargets(1, &nv12_UV_rt, nullptr); - device_ctx->VSSetShader(convert_UV_vs.get(), nullptr, 0); - device_ctx->PSSetShader(img.format == DXGI_FORMAT_R16G16B16A16_FLOAT ? convert_UV_fp16_ps.get() : convert_UV_ps.get(), nullptr, 0); - device_ctx->RSSetViewports(1, &outUV_view); - device_ctx->Draw(3, 0); + device_ctx->OMSetRenderTargets(1, &nv12_UV_rt, nullptr); + device_ctx->VSSetShader(convert_UV_vs.get(), nullptr, 0); + device_ctx->PSSetShader(img.format == DXGI_FORMAT_R16G16B16A16_FLOAT ? convert_UV_fp16_ps.get() : convert_UV_ps.get(), nullptr, 0); + device_ctx->RSSetViewports(1, &outUV_view); + device_ctx->Draw(3, 0); - // Release encoder mutex to allow capture code to reuse this image - img_ctx.encoder_mutex->ReleaseSync(0); + // Release encoder mutex to allow capture code to reuse this image + img_ctx.encoder_mutex->ReleaseSync(0); - ID3D11ShaderResourceView *emptyShaderResourceView = nullptr; - device_ctx->PSSetShaderResources(0, 1, &emptyShaderResourceView); + ID3D11ShaderResourceView *emptyShaderResourceView = nullptr; + device_ctx->PSSetShaderResources(0, 1, &emptyShaderResourceView); + } return 0; } void - set_colorspace(std::uint32_t colorspace, std::uint32_t color_range) override { - switch (colorspace) { - case 5: // SWS_CS_SMPTE170M - color_p = &::video::colors[0]; - break; - case 1: // SWS_CS_ITU709 - color_p = &::video::colors[2]; - break; - case 9: // SWS_CS_BT2020 - color_p = &::video::colors[4]; - break; - default: - BOOST_LOG(warning) << "Colorspace: ["sv << colorspace << "] not yet supported: switching to default"sv; - color_p = &::video::colors[0]; - }; + apply_colorspace(const ::video::sunshine_colorspace_t &colorspace) { + auto color_vectors = ::video::color_vectors_from_colorspace(colorspace); - if (color_range > 1) { - // Full range - ++color_p; + if (!color_vectors) { + BOOST_LOG(error) << "No vector data for colorspace"sv; + return; } - auto color_matrix = make_buffer((device_t::pointer) data, *color_p); + auto color_matrix = make_buffer(device.get(), *color_vectors); if (!color_matrix) { BOOST_LOG(warning) << "Failed to create color matrix"sv; return; } - device_ctx->VSSetConstantBuffers(0, 1, &info_scene); device_ctx->PSSetConstantBuffers(0, 1, &color_matrix); this->color_matrix = std::move(color_matrix); } - void - init_hwframes(AVHWFramesContext *frames) override { - // We may be called with a QSV or D3D11VA context - if (frames->device_ctx->type == AV_HWDEVICE_TYPE_D3D11VA) { - auto d3d11_frames = (AVD3D11VAFramesContext *) frames->hwctx; - - // The encoder requires textures with D3D11_BIND_RENDER_TARGET set - d3d11_frames->BindFlags = D3D11_BIND_RENDER_TARGET; - d3d11_frames->MiscFlags = 0; - } - - // We require a single texture - frames->initial_pool_size = 1; - } - - int - prepare_to_derive_context(int hw_device_type) override { - // QuickSync requires our device to be multithread-protected - if (hw_device_type == AV_HWDEVICE_TYPE_QSV) { - multithread_t mt; - - auto status = device->QueryInterface(IID_ID3D11Multithread, (void **) &mt); - if (FAILED(status)) { - BOOST_LOG(warning) << "Failed to query ID3D11Multithread interface from device [0x"sv << util::hex(status).to_string_view() << ']'; - return -1; - } - - mt->SetMultithreadProtected(TRUE); - } - - return 0; - } - int - set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) override { - this->hwframe.reset(frame); - this->frame = frame; - - // Populate this frame with a hardware buffer if one isn't there already - if (!frame->buf[0]) { - auto err = av_hwframe_get_buffer(hw_frames_ctx, frame, 0); - if (err) { - char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 }; - BOOST_LOG(error) << "Failed to get hwframe buffer: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err); - return -1; - } - } - - // If this is a frame from a derived context, we'll need to map it to D3D11 - ID3D11Texture2D *frame_texture; - if (frame->format != AV_PIX_FMT_D3D11) { - frame_t d3d11_frame { av_frame_alloc() }; - - d3d11_frame->format = AV_PIX_FMT_D3D11; - - auto err = av_hwframe_map(d3d11_frame.get(), frame, AV_HWFRAME_MAP_WRITE | AV_HWFRAME_MAP_OVERWRITE); - if (err) { - char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 }; - BOOST_LOG(error) << "Failed to map D3D11 frame: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err); - return -1; - } - - // Get the texture from the mapped frame - frame_texture = (ID3D11Texture2D *) d3d11_frame->data[0]; - } - else { - // Otherwise, we can just use the texture inside the original frame - frame_texture = (ID3D11Texture2D *) frame->data[0]; - } + init_output(ID3D11Texture2D *frame_texture, int width, int height) { + // The underlying frame pool owns the texture, so we must reference it for ourselves + frame_texture->AddRef(); + output_texture.reset(frame_texture); - auto out_width = frame->width; - auto out_height = frame->height; + auto out_width = width; + auto out_height = height; float in_width = display->width; float in_height = display->height; @@ -533,24 +468,32 @@ namespace platf::dxgi { outY_view = D3D11_VIEWPORT { offsetX, offsetY, out_width_f, out_height_f, 0.0f, 1.0f }; outUV_view = D3D11_VIEWPORT { offsetX / 2, offsetY / 2, out_width_f / 2, out_height_f / 2, 0.0f, 1.0f }; - // The underlying frame pool owns the texture, so we must reference it for ourselves - frame_texture->AddRef(); - hwframe_texture.reset(frame_texture); - - float info_in[16 / sizeof(float)] { 1.0f / (float) out_width_f }; // aligned to 16-byte - info_scene = make_buffer(device.get(), info_in); + float subsample_offset_in[16 / sizeof(float)] { 1.0f / (float) out_width_f, 1.0f / (float) out_height_f }; // aligned to 16-byte + subsample_offset = make_buffer(device.get(), subsample_offset_in); - if (!info_scene) { - BOOST_LOG(error) << "Failed to create info scene buffer"sv; + if (!subsample_offset) { + BOOST_LOG(error) << "Failed to create subsample offset vertex constant buffer"; return -1; } + device_ctx->VSSetConstantBuffers(0, 1, &subsample_offset); + + { + int32_t rotation_modifier = display->display_rotation == DXGI_MODE_ROTATION_UNSPECIFIED ? 0 : display->display_rotation - 1; + int32_t rotation_data[16 / sizeof(int32_t)] { -rotation_modifier }; // aligned to 16-byte + auto rotation = make_buffer(device.get(), rotation_data); + if (!rotation) { + BOOST_LOG(error) << "Failed to create display rotation vertex constant buffer"; + return -1; + } + device_ctx->VSSetConstantBuffers(1, 1, &rotation); + } D3D11_RENDER_TARGET_VIEW_DESC nv12_rt_desc { format == DXGI_FORMAT_P010 ? DXGI_FORMAT_R16_UNORM : DXGI_FORMAT_R8_UNORM, D3D11_RTV_DIMENSION_TEXTURE2D }; - auto status = device->CreateRenderTargetView(hwframe_texture.get(), &nv12_rt_desc, &nv12_Y_rt); + auto status = device->CreateRenderTargetView(output_texture.get(), &nv12_rt_desc, &nv12_Y_rt); if (FAILED(status)) { BOOST_LOG(error) << "Failed to create render target view [0x"sv << util::hex(status).to_string_view() << ']'; return -1; @@ -558,7 +501,7 @@ namespace platf::dxgi { nv12_rt_desc.Format = (format == DXGI_FORMAT_P010) ? DXGI_FORMAT_R16G16_UNORM : DXGI_FORMAT_R8G8_UNORM; - status = device->CreateRenderTargetView(hwframe_texture.get(), &nv12_rt_desc, &nv12_UV_rt); + status = device->CreateRenderTargetView(output_texture.get(), &nv12_rt_desc, &nv12_UV_rt); if (FAILED(status)) { BOOST_LOG(error) << "Failed to create render target view [0x"sv << util::hex(status).to_string_view() << ']'; return -1; @@ -574,9 +517,7 @@ namespace platf::dxgi { } int - init( - std::shared_ptr display, adapter_t::pointer adapter_p, - pix_fmt_e pix_fmt) { + init(std::shared_ptr display, adapter_t::pointer adapter_p, pix_fmt_e pix_fmt) { D3D_FEATURE_LEVEL featureLevels[] { D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0, @@ -615,16 +556,14 @@ namespace platf::dxgi { BOOST_LOG(warning) << "Failed to increase encoding GPU thread priority. Please run application as administrator for optimal performance."; } - data = device.get(); - format = (pix_fmt == pix_fmt_e::nv12 ? DXGI_FORMAT_NV12 : DXGI_FORMAT_P010); - status = device->CreateVertexShader(scene_vs_hlsl->GetBufferPointer(), scene_vs_hlsl->GetBufferSize(), nullptr, &scene_vs); + status = device->CreateVertexShader(convert_yuv420_planar_y_vs_hlsl->GetBufferPointer(), convert_yuv420_planar_y_vs_hlsl->GetBufferSize(), nullptr, &scene_vs); if (status) { BOOST_LOG(error) << "Failed to create scene vertex shader [0x"sv << util::hex(status).to_string_view() << ']'; return -1; } - status = device->CreateVertexShader(convert_UV_vs_hlsl->GetBufferPointer(), convert_UV_vs_hlsl->GetBufferSize(), nullptr, &convert_UV_vs); + status = device->CreateVertexShader(convert_yuv420_packed_uv_type0_vs_hlsl->GetBufferPointer(), convert_yuv420_packed_uv_type0_vs_hlsl->GetBufferSize(), nullptr, &convert_UV_vs); if (status) { BOOST_LOG(error) << "Failed to create convertUV vertex shader [0x"sv << util::hex(status).to_string_view() << ']'; return -1; @@ -632,13 +571,13 @@ namespace platf::dxgi { // If the display is in HDR and we're streaming HDR, we'll be converting scRGB to SMPTE 2084 PQ. if (format == DXGI_FORMAT_P010 && display->is_hdr()) { - status = device->CreatePixelShader(convert_Y_PQ_ps_hlsl->GetBufferPointer(), convert_Y_PQ_ps_hlsl->GetBufferSize(), nullptr, &convert_Y_fp16_ps); + status = device->CreatePixelShader(convert_yuv420_planar_y_ps_perceptual_quantizer_hlsl->GetBufferPointer(), convert_yuv420_planar_y_ps_perceptual_quantizer_hlsl->GetBufferSize(), nullptr, &convert_Y_fp16_ps); if (status) { BOOST_LOG(error) << "Failed to create convertY pixel shader [0x"sv << util::hex(status).to_string_view() << ']'; return -1; } - status = device->CreatePixelShader(convert_UV_PQ_ps_hlsl->GetBufferPointer(), convert_UV_PQ_ps_hlsl->GetBufferSize(), nullptr, &convert_UV_fp16_ps); + status = device->CreatePixelShader(convert_yuv420_packed_uv_type0_ps_perceptual_quantizer_hlsl->GetBufferPointer(), convert_yuv420_packed_uv_type0_ps_perceptual_quantizer_hlsl->GetBufferSize(), nullptr, &convert_UV_fp16_ps); if (status) { BOOST_LOG(error) << "Failed to create convertUV pixel shader [0x"sv << util::hex(status).to_string_view() << ']'; return -1; @@ -647,13 +586,13 @@ namespace platf::dxgi { else { // If the display is in Advanced Color mode, the desktop format will be scRGB FP16. // scRGB uses linear gamma, so we must use our linear to sRGB conversion shaders. - status = device->CreatePixelShader(convert_Y_linear_ps_hlsl->GetBufferPointer(), convert_Y_linear_ps_hlsl->GetBufferSize(), nullptr, &convert_Y_fp16_ps); + status = device->CreatePixelShader(convert_yuv420_planar_y_ps_linear_hlsl->GetBufferPointer(), convert_yuv420_planar_y_ps_linear_hlsl->GetBufferSize(), nullptr, &convert_Y_fp16_ps); if (status) { BOOST_LOG(error) << "Failed to create convertY pixel shader [0x"sv << util::hex(status).to_string_view() << ']'; return -1; } - status = device->CreatePixelShader(convert_UV_linear_ps_hlsl->GetBufferPointer(), convert_UV_linear_ps_hlsl->GetBufferSize(), nullptr, &convert_UV_fp16_ps); + status = device->CreatePixelShader(convert_yuv420_packed_uv_type0_ps_linear_hlsl->GetBufferPointer(), convert_yuv420_packed_uv_type0_ps_linear_hlsl->GetBufferSize(), nullptr, &convert_UV_fp16_ps); if (status) { BOOST_LOG(error) << "Failed to create convertUV pixel shader [0x"sv << util::hex(status).to_string_view() << ']'; return -1; @@ -661,34 +600,36 @@ namespace platf::dxgi { } // These shaders consume standard 8-bit sRGB input - status = device->CreatePixelShader(convert_Y_ps_hlsl->GetBufferPointer(), convert_Y_ps_hlsl->GetBufferSize(), nullptr, &convert_Y_ps); + status = device->CreatePixelShader(convert_yuv420_planar_y_ps_hlsl->GetBufferPointer(), convert_yuv420_planar_y_ps_hlsl->GetBufferSize(), nullptr, &convert_Y_ps); if (status) { BOOST_LOG(error) << "Failed to create convertY pixel shader [0x"sv << util::hex(status).to_string_view() << ']'; return -1; } - status = device->CreatePixelShader(convert_UV_ps_hlsl->GetBufferPointer(), convert_UV_ps_hlsl->GetBufferSize(), nullptr, &convert_UV_ps); + status = device->CreatePixelShader(convert_yuv420_packed_uv_type0_ps_hlsl->GetBufferPointer(), convert_yuv420_packed_uv_type0_ps_hlsl->GetBufferSize(), nullptr, &convert_UV_ps); if (status) { BOOST_LOG(error) << "Failed to create convertUV pixel shader [0x"sv << util::hex(status).to_string_view() << ']'; return -1; } - color_matrix = make_buffer(device.get(), ::video::colors[0]); + auto default_color_vectors = ::video::color_vectors_from_colorspace(::video::colorspace_e::rec601, false); + if (!default_color_vectors) { + BOOST_LOG(error) << "Missing color vectors for Rec. 601"sv; + return -1; + } + + color_matrix = make_buffer(device.get(), *default_color_vectors); if (!color_matrix) { BOOST_LOG(error) << "Failed to create color matrix buffer"sv; return -1; } + device_ctx->PSSetConstantBuffers(0, 1, &color_matrix); - D3D11_INPUT_ELEMENT_DESC layout_desc { - "SV_Position", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 - }; - - status = device->CreateInputLayout( - &layout_desc, 1, - convert_UV_vs_hlsl->GetBufferPointer(), convert_UV_vs_hlsl->GetBufferSize(), - &input_layout); - - this->display = std::move(display); + this->display = std::dynamic_pointer_cast(display); + if (!this->display) { + return -1; + } + display = nullptr; blend_disable = make_blend(device.get(), false, false); if (!blend_disable) { @@ -710,10 +651,6 @@ namespace platf::dxgi { return -1; } - device_ctx->IASetInputLayout(input_layout.get()); - device_ctx->PSSetConstantBuffers(0, 1, &color_matrix); - device_ctx->VSSetConstantBuffers(0, 1, &info_scene); - device_ctx->OMSetBlendState(blend_disable.get(), nullptr, 0xFFFFFFFFu); device_ctx->PSSetSamplers(0, 1, &sampler_linear); device_ctx->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP); @@ -721,7 +658,6 @@ namespace platf::dxgi { return 0; } - private: struct encoder_img_ctx_t { // Used to determine if the underlying texture changes. // Not safe for actual use by the encoder! @@ -789,32 +725,24 @@ namespace platf::dxgi { return 0; } - public: - frame_t hwframe; - ::video::color_t *color_p; - buf_t info_scene; + buf_t subsample_offset; buf_t color_matrix; - input_layout_t input_layout; - blend_t blend_disable; sampler_state_t sampler_linear; render_target_t nv12_Y_rt; render_target_t nv12_UV_rt; - // The image referenced by hwframe - texture2d_t hwframe_texture; - // d3d_img_t::id -> encoder_img_ctx_t // These store the encoder textures for each img_t that passes through // convert(). We can't store them in the img_t itself because it is shared // amongst multiple hwdevice_t objects (and therefore multiple ID3D11Devices). std::map img_ctx_map; - std::shared_ptr display; + std::shared_ptr display; vs_t convert_UV_vs; ps_t convert_UV_ps; @@ -830,6 +758,145 @@ namespace platf::dxgi { device_t device; device_ctx_t device_ctx; + + texture2d_t output_texture; + }; + + class d3d_avcodec_encode_device_t: public avcodec_encode_device_t { + public: + int + init(std::shared_ptr display, adapter_t::pointer adapter_p, pix_fmt_e pix_fmt) { + int result = base.init(display, adapter_p, pix_fmt); + data = base.device.get(); + return result; + } + + int + convert(platf::img_t &img_base) override { + return base.convert(img_base); + } + + void + apply_colorspace() override { + base.apply_colorspace(colorspace); + } + + void + init_hwframes(AVHWFramesContext *frames) override { + // We may be called with a QSV or D3D11VA context + if (frames->device_ctx->type == AV_HWDEVICE_TYPE_D3D11VA) { + auto d3d11_frames = (AVD3D11VAFramesContext *) frames->hwctx; + + // The encoder requires textures with D3D11_BIND_RENDER_TARGET set + d3d11_frames->BindFlags = D3D11_BIND_RENDER_TARGET; + d3d11_frames->MiscFlags = 0; + } + + // We require a single texture + frames->initial_pool_size = 1; + } + + int + prepare_to_derive_context(int hw_device_type) override { + // QuickSync requires our device to be multithread-protected + if (hw_device_type == AV_HWDEVICE_TYPE_QSV) { + multithread_t mt; + + auto status = base.device->QueryInterface(IID_ID3D11Multithread, (void **) &mt); + if (FAILED(status)) { + BOOST_LOG(warning) << "Failed to query ID3D11Multithread interface from device [0x"sv << util::hex(status).to_string_view() << ']'; + return -1; + } + + mt->SetMultithreadProtected(TRUE); + } + + return 0; + } + + int + set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) override { + this->hwframe.reset(frame); + this->frame = frame; + + // Populate this frame with a hardware buffer if one isn't there already + if (!frame->buf[0]) { + auto err = av_hwframe_get_buffer(hw_frames_ctx, frame, 0); + if (err) { + char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 }; + BOOST_LOG(error) << "Failed to get hwframe buffer: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err); + return -1; + } + } + + // If this is a frame from a derived context, we'll need to map it to D3D11 + ID3D11Texture2D *frame_texture; + if (frame->format != AV_PIX_FMT_D3D11) { + frame_t d3d11_frame { av_frame_alloc() }; + + d3d11_frame->format = AV_PIX_FMT_D3D11; + + auto err = av_hwframe_map(d3d11_frame.get(), frame, AV_HWFRAME_MAP_WRITE | AV_HWFRAME_MAP_OVERWRITE); + if (err) { + char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 }; + BOOST_LOG(error) << "Failed to map D3D11 frame: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err); + return -1; + } + + // Get the texture from the mapped frame + frame_texture = (ID3D11Texture2D *) d3d11_frame->data[0]; + } + else { + // Otherwise, we can just use the texture inside the original frame + frame_texture = (ID3D11Texture2D *) frame->data[0]; + } + + return base.init_output(frame_texture, frame->width, frame->height); + } + + private: + d3d_base_encode_device base; + frame_t hwframe; + }; + + class d3d_nvenc_encode_device_t: public nvenc_encode_device_t { + public: + bool + init_device(std::shared_ptr display, adapter_t::pointer adapter_p, pix_fmt_e pix_fmt) { + buffer_format = nvenc::nvenc_format_from_sunshine_format(pix_fmt); + if (buffer_format == NV_ENC_BUFFER_FORMAT_UNDEFINED) { + BOOST_LOG(error) << "Unexpected pixel format for NvENC ["sv << from_pix_fmt(pix_fmt) << ']'; + return false; + } + + if (base.init(display, adapter_p, pix_fmt)) return false; + + nvenc_d3d = std::make_unique(base.device.get()); + nvenc = nvenc_d3d.get(); + + return true; + } + + bool + init_encoder(const ::video::config_t &client_config, const ::video::sunshine_colorspace_t &colorspace) override { + if (!nvenc_d3d) return false; + + auto nvenc_colorspace = nvenc::nvenc_colorspace_from_sunshine_colorspace(colorspace); + if (!nvenc_d3d->create_encoder(config::video.nv, client_config, nvenc_colorspace, buffer_format)) return false; + + base.apply_colorspace(colorspace); + return base.init_output(nvenc_d3d->get_input_texture(), client_config.width, client_config.height) == 0; + } + + int + convert(platf::img_t &img_base) override { + return base.convert(img_base); + } + + private: + d3d_base_encode_device base; + std::unique_ptr nvenc_d3d; + NV_ENC_BUFFER_FORMAT buffer_format = NV_ENC_BUFFER_FORMAT_UNDEFINED; }; bool @@ -878,9 +945,8 @@ namespace platf::dxgi { } capture_e - display_vram_t::snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor_visible) { + display_ddup_vram_t::snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor_visible) { HRESULT status; - DXGI_OUTDUPL_FRAME_INFO frame_info; resource_t::pointer res_p {}; @@ -892,7 +958,7 @@ namespace platf::dxgi { } const bool mouse_update_flag = frame_info.LastMouseUpdateTime.QuadPart != 0 || frame_info.PointerShapeBufferSize > 0; - const bool frame_update_flag = frame_info.AccumulatedFrames != 0 || frame_info.LastPresentTime.QuadPart != 0; + const bool frame_update_flag = frame_info.LastPresentTime.QuadPart != 0; const bool update_flag = mouse_update_flag || frame_update_flag; if (!update_flag) { @@ -928,8 +994,11 @@ namespace platf::dxgi { } if (frame_info.LastMouseUpdateTime.QuadPart) { - cursor_alpha.set_pos(frame_info.PointerPosition.Position.x, frame_info.PointerPosition.Position.y, frame_info.PointerPosition.Visible); - cursor_xor.set_pos(frame_info.PointerPosition.Position.x, frame_info.PointerPosition.Position.y, frame_info.PointerPosition.Visible); + cursor_alpha.set_pos(frame_info.PointerPosition.Position.x, frame_info.PointerPosition.Position.y, + width, height, display_rotation, frame_info.PointerPosition.Visible); + + cursor_xor.set_pos(frame_info.PointerPosition.Position.x, frame_info.PointerPosition.Position.y, + width, height, display_rotation, frame_info.PointerPosition.Visible); } const bool blend_mouse_cursor_flag = (cursor_alpha.visible || cursor_xor.visible) && cursor_visible; @@ -948,7 +1017,7 @@ namespace platf::dxgi { // It's possible for our display enumeration to race with mode changes and result in // mismatched image pool and desktop texture sizes. If this happens, just reinit again. - if (desc.Width != width || desc.Height != height) { + if (desc.Width != width_before_rotation || desc.Height != height_before_rotation) { BOOST_LOG(info) << "Capture size changed ["sv << width << 'x' << height << " -> "sv << desc.Width << 'x' << desc.Height << ']'; return capture_e::reinit; } @@ -1049,8 +1118,8 @@ namespace platf::dxgi { // Otherwise create a new surface. D3D11_TEXTURE2D_DESC t {}; - t.Width = width; - t.Height = height; + t.Width = width_before_rotation; + t.Height = height_before_rotation; t.MipLevels = 1; t.ArraySize = 1; t.SampleDesc.Count = 1; @@ -1070,7 +1139,7 @@ namespace platf::dxgi { auto d3d_img = std::static_pointer_cast(img); // Finish creating the image (if it hasn't happened already), - // also creates synchonization primitives for shared access from multiple direct3d devices. + // also creates synchronization primitives for shared access from multiple direct3d devices. if (complete_img(d3d_img.get(), dummy)) return { nullptr, nullptr }; // This image is shared between capture direct3d device and encoders direct3d devices, @@ -1081,6 +1150,9 @@ namespace platf::dxgi { return { nullptr, nullptr }; } + // Clear the blank flag now that we're ready to capture into the image + d3d_img->blank = false; + return { std::move(d3d_img), std::move(lock_helper) }; }; @@ -1157,8 +1229,8 @@ namespace platf::dxgi { } auto blend_cursor = [&](img_d3d_t &d3d_img) { - device_ctx->VSSetShader(scene_vs.get(), nullptr, 0); - device_ctx->PSSetShader(scene_ps.get(), nullptr, 0); + device_ctx->VSSetShader(cursor_vs.get(), nullptr, 0); + device_ctx->PSSetShader(cursor_ps.get(), nullptr, 0); device_ctx->OMSetRenderTargets(1, &d3d_img.capture_rt, nullptr); if (cursor_alpha.texture.get()) { @@ -1226,15 +1298,14 @@ namespace platf::dxgi { // Clear the image if it has been used as a dummy. // It can have the mouse cursor blended onto it. auto old_d3d_img = (img_d3d_t *) img_out.get(); - bool reclear_dummy = old_d3d_img->dummy && old_d3d_img->capture_texture; + bool reclear_dummy = !old_d3d_img->blank && old_d3d_img->capture_texture; auto [d3d_img, lock] = get_locked_d3d_img(img_out, true); if (!d3d_img) return capture_e::error; if (reclear_dummy) { - auto dummy_data = std::make_unique(d3d_img->row_pitch * d3d_img->height); - std::fill_n(dummy_data.get(), d3d_img->row_pitch * d3d_img->height, 0); - device_ctx->UpdateSubresource(d3d_img->capture_texture.get(), 0, nullptr, dummy_data.get(), d3d_img->row_pitch, 0); + const float rgb_black[] = { 0.0f, 0.0f, 0.0f, 0.0f }; + device_ctx->ClearRenderTargetView(d3d_img->capture_rt.get(), rgb_black); } if (blend_mouse_cursor_flag) { @@ -1257,9 +1328,14 @@ namespace platf::dxgi { return capture_e::ok; } + capture_e + display_ddup_vram_t::release_snapshot() { + return dup.release_frame(); + } + int - display_vram_t::init(const ::video::config_t &config, const std::string &display_name) { - if (display_base_t::init(config, display_name)) { + display_ddup_vram_t::init(const ::video::config_t &config, const std::string &display_name) { + if (display_base_t::init(config, display_name) || dup.init(this, config)) { return -1; } @@ -1278,36 +1354,47 @@ namespace platf::dxgi { return -1; } - status = device->CreateVertexShader(scene_vs_hlsl->GetBufferPointer(), scene_vs_hlsl->GetBufferSize(), nullptr, &scene_vs); + status = device->CreateVertexShader(cursor_vs_hlsl->GetBufferPointer(), cursor_vs_hlsl->GetBufferSize(), nullptr, &cursor_vs); if (status) { BOOST_LOG(error) << "Failed to create scene vertex shader [0x"sv << util::hex(status).to_string_view() << ']'; return -1; } + { + int32_t rotation_modifier = display_rotation == DXGI_MODE_ROTATION_UNSPECIFIED ? 0 : display_rotation - 1; + int32_t rotation_data[16 / sizeof(int32_t)] { rotation_modifier }; // aligned to 16-byte + auto rotation = make_buffer(device.get(), rotation_data); + if (!rotation) { + BOOST_LOG(error) << "Failed to create display rotation vertex constant buffer"; + return -1; + } + device_ctx->VSSetConstantBuffers(2, 1, &rotation); + } + if (config.dynamicRange && is_hdr()) { // This shader will normalize scRGB white levels to a user-defined white level - status = device->CreatePixelShader(scene_NW_ps_hlsl->GetBufferPointer(), scene_NW_ps_hlsl->GetBufferSize(), nullptr, &scene_ps); + status = device->CreatePixelShader(cursor_ps_normalize_white_hlsl->GetBufferPointer(), cursor_ps_normalize_white_hlsl->GetBufferSize(), nullptr, &cursor_ps); if (status) { - BOOST_LOG(error) << "Failed to create scene pixel shader [0x"sv << util::hex(status).to_string_view() << ']'; + BOOST_LOG(error) << "Failed to create cursor blending (normalized white) pixel shader [0x"sv << util::hex(status).to_string_view() << ']'; return -1; } // Use a 300 nit target for the mouse cursor. We should really get // the user's SDR white level in nits, but there is no API that // provides that information to Win32 apps. - float sdr_multiplier_data[16 / sizeof(float)] { 300.0f / 80.f }; // aligned to 16-byte - auto sdr_multiplier = make_buffer(device.get(), sdr_multiplier_data); - if (!sdr_multiplier) { - BOOST_LOG(warning) << "Failed to create SDR multiplier"sv; + float white_multiplier_data[16 / sizeof(float)] { 300.0f / 80.f }; // aligned to 16-byte + auto white_multiplier = make_buffer(device.get(), white_multiplier_data); + if (!white_multiplier) { + BOOST_LOG(warning) << "Failed to create cursor blending (normalized white) white multiplier constant buffer"; return -1; } - device_ctx->PSSetConstantBuffers(0, 1, &sdr_multiplier); + device_ctx->PSSetConstantBuffers(1, 1, &white_multiplier); } else { - status = device->CreatePixelShader(scene_ps_hlsl->GetBufferPointer(), scene_ps_hlsl->GetBufferSize(), nullptr, &scene_ps); + status = device->CreatePixelShader(cursor_ps_hlsl->GetBufferPointer(), cursor_ps_hlsl->GetBufferSize(), nullptr, &cursor_ps); if (status) { - BOOST_LOG(error) << "Failed to create scene pixel shader [0x"sv << util::hex(status).to_string_view() << ']'; + BOOST_LOG(error) << "Failed to create cursor blending pixel shader [0x"sv << util::hex(status).to_string_view() << ']'; return -1; } } @@ -1327,14 +1414,89 @@ namespace platf::dxgi { return 0; } + /** + * Get the next frame from the Windows.Graphics.Capture API and copy it into a new snapshot texture. + * @param pull_free_image_cb call this to get a new free image from the video subsystem. + * @param img_out the captured frame is returned here + * @param timeout how long to wait for the next frame + * @param cursor_visible + */ + capture_e + display_wgc_vram_t::snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor_visible) { + texture2d_t src; + uint64_t frame_qpc; + dup.set_cursor_visible(cursor_visible); + auto capture_status = dup.next_frame(timeout, &src, frame_qpc); + if (capture_status != capture_e::ok) + return capture_status; + + auto frame_timestamp = std::chrono::steady_clock::now() - qpc_time_difference(qpc_counter(), frame_qpc); + D3D11_TEXTURE2D_DESC desc; + src->GetDesc(&desc); + + // It's possible for our display enumeration to race with mode changes and result in + // mismatched image pool and desktop texture sizes. If this happens, just reinit again. + if (desc.Width != width_before_rotation || desc.Height != height_before_rotation) { + BOOST_LOG(info) << "Capture size changed ["sv << width << 'x' << height << " -> "sv << desc.Width << 'x' << desc.Height << ']'; + return capture_e::reinit; + } + + // It's also possible for the capture format to change on the fly. If that happens, + // reinitialize capture to try format detection again and create new images. + if (capture_format != desc.Format) { + BOOST_LOG(info) << "Capture format changed ["sv << dxgi_format_to_string(capture_format) << " -> "sv << dxgi_format_to_string(desc.Format) << ']'; + return capture_e::reinit; + } + + std::shared_ptr img; + if (!pull_free_image_cb(img)) + return capture_e::interrupted; + + auto d3d_img = std::static_pointer_cast(img); + d3d_img->blank = false; // image is always ready for capture + if (complete_img(d3d_img.get(), false) == 0) { + texture_lock_helper lock_helper(d3d_img->capture_mutex.get()); + if (lock_helper.lock()) { + device_ctx->CopyResource(d3d_img->capture_texture.get(), src.get()); + } + else { + BOOST_LOG(error) << "Failed to lock capture texture"; + return capture_e::error; + } + } + else { + return capture_e::error; + } + img_out = img; + if (img_out) { + img_out->frame_timestamp = frame_timestamp; + } + + return capture_e::ok; + } + + capture_e + display_wgc_vram_t::release_snapshot() { + return dup.release_frame(); + } + + int + display_wgc_vram_t::init(const ::video::config_t &config, const std::string &display_name) { + if (display_base_t::init(config, display_name) || dup.init(this, config)) + return -1; + + return 0; + } + std::shared_ptr display_vram_t::alloc_img() { auto img = std::make_shared(); // Initialize format-independent fields - img->width = width; - img->height = height; + img->width = width_before_rotation; + img->height = height_before_rotation; img->id = next_image_id++; + img->blank = true; return img; } @@ -1382,20 +1544,7 @@ namespace platf::dxgi { t.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET; t.MiscFlags = D3D11_RESOURCE_MISC_SHARED_NTHANDLE | D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX; - HRESULT status; - if (dummy) { - auto dummy_data = std::make_unique(img->row_pitch * img->height); - std::fill_n(dummy_data.get(), img->row_pitch * img->height, 0); - D3D11_SUBRESOURCE_DATA initial_data { - dummy_data.get(), - (UINT) img->row_pitch, - 0 - }; - status = device->CreateTexture2D(&t, &initial_data, &img->capture_texture); - } - else { - status = device->CreateTexture2D(&t, nullptr, &img->capture_texture); - } + auto status = device->CreateTexture2D(&t, nullptr, &img->capture_texture); if (FAILED(status)) { BOOST_LOG(error) << "Failed to create img buf texture [0x"sv << util::hex(status).to_string_view() << ']'; return -1; @@ -1464,82 +1613,145 @@ namespace platf::dxgi { }; } - std::shared_ptr - display_vram_t::make_hwdevice(pix_fmt_e pix_fmt) { - if (pix_fmt != platf::pix_fmt_e::nv12 && pix_fmt != platf::pix_fmt_e::p010) { - BOOST_LOG(error) << "display_vram_t doesn't support pixel format ["sv << from_pix_fmt(pix_fmt) << ']'; - - return nullptr; - } + /** + * @brief Checks that a given codec is supported by the display device. + * @param name The FFmpeg codec name (or similar for non-FFmpeg codecs). + * @param config The codec configuration. + * @return true if supported, false otherwise. + */ + bool + display_vram_t::is_codec_supported(std::string_view name, const ::video::config_t &config) { + DXGI_ADAPTER_DESC adapter_desc; + adapter->GetDesc(&adapter_desc); - auto hwdevice = std::make_shared(); + if (adapter_desc.VendorId == 0x1002) { // AMD + // If it's not an AMF encoder, it's not compatible with an AMD GPU + if (!boost::algorithm::ends_with(name, "_amf")) { + return false; + } - auto ret = hwdevice->init( - shared_from_this(), - adapter.get(), - pix_fmt); + // Perform AMF version checks if we're using an AMD GPU. This check is placed in display_vram_t + // to avoid hitting the display_ram_t path which uses software encoding and doesn't touch AMF. + HMODULE amfrt = LoadLibraryW(AMF_DLL_NAME); + if (amfrt) { + auto unload_amfrt = util::fail_guard([amfrt]() { + FreeLibrary(amfrt); + }); - if (ret) { - return nullptr; + auto fnAMFQueryVersion = (AMFQueryVersion_Fn) GetProcAddress(amfrt, AMF_QUERY_VERSION_FUNCTION_NAME); + if (fnAMFQueryVersion) { + amf_uint64 version; + auto result = fnAMFQueryVersion(&version); + if (result == AMF_OK) { + if (config.videoFormat == 2 && version < AMF_MAKE_FULL_VERSION(1, 4, 30, 0)) { + // AMF 1.4.30 adds ultra low latency mode for AV1. Don't use AV1 on earlier versions. + // This corresponds to driver version 23.5.2 (23.10.01.45) or newer. + BOOST_LOG(warning) << "AV1 encoding is disabled on AMF version "sv + << AMF_GET_MAJOR_VERSION(version) << '.' + << AMF_GET_MINOR_VERSION(version) << '.' + << AMF_GET_SUBMINOR_VERSION(version) << '.' + << AMF_GET_BUILD_VERSION(version); + BOOST_LOG(warning) << "If your AMD GPU supports AV1 encoding, update your graphics drivers!"sv; + return false; + } + else if (config.dynamicRange && version < AMF_MAKE_FULL_VERSION(1, 4, 23, 0)) { + // Older versions of the AMD AMF runtime can crash when fed P010 surfaces. + // Fail if AMF version is below 1.4.23 where HEVC Main10 encoding was introduced. + // AMF 1.4.23 corresponds to driver version 21.12.1 (21.40.11.03) or newer. + BOOST_LOG(warning) << "HDR encoding is disabled on AMF version "sv + << AMF_GET_MAJOR_VERSION(version) << '.' + << AMF_GET_MINOR_VERSION(version) << '.' + << AMF_GET_SUBMINOR_VERSION(version) << '.' + << AMF_GET_BUILD_VERSION(version); + BOOST_LOG(warning) << "If your AMD GPU supports HEVC Main10 encoding, update your graphics drivers!"sv; + return false; + } + } + else { + BOOST_LOG(warning) << "AMFQueryVersion() failed: "sv << result; + } + } + else { + BOOST_LOG(warning) << "AMF DLL missing export: "sv << AMF_QUERY_VERSION_FUNCTION_NAME; + } + } + else { + BOOST_LOG(warning) << "Detected AMD GPU but AMF failed to load"sv; + } + } + else if (adapter_desc.VendorId == 0x8086) { // Intel + // If it's not a QSV encoder, it's not compatible with an Intel GPU + if (!boost::algorithm::ends_with(name, "_qsv")) { + return false; + } + } + else if (adapter_desc.VendorId == 0x10de) { // Nvidia + // If it's not an NVENC encoder, it's not compatible with an Nvidia GPU + if (!boost::algorithm::ends_with(name, "_nvenc")) { + return false; + } + } + else { + BOOST_LOG(warning) << "Unknown GPU vendor ID: " << util::hex(adapter_desc.VendorId).to_string_view(); } - return hwdevice; + return true; } - int - init() { - BOOST_LOG(info) << "Compiling shaders..."sv; - scene_vs_hlsl = compile_vertex_shader(SUNSHINE_SHADERS_DIR "/SceneVS.hlsl"); - if (!scene_vs_hlsl) { - return -1; - } + std::unique_ptr + display_vram_t::make_avcodec_encode_device(pix_fmt_e pix_fmt) { + if (pix_fmt != platf::pix_fmt_e::nv12 && pix_fmt != platf::pix_fmt_e::p010) { + BOOST_LOG(error) << "display_vram_t doesn't support pixel format ["sv << from_pix_fmt(pix_fmt) << ']'; - convert_Y_ps_hlsl = compile_pixel_shader(SUNSHINE_SHADERS_DIR "/ConvertYPS.hlsl"); - if (!convert_Y_ps_hlsl) { - return -1; + return nullptr; } - convert_Y_PQ_ps_hlsl = compile_pixel_shader(SUNSHINE_SHADERS_DIR "/ConvertYPS_PQ.hlsl"); - if (!convert_Y_PQ_ps_hlsl) { - return -1; - } + auto device = std::make_unique(); - convert_Y_linear_ps_hlsl = compile_pixel_shader(SUNSHINE_SHADERS_DIR "/ConvertYPS_Linear.hlsl"); - if (!convert_Y_linear_ps_hlsl) { - return -1; - } + auto ret = device->init(shared_from_this(), adapter.get(), pix_fmt); - convert_UV_ps_hlsl = compile_pixel_shader(SUNSHINE_SHADERS_DIR "/ConvertUVPS.hlsl"); - if (!convert_UV_ps_hlsl) { - return -1; + if (ret) { + return nullptr; } - convert_UV_PQ_ps_hlsl = compile_pixel_shader(SUNSHINE_SHADERS_DIR "/ConvertUVPS_PQ.hlsl"); - if (!convert_UV_PQ_ps_hlsl) { - return -1; - } + return device; + } - convert_UV_linear_ps_hlsl = compile_pixel_shader(SUNSHINE_SHADERS_DIR "/ConvertUVPS_Linear.hlsl"); - if (!convert_UV_linear_ps_hlsl) { - return -1; + std::unique_ptr + display_vram_t::make_nvenc_encode_device(pix_fmt_e pix_fmt) { + auto device = std::make_unique(); + if (!device->init_device(shared_from_this(), adapter.get(), pix_fmt)) { + return nullptr; } + return device; + } - convert_UV_vs_hlsl = compile_vertex_shader(SUNSHINE_SHADERS_DIR "/ConvertUVVS.hlsl"); - if (!convert_UV_vs_hlsl) { - return -1; - } + int + init() { + BOOST_LOG(info) << "Compiling shaders..."sv; - scene_ps_hlsl = compile_pixel_shader(SUNSHINE_SHADERS_DIR "/ScenePS.hlsl"); - if (!scene_ps_hlsl) { - return -1; - } +#define compile_vertex_shader_helper(x) \ + if (!(x##_hlsl = compile_vertex_shader(SUNSHINE_SHADERS_DIR "/" #x ".hlsl"))) return -1; +#define compile_pixel_shader_helper(x) \ + if (!(x##_hlsl = compile_pixel_shader(SUNSHINE_SHADERS_DIR "/" #x ".hlsl"))) return -1; + + compile_pixel_shader_helper(convert_yuv420_packed_uv_type0_ps); + compile_pixel_shader_helper(convert_yuv420_packed_uv_type0_ps_linear); + compile_pixel_shader_helper(convert_yuv420_packed_uv_type0_ps_perceptual_quantizer); + compile_vertex_shader_helper(convert_yuv420_packed_uv_type0_vs); + compile_pixel_shader_helper(convert_yuv420_planar_y_ps); + compile_pixel_shader_helper(convert_yuv420_planar_y_ps_linear); + compile_pixel_shader_helper(convert_yuv420_planar_y_ps_perceptual_quantizer); + compile_vertex_shader_helper(convert_yuv420_planar_y_vs); + compile_pixel_shader_helper(cursor_ps); + compile_pixel_shader_helper(cursor_ps_normalize_white); + compile_vertex_shader_helper(cursor_vs); - scene_NW_ps_hlsl = compile_pixel_shader(SUNSHINE_SHADERS_DIR "/ScenePS_NW.hlsl"); - if (!scene_NW_ps_hlsl) { - return -1; - } BOOST_LOG(info) << "Compiled shaders"sv; +#undef compile_vertex_shader_helper +#undef compile_pixel_shader_helper + return 0; } } // namespace platf::dxgi diff --git a/src/platform/windows/display_wgc.cpp b/src/platform/windows/display_wgc.cpp new file mode 100644 index 00000000000..b77c600b8f4 --- /dev/null +++ b/src/platform/windows/display_wgc.cpp @@ -0,0 +1,325 @@ +/** + * @file src/platform/windows/display_wgc.cpp + * @brief WinRT Windows.Graphics.Capture API + */ +#include + +#include "display.h" + +#include "misc.h" +#include "src/logging.h" + +#include +#include +#include + +namespace platf { + using namespace std::literals; +} + +namespace winrt { + using namespace Windows::Foundation; + using namespace Windows::Graphics::Capture; + using namespace Windows::Graphics::DirectX::Direct3D11; + + extern "C" { + HRESULT __stdcall CreateDirect3D11DeviceFromDXGIDevice(::IDXGIDevice *dxgiDevice, ::IInspectable **graphicsDevice); + } + + /* Windows structures sometimes have compile-time GUIDs. GCC supports this, but in a roundabout way. + * If WINRT_IMPL_HAS_DECLSPEC_UUID is true, then the compiler supports adding this attribute to a struct. For example, Visual Studio. + * If not, then MinGW GCC has a workaround to assign a GUID to a structure. + */ + struct +#if WINRT_IMPL_HAS_DECLSPEC_UUID + __declspec(uuid("A9B3D012-3DF2-4EE3-B8D1-8695F457D3C1")) +#endif + IDirect3DDxgiInterfaceAccess: ::IUnknown { + virtual HRESULT __stdcall GetInterface(REFIID id, void **object) = 0; + }; +} // namespace winrt +#if !WINRT_IMPL_HAS_DECLSPEC_UUID +static constexpr GUID GUID__IDirect3DDxgiInterfaceAccess = { + 0xA9B3D012, 0x3DF2, 0x4EE3, { 0xB8, 0xD1, 0x86, 0x95, 0xF4, 0x57, 0xD3, 0xC1 } + // compare with __declspec(uuid(...)) for the struct above. +}; +template <> +constexpr auto +__mingw_uuidof() -> GUID const & { + return GUID__IDirect3DDxgiInterfaceAccess; +} +#endif + +namespace platf::dxgi { + wgc_capture_t::wgc_capture_t() { + InitializeConditionVariable(&frame_present_cv); + } + + wgc_capture_t::~wgc_capture_t() { + if (capture_session) + capture_session.Close(); + if (frame_pool) + frame_pool.Close(); + item = nullptr; + capture_session = nullptr; + frame_pool = nullptr; + } + + /** + * Initialize the Windows.Graphics.Capture backend. + * @return 0 on success + */ + int + wgc_capture_t::init(display_base_t *display, const ::video::config_t &config) { + HRESULT status; + dxgi::dxgi_t dxgi; + winrt::com_ptr<::IInspectable> d3d_comhandle; + try { + if (!winrt::GraphicsCaptureSession::IsSupported()) { + BOOST_LOG(error) << "Screen capture is not supported on this device for this release of Windows!"sv; + return -1; + } + if (FAILED(status = display->device->QueryInterface(IID_IDXGIDevice, (void **) &dxgi))) { + BOOST_LOG(error) << "Failed to query DXGI interface from device [0x"sv << util::hex(status).to_string_view() << ']'; + return -1; + } + if (FAILED(status = winrt::CreateDirect3D11DeviceFromDXGIDevice(*&dxgi, d3d_comhandle.put()))) { + BOOST_LOG(error) << "Failed to query WinRT DirectX interface from device [0x"sv << util::hex(status).to_string_view() << ']'; + return -1; + } + } + catch (winrt::hresult_error &e) { + BOOST_LOG(error) << "Screen capture is not supported on this device for this release of Windows: failed to acquire device: [0x"sv << util::hex(e.code()).to_string_view() << ']'; + return -1; + } + + DXGI_OUTPUT_DESC output_desc; + uwp_device = d3d_comhandle.as(); + display->output->GetDesc(&output_desc); + + auto monitor_factory = winrt::get_activation_factory(); + if (monitor_factory == nullptr || + FAILED(status = monitor_factory->CreateForMonitor(output_desc.Monitor, winrt::guid_of(), winrt::put_abi(item)))) { + BOOST_LOG(error) << "Screen capture is not supported on this device for this release of Windows: failed to acquire display: [0x"sv << util::hex(status).to_string_view() << ']'; + return -1; + } + + if (config.dynamicRange) + display->capture_format = DXGI_FORMAT_R16G16B16A16_FLOAT; + else + display->capture_format = DXGI_FORMAT_B8G8R8A8_UNORM; + + try { + frame_pool = winrt::Direct3D11CaptureFramePool::CreateFreeThreaded(uwp_device, static_cast(display->capture_format), 2, item.Size()); + capture_session = frame_pool.CreateCaptureSession(item); + frame_pool.FrameArrived({ this, &wgc_capture_t::on_frame_arrived }); + } + catch (winrt::hresult_error &e) { + BOOST_LOG(error) << "Screen capture is not supported on this device for this release of Windows: failed to create capture session: [0x"sv << util::hex(e.code()).to_string_view() << ']'; + return -1; + } + try { + capture_session.IsBorderRequired(false); + } + catch (winrt::hresult_error &e) { + BOOST_LOG(warning) << "Screen capture may not be fully supported on this device for this release of Windows: failed to disable border around capture area: [0x"sv << util::hex(e.code()).to_string_view() << ']'; + } + try { + capture_session.StartCapture(); + } + catch (winrt::hresult_error &e) { + BOOST_LOG(error) << "Screen capture is not supported on this device for this release of Windows: failed to start capture: [0x"sv << util::hex(e.code()).to_string_view() << ']'; + return -1; + } + return 0; + } + + /** + * This function runs in a separate thread spawned by the frame pool and is a producer of frames. + * To maintain parity with the original display interface, this frame will be consumed by the capture thread. + * Acquire a read-write lock, make the produced frame available to the capture thread, then wake the capture thread. + */ + void + wgc_capture_t::on_frame_arrived(winrt::Direct3D11CaptureFramePool const &sender, winrt::IInspectable const &) { + winrt::Windows::Graphics::Capture::Direct3D11CaptureFrame frame { nullptr }; + try { + frame = sender.TryGetNextFrame(); + } + catch (winrt::hresult_error &e) { + BOOST_LOG(warning) << "Failed to capture frame: "sv << e.code(); + return; + } + if (frame != nullptr) { + AcquireSRWLockExclusive(&frame_lock); + if (produced_frame) + produced_frame.Close(); + + produced_frame = frame; + ReleaseSRWLockExclusive(&frame_lock); + WakeConditionVariable(&frame_present_cv); + } + } + + /** + * Get the next frame from the producer thread. + * If not available, the capture thread blocks until one is, or the wait times out. + * @param timeout how long to wait for the next frame + * @param out a texture containing the frame just captured + * @param out_time the timestamp of the frame just captured + */ + capture_e + wgc_capture_t::next_frame(std::chrono::milliseconds timeout, ID3D11Texture2D **out, uint64_t &out_time) { + // this CONSUMER runs in the capture thread + release_frame(); + + AcquireSRWLockExclusive(&frame_lock); + if (produced_frame == nullptr && SleepConditionVariableSRW(&frame_present_cv, &frame_lock, timeout.count(), 0) == 0) { + ReleaseSRWLockExclusive(&frame_lock); + if (GetLastError() == ERROR_TIMEOUT) + return capture_e::timeout; + else + return capture_e::error; + } + if (produced_frame) { + consumed_frame = produced_frame; + produced_frame = nullptr; + } + ReleaseSRWLockExclusive(&frame_lock); + if (consumed_frame == nullptr) // spurious wakeup + return capture_e::timeout; + + auto capture_access = consumed_frame.Surface().as(); + if (capture_access == nullptr) + return capture_e::error; + capture_access->GetInterface(IID_ID3D11Texture2D, (void **) out); + out_time = consumed_frame.SystemRelativeTime().count(); // raw ticks from query performance counter + return capture_e::ok; + } + + capture_e + wgc_capture_t::release_frame() { + if (consumed_frame != nullptr) { + consumed_frame.Close(); + consumed_frame = nullptr; + } + return capture_e::ok; + } + + int + wgc_capture_t::set_cursor_visible(bool x) { + try { + if (capture_session.IsCursorCaptureEnabled() != x) + capture_session.IsCursorCaptureEnabled(x); + return 0; + } + catch (winrt::hresult_error &) { + return -1; + } + } + + int + display_wgc_ram_t::init(const ::video::config_t &config, const std::string &display_name) { + if (display_base_t::init(config, display_name) || dup.init(this, config)) + return -1; + + texture.reset(); + return 0; + } + + /** + * Get the next frame from the Windows.Graphics.Capture API and copy it into a new snapshot texture. + * @param pull_free_image_cb call this to get a new free image from the video subsystem. + * @param img_out the captured frame is returned here + * @param timeout how long to wait for the next frame + * @param cursor_visible + */ + capture_e + display_wgc_ram_t::snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor_visible) { + HRESULT status; + texture2d_t src; + uint64_t frame_qpc; + dup.set_cursor_visible(cursor_visible); + auto capture_status = dup.next_frame(timeout, &src, frame_qpc); + if (capture_status != capture_e::ok) + return capture_status; + + auto frame_timestamp = std::chrono::steady_clock::now() - qpc_time_difference(qpc_counter(), frame_qpc); + D3D11_TEXTURE2D_DESC desc; + src->GetDesc(&desc); + + // Create the staging texture if it doesn't exist. It should match the source in size and format. + if (texture == nullptr) { + capture_format = desc.Format; + BOOST_LOG(info) << "Capture format ["sv << dxgi_format_to_string(capture_format) << ']'; + + D3D11_TEXTURE2D_DESC t {}; + t.Width = width; + t.Height = height; + t.MipLevels = 1; + t.ArraySize = 1; + t.SampleDesc.Count = 1; + t.Usage = D3D11_USAGE_STAGING; + t.Format = capture_format; + t.CPUAccessFlags = D3D11_CPU_ACCESS_READ; + + auto status = device->CreateTexture2D(&t, nullptr, &texture); + + if (FAILED(status)) { + BOOST_LOG(error) << "Failed to create staging texture [0x"sv << util::hex(status).to_string_view() << ']'; + return capture_e::error; + } + } + + // It's possible for our display enumeration to race with mode changes and result in + // mismatched image pool and desktop texture sizes. If this happens, just reinit again. + if (desc.Width != width || desc.Height != height) { + BOOST_LOG(info) << "Capture size changed ["sv << width << 'x' << height << " -> "sv << desc.Width << 'x' << desc.Height << ']'; + return capture_e::reinit; + } + // It's also possible for the capture format to change on the fly. If that happens, + // reinitialize capture to try format detection again and create new images. + if (capture_format != desc.Format) { + BOOST_LOG(info) << "Capture format changed ["sv << dxgi_format_to_string(capture_format) << " -> "sv << dxgi_format_to_string(desc.Format) << ']'; + return capture_e::reinit; + } + + // Copy from GPU to CPU + device_ctx->CopyResource(texture.get(), src.get()); + + if (!pull_free_image_cb(img_out)) { + return capture_e::interrupted; + } + auto img = (img_t *) img_out.get(); + + // Map the staging texture for CPU access (making it inaccessible for the GPU) + if (FAILED(status = device_ctx->Map(texture.get(), 0, D3D11_MAP_READ, 0, &img_info))) { + BOOST_LOG(error) << "Failed to map texture [0x"sv << util::hex(status).to_string_view() << ']'; + + return capture_e::error; + } + + // Now that we know the capture format, we can finish creating the image + if (complete_img(img, false)) { + device_ctx->Unmap(texture.get(), 0); + img_info.pData = nullptr; + return capture_e::error; + } + + std::copy_n((std::uint8_t *) img_info.pData, height * img_info.RowPitch, (std::uint8_t *) img->data); + + // Unmap the staging texture to allow GPU access again + device_ctx->Unmap(texture.get(), 0); + img_info.pData = nullptr; + + if (img) { + img->frame_timestamp = frame_timestamp; + } + + return capture_e::ok; + } + + capture_e + display_wgc_ram_t::release_snapshot() { + return dup.release_frame(); + } +} // namespace platf::dxgi diff --git a/src/platform/windows/input.cpp b/src/platform/windows/input.cpp index c94904267b3..dfc9852f586 100644 --- a/src/platform/windows/input.cpp +++ b/src/platform/windows/input.cpp @@ -2,17 +2,31 @@ * @file src/platform/windows/input.cpp * @brief todo */ +#define WINVER 0x0A00 #include #include +#include #include +#include "keylayout.h" #include "misc.h" #include "src/config.h" -#include "src/main.h" +#include "src/globals.h" +#include "src/logging.h" #include "src/platform/common.h" +#ifdef __MINGW32__ +DECLARE_HANDLE(HSYNTHETICPOINTERDEVICE); +WINUSERAPI HSYNTHETICPOINTERDEVICE WINAPI +CreateSyntheticPointerDevice(POINTER_INPUT_TYPE pointerType, ULONG maxCount, POINTER_FEEDBACK_MODE mode); +WINUSERAPI BOOL WINAPI +InjectSyntheticPointerInput(HSYNTHETICPOINTERDEVICE device, CONST POINTER_TYPE_INFO *pointerInfo, UINT32 count); +WINUSERAPI VOID WINAPI +DestroySyntheticPointerDevice(HSYNTHETICPOINTERDEVICE device); +#endif + namespace platf { using namespace std::literals; @@ -26,15 +40,6 @@ namespace platf { using client_t = util::safe_ptr<_VIGEM_CLIENT_T, vigem_free>; using target_t = util::safe_ptr<_VIGEM_TARGET_T, vigem_target_free>; - static VIGEM_TARGET_TYPE - map(const std::string_view &gp) { - if (gp == "x360"sv) { - return Xbox360Wired; - } - - return DualShock4Wired; - } - void CALLBACK x360_notify( client_t::pointer client, @@ -51,19 +56,154 @@ namespace platf { DS4_LIGHTBAR_COLOR /* led_color */, void *userdata); + struct gp_touch_context_t { + uint8_t pointerIndex; + uint16_t x; + uint16_t y; + }; + + struct gamepad_context_t { + target_t gp; + feedback_queue_t feedback_queue; + + union { + XUSB_REPORT x360; + DS4_REPORT_EX ds4; + } report; + + // Map from pointer ID to pointer index + std::map pointer_id_map; + uint8_t available_pointers; + + uint8_t client_relative_index; + + thread_pool_util::ThreadPool::task_id_t repeat_task {}; + std::chrono::steady_clock::time_point last_report_ts; + + gamepad_feedback_msg_t last_rumble; + gamepad_feedback_msg_t last_rgb_led; + }; + + constexpr float EARTH_G = 9.80665f; + +#define MPS2_TO_DS4_ACCEL(x) (int32_t)(((x) / EARTH_G) * 8192) +#define DPS_TO_DS4_GYRO(x) (int32_t)((x) * (1024 / 64)) + +#define APPLY_CALIBRATION(val, bias, scale) (int32_t)(((float) (val) + (bias)) / (scale)) + + constexpr DS4_TOUCH ds4_touch_unused = { + .bPacketCounter = 0, + .bIsUpTrackingNum1 = 0x80, + .bTouchData1 = { 0x00, 0x00, 0x00 }, + .bIsUpTrackingNum2 = 0x80, + .bTouchData2 = { 0x00, 0x00, 0x00 }, + }; + + // See https://github.com/ViGEm/ViGEmBus/blob/22835473d17fbf0c4d4bb2f2d42fd692b6e44df4/sys/Ds4Pdo.cpp#L153-L164 + constexpr DS4_REPORT_EX ds4_report_init_ex = { + { { .bThumbLX = 0x80, + .bThumbLY = 0x80, + .bThumbRX = 0x80, + .bThumbRY = 0x80, + .wButtons = DS4_BUTTON_DPAD_NONE, + .bSpecial = 0, + .bTriggerL = 0, + .bTriggerR = 0, + .wTimestamp = 0, + .bBatteryLvl = 0xFF, + .wGyroX = 0, + .wGyroY = 0, + .wGyroZ = 0, + .wAccelX = 0, + .wAccelY = 0, + .wAccelZ = 0, + ._bUnknown1 = { 0x00, 0x00, 0x00, 0x00, 0x00 }, + .bBatteryLvlSpecial = 0x1A, // Wired - Full battery + ._bUnknown2 = { 0x00, 0x00 }, + .bTouchPacketsN = 1, + .sCurrentTouch = ds4_touch_unused, + .sPreviousTouch = { ds4_touch_unused, ds4_touch_unused } } } + }; + + /** + * @brief Updates the DS4 input report with the provided motion data. + * @details Acceleration is in m/s^2 and gyro is in deg/s. + * @param gamepad The gamepad to update. + * @param motion_type The type of motion data. + * @param x X component of motion. + * @param y Y component of motion. + * @param z Z component of motion. + */ + static void + ds4_update_motion(gamepad_context_t &gamepad, uint8_t motion_type, float x, float y, float z) { + auto &report = gamepad.report.ds4.Report; + + // Use int32 to process this data, so we can clamp if needed. + int32_t intX, intY, intZ; + + switch (motion_type) { + case LI_MOTION_TYPE_ACCEL: + // Convert to the DS4's accelerometer scale + intX = MPS2_TO_DS4_ACCEL(x); + intY = MPS2_TO_DS4_ACCEL(y); + intZ = MPS2_TO_DS4_ACCEL(z); + + // Apply the inverse of ViGEmBus's calibration data + intX = APPLY_CALIBRATION(intX, -297, 1.010796f); + intY = APPLY_CALIBRATION(intY, -42, 1.014614f); + intZ = APPLY_CALIBRATION(intZ, -512, 1.024768f); + break; + case LI_MOTION_TYPE_GYRO: + // Convert to the DS4's gyro scale + intX = DPS_TO_DS4_GYRO(x); + intY = DPS_TO_DS4_GYRO(y); + intZ = DPS_TO_DS4_GYRO(z); + + // Apply the inverse of ViGEmBus's calibration data + intX = APPLY_CALIBRATION(intX, 1, 0.977596f); + intY = APPLY_CALIBRATION(intY, 0, 0.972370f); + intZ = APPLY_CALIBRATION(intZ, 0, 0.971550f); + break; + default: + return; + } + + // Clamp the values to the range of the data type + intX = std::clamp(intX, INT16_MIN, INT16_MAX); + intY = std::clamp(intY, INT16_MIN, INT16_MAX); + intZ = std::clamp(intZ, INT16_MIN, INT16_MAX); + + // Populate the report + switch (motion_type) { + case LI_MOTION_TYPE_ACCEL: + report.wAccelX = (int16_t) intX; + report.wAccelY = (int16_t) intY; + report.wAccelZ = (int16_t) intZ; + break; + case LI_MOTION_TYPE_GYRO: + report.wGyroX = (int16_t) intX; + report.wGyroY = (int16_t) intY; + report.wGyroZ = (int16_t) intZ; + break; + default: + return; + } + } + class vigem_t { public: int init() { - VIGEM_ERROR status; - - client.reset(vigem_alloc()); - - status = vigem_connect(client.get()); + // Probe ViGEm during startup to see if we can successfully attach gamepads. This will allow us to + // immediately display the error message in the web UI even before the user tries to stream. + client_t client { vigem_alloc() }; + VIGEM_ERROR status = vigem_connect(client.get()); if (!VIGEM_SUCCESS(status)) { - BOOST_LOG(warning) << "Couldn't setup connection to ViGEm for gamepad support ["sv << util::hex(status).to_string_view() << ']'; - - return -1; + // Log a special fatal message for this case to show the error in the web UI + BOOST_LOG(fatal) << "ViGEmBus is not installed or running. You must install ViGEmBus for gamepad support!"sv; + } + else { + vigem_disconnect(client.get()); } gamepads.resize(MAX_GAMEPADS); @@ -71,32 +211,70 @@ namespace platf { return 0; } + /** + * @brief Attaches a new gamepad. + * @param id The gamepad ID. + * @param feedback_queue The queue for posting messages back to the client. + * @param gp_type The type of gamepad. + * @return 0 on success. + */ int - alloc_gamepad_interal(int nr, rumble_queue_t &rumble_queue, VIGEM_TARGET_TYPE gp_type) { - auto &[rumble, gp] = gamepads[nr]; - assert(!gp); + alloc_gamepad_internal(const gamepad_id_t &id, feedback_queue_t &feedback_queue, VIGEM_TARGET_TYPE gp_type) { + auto &gamepad = gamepads[id.globalIndex]; + assert(!gamepad.gp); + + gamepad.client_relative_index = id.clientRelativeIndex; + gamepad.last_report_ts = std::chrono::steady_clock::now(); + + // Establish a connect to the ViGEm driver if we don't have one yet + if (!client) { + BOOST_LOG(debug) << "Connecting to ViGEmBus driver"sv; + client.reset(vigem_alloc()); + + auto status = vigem_connect(client.get()); + if (!VIGEM_SUCCESS(status)) { + BOOST_LOG(warning) << "Couldn't setup connection to ViGEm for gamepad support ["sv << util::hex(status).to_string_view() << ']'; + client.reset(); + return -1; + } + } if (gp_type == Xbox360Wired) { - gp.reset(vigem_target_x360_alloc()); + gamepad.gp.reset(vigem_target_x360_alloc()); + XUSB_REPORT_INIT(&gamepad.report.x360); } else { - gp.reset(vigem_target_ds4_alloc()); + gamepad.gp.reset(vigem_target_ds4_alloc()); + + // There is no equivalent DS4_REPORT_EX_INIT() + gamepad.report.ds4 = ds4_report_init_ex; + + // Set initial accelerometer and gyro state + ds4_update_motion(gamepad, LI_MOTION_TYPE_ACCEL, 0.0f, EARTH_G, 0.0f); + ds4_update_motion(gamepad, LI_MOTION_TYPE_GYRO, 0.0f, 0.0f, 0.0f); + + // Request motion events from the client at 100 Hz + feedback_queue->raise(gamepad_feedback_msg_t::make_motion_event_state(gamepad.client_relative_index, LI_MOTION_TYPE_ACCEL, 100)); + feedback_queue->raise(gamepad_feedback_msg_t::make_motion_event_state(gamepad.client_relative_index, LI_MOTION_TYPE_GYRO, 100)); + + // We support pointer index 0 and 1 + gamepad.available_pointers = 0x3; } - auto status = vigem_target_add(client.get(), gp.get()); + auto status = vigem_target_add(client.get(), gamepad.gp.get()); if (!VIGEM_SUCCESS(status)) { BOOST_LOG(error) << "Couldn't add Gamepad to ViGEm connection ["sv << util::hex(status).to_string_view() << ']'; return -1; } - rumble = std::move(rumble_queue); + gamepad.feedback_queue = std::move(feedback_queue); if (gp_type == Xbox360Wired) { - status = vigem_target_x360_register_notification(client.get(), gp.get(), x360_notify, this); + status = vigem_target_x360_register_notification(client.get(), gamepad.gp.get(), x360_notify, this); } else { - status = vigem_target_ds4_register_notification(client.get(), gp.get(), ds4_notify, this); + status = vigem_target_ds4_register_notification(client.get(), gamepad.gp.get(), ds4_notify, this); } if (!VIGEM_SUCCESS(status)) { @@ -106,38 +284,108 @@ namespace platf { return 0; } + /** + * @brief Detaches the specified gamepad + * @param nr The gamepad. + */ void free_target(int nr) { - auto &[_, gp] = gamepads[nr]; + auto &gamepad = gamepads[nr]; + + if (gamepad.repeat_task) { + task_pool.cancel(gamepad.repeat_task); + gamepad.repeat_task = 0; + } - if (gp && vigem_target_is_attached(gp.get())) { - auto status = vigem_target_remove(client.get(), gp.get()); + if (gamepad.gp && vigem_target_is_attached(gamepad.gp.get())) { + auto status = vigem_target_remove(client.get(), gamepad.gp.get()); if (!VIGEM_SUCCESS(status)) { BOOST_LOG(warning) << "Couldn't detach gamepad from ViGEm ["sv << util::hex(status).to_string_view() << ']'; } } - gp.reset(); + gamepad.gp.reset(); + + // Disconnect from ViGEm if we just removed the last gamepad + bool disconnect = true; + for (auto &gamepad : gamepads) { + if (gamepad.gp && vigem_target_is_attached(gamepad.gp.get())) { + disconnect = false; + break; + } + } + if (disconnect) { + BOOST_LOG(debug) << "Disconnecting from ViGEmBus driver"sv; + vigem_disconnect(client.get()); + client.reset(); + } } + /** + * @brief Pass rumble data back to the client. + * @param target The gamepad. + * @param largeMotor The large motor. + * @param smallMotor The small motor. + */ void - rumble(target_t::pointer target, std::uint8_t smallMotor, std::uint8_t largeMotor) { + rumble(target_t::pointer target, std::uint8_t largeMotor, std::uint8_t smallMotor) { for (int x = 0; x < gamepads.size(); ++x) { - auto &[rumble_queue, gp] = gamepads[x]; - - if (gp.get() == target) { - rumble_queue->raise(x, ((std::uint16_t) smallMotor) << 8, ((std::uint16_t) largeMotor) << 8); + auto &gamepad = gamepads[x]; + + if (gamepad.gp.get() == target) { + // Convert from 8-bit to 16-bit values + uint16_t normalizedLargeMotor = largeMotor << 8; + uint16_t normalizedSmallMotor = smallMotor << 8; + + // Don't resend duplicate rumble data + if (normalizedSmallMotor != gamepad.last_rumble.data.rumble.highfreq || + normalizedLargeMotor != gamepad.last_rumble.data.rumble.lowfreq) { + // We have to use the client-relative index when communicating back to the client + gamepad_feedback_msg_t msg = gamepad_feedback_msg_t::make_rumble( + gamepad.client_relative_index, normalizedLargeMotor, normalizedSmallMotor); + gamepad.feedback_queue->raise(msg); + gamepad.last_rumble = msg; + } + return; + } + } + } + /** + * @brief Pass RGB LED data back to the client. + * @param target The gamepad. + * @param r The red channel. + * @param g The red channel. + * @param b The red channel. + */ + void + set_rgb_led(target_t::pointer target, std::uint8_t r, std::uint8_t g, std::uint8_t b) { + for (int x = 0; x < gamepads.size(); ++x) { + auto &gamepad = gamepads[x]; + + if (gamepad.gp.get() == target) { + // Don't resend duplicate RGB data + if (r != gamepad.last_rgb_led.data.rgb_led.r || + g != gamepad.last_rgb_led.data.rgb_led.g || + b != gamepad.last_rgb_led.data.rgb_led.b) { + // We have to use the client-relative index when communicating back to the client + gamepad_feedback_msg_t msg = gamepad_feedback_msg_t::make_rgb_led(gamepad.client_relative_index, r, g, b); + gamepad.feedback_queue->raise(msg); + gamepad.last_rgb_led = msg; + } return; } } } + /** + * @brief vigem_t destructor. + */ ~vigem_t() { if (client) { - for (auto &[_, gp] : gamepads) { - if (gp && vigem_target_is_attached(gp.get())) { - auto status = vigem_target_remove(client.get(), gp.get()); + for (auto &gamepad : gamepads) { + if (gamepad.gp && vigem_target_is_attached(gamepad.gp.get())) { + auto status = vigem_target_remove(client.get(), gamepad.gp.get()); if (!VIGEM_SUCCESS(status)) { BOOST_LOG(warning) << "Couldn't detach gamepad from ViGEm ["sv << util::hex(status).to_string_view() << ']'; } @@ -148,7 +396,7 @@ namespace platf { } } - std::vector> gamepads; + std::vector gamepads; client_t client; }; @@ -164,7 +412,7 @@ namespace platf { << "largeMotor: "sv << (int) largeMotor << std::endl << "smallMotor: "sv << (int) smallMotor; - task_pool.push(&vigem_t::rumble, (vigem_t *) userdata, target, smallMotor, largeMotor); + task_pool.push(&vigem_t::rumble, (vigem_t *) userdata, target, largeMotor, smallMotor); } void CALLBACK @@ -172,13 +420,17 @@ namespace platf { client_t::pointer client, target_t::pointer target, std::uint8_t largeMotor, std::uint8_t smallMotor, - DS4_LIGHTBAR_COLOR /* led_color */, + DS4_LIGHTBAR_COLOR led_color, void *userdata) { BOOST_LOG(debug) << "largeMotor: "sv << (int) largeMotor << std::endl - << "smallMotor: "sv << (int) smallMotor; + << "smallMotor: "sv << (int) smallMotor << std::endl + << "LED: "sv << util::hex(led_color.Red).to_string_view() << ' ' + << util::hex(led_color.Green).to_string_view() << ' ' + << util::hex(led_color.Blue).to_string_view() << std::endl; - task_pool.push(&vigem_t::rumble, (vigem_t *) userdata, target, smallMotor, largeMotor); + task_pool.push(&vigem_t::rumble, (vigem_t *) userdata, target, largeMotor, smallMotor); + task_pool.push(&vigem_t::set_rgb_led, (vigem_t *) userdata, target, led_color.Red, led_color.Green, led_color.Blue); } struct input_raw_t { @@ -187,8 +439,10 @@ namespace platf { } vigem_t *vigem; - HKL keyboard_layout; - HKL active_layout; + + decltype(CreateSyntheticPointerDevice) *fnCreateSyntheticPointerDevice; + decltype(InjectSyntheticPointerInput) *fnInjectSyntheticPointerInput; + decltype(DestroySyntheticPointerDevice) *fnDestroySyntheticPointerDevice; }; input_t @@ -202,24 +456,18 @@ namespace platf { raw.vigem = nullptr; } - // Moonlight currently sends keys normalized to the US English layout. - // We need to use that layout when converting to scancodes. - raw.keyboard_layout = LoadKeyboardLayoutA("00000409", 0); - if (!raw.keyboard_layout || LOWORD(raw.keyboard_layout) != 0x409) { - BOOST_LOG(warning) << "Unable to load US English keyboard layout for scancode translation. Keyboard input may not work in games."sv; - raw.keyboard_layout = NULL; - } - - // Activate layout for current process only - raw.active_layout = ActivateKeyboardLayout(raw.keyboard_layout, KLF_SETFORPROCESS); - if (!raw.active_layout) { - BOOST_LOG(warning) << "Unable to activate US English keyboard layout for scancode translation. Keyboard input may not work in games."sv; - raw.keyboard_layout = NULL; - } + // Get pointers to virtual touch/pen input functions (Win10 1809+) + raw.fnCreateSyntheticPointerDevice = (decltype(CreateSyntheticPointerDevice) *) GetProcAddress(GetModuleHandleA("user32.dll"), "CreateSyntheticPointerDevice"); + raw.fnInjectSyntheticPointerInput = (decltype(InjectSyntheticPointerInput) *) GetProcAddress(GetModuleHandleA("user32.dll"), "InjectSyntheticPointerInput"); + raw.fnDestroySyntheticPointerDevice = (decltype(DestroySyntheticPointerDevice) *) GetProcAddress(GetModuleHandleA("user32.dll"), "DestroySyntheticPointerDevice"); return result; } + /** + * @brief Calls SendInput() and switches input desktops if required. + * @param i The `INPUT` struct to send. + */ void send_input(INPUT &i) { retry: @@ -234,6 +482,29 @@ namespace platf { } } + /** + * @brief Calls InjectSyntheticPointerInput() and switches input desktops if required. + * @details Must only be called if InjectSyntheticPointerInput() is available. + * @param input The global input context. + * @param device The synthetic pointer device handle. + * @param pointerInfo An array of `POINTER_TYPE_INFO` structs. + * @param count The number of elements in `pointerInfo`. + * @return true if input was successfully injected. + */ + bool + inject_synthetic_pointer_input(input_raw_t *input, HSYNTHETICPOINTERDEVICE device, const POINTER_TYPE_INFO *pointerInfo, UINT32 count) { + retry: + if (!input->fnInjectSyntheticPointerInput(device, pointerInfo, count)) { + auto hDesk = syncThreadDesktop(); + if (_lastKnownInputDesktop != hDesk) { + _lastKnownInputDesktop = hDesk; + goto retry; + } + return false; + } + return true; + } + void abs_mouse(input_t &input, const touch_port_t &touch_port, float x, float y) { INPUT i {}; @@ -273,43 +544,27 @@ namespace platf { void button_mouse(input_t &input, int button, bool release) { - constexpr auto KEY_STATE_DOWN = (SHORT) 0x8000; - INPUT i {}; i.type = INPUT_MOUSE; auto &mi = i.mi; - int mouse_button; if (button == 1) { mi.dwFlags = release ? MOUSEEVENTF_LEFTUP : MOUSEEVENTF_LEFTDOWN; - mouse_button = VK_LBUTTON; } else if (button == 2) { mi.dwFlags = release ? MOUSEEVENTF_MIDDLEUP : MOUSEEVENTF_MIDDLEDOWN; - mouse_button = VK_MBUTTON; } else if (button == 3) { mi.dwFlags = release ? MOUSEEVENTF_RIGHTUP : MOUSEEVENTF_RIGHTDOWN; - mouse_button = VK_RBUTTON; } else if (button == 4) { mi.dwFlags = release ? MOUSEEVENTF_XUP : MOUSEEVENTF_XDOWN; mi.mouseData = XBUTTON1; - mouse_button = VK_XBUTTON1; } else { mi.dwFlags = release ? MOUSEEVENTF_XUP : MOUSEEVENTF_XDOWN; mi.mouseData = XBUTTON2; - mouse_button = VK_XBUTTON2; - } - - auto key_state = GetAsyncKeyState(mouse_button); - bool key_state_down = (key_state & KEY_STATE_DOWN) != 0; - if (key_state_down != release) { - BOOST_LOG(warning) << "Button state of mouse_button ["sv << button << "] does not match the desired state"sv; - - return; } send_input(i); @@ -343,8 +598,6 @@ namespace platf { void keyboard(input_t &input, uint16_t modcode, bool release, uint8_t flags) { - auto raw = (input_raw_t *) input.get(); - INPUT i {}; i.type = INPUT_KEYBOARD; auto &ki = i.ki; @@ -352,9 +605,9 @@ namespace platf { // If the client did not normalize this VK code to a US English layout, we can't accurately convert it to a scancode. bool send_scancode = !(flags & SS_KBE_FLAG_NON_NORMALIZED) || config::input.always_send_scancodes; - if (send_scancode && modcode != VK_LWIN && modcode != VK_RWIN && modcode != VK_PAUSE && raw->keyboard_layout != NULL) { - // For some reason, MapVirtualKey(VK_LWIN, MAPVK_VK_TO_VSC) doesn't seem to work :/ - ki.wScan = MapVirtualKeyEx(modcode, MAPVK_VK_TO_VSC, raw->keyboard_layout); + if (send_scancode) { + // Mask off the extended key byte + ki.wScan = VK_TO_SCANCODE_MAP[modcode & 0xFF]; } // If we can map this to a scancode, send it as a scancode for maximum game compatibility. @@ -368,6 +621,8 @@ namespace platf { // https://docs.microsoft.com/en-us/windows/win32/inputdev/about-keyboard-input#keystroke-message-flags switch (modcode) { + case VK_LWIN: + case VK_RWIN: case VK_RMENU: case VK_RCONTROL: case VK_INSERT: @@ -381,6 +636,7 @@ namespace platf { case VK_LEFT: case VK_RIGHT: case VK_DIVIDE: + case VK_APPS: ki.dwFlags |= KEYEVENTF_EXTENDEDKEY; break; default: @@ -394,6 +650,509 @@ namespace platf { send_input(i); } + struct client_input_raw_t: public client_input_t { + client_input_raw_t(input_t &input) { + global = (input_raw_t *) input.get(); + } + + ~client_input_raw_t() override { + if (penRepeatTask) { + task_pool.cancel(penRepeatTask); + } + if (touchRepeatTask) { + task_pool.cancel(touchRepeatTask); + } + + if (pen) { + global->fnDestroySyntheticPointerDevice(pen); + } + if (touch) { + global->fnDestroySyntheticPointerDevice(touch); + } + } + + input_raw_t *global; + + // Device state and handles for pen and touch input must be stored in the per-client + // input context, because each connected client may be sending their own independent + // pen/touch events. To maintain separation, we expose separate pen and touch devices + // for each client. + + HSYNTHETICPOINTERDEVICE pen {}; + POINTER_TYPE_INFO penInfo {}; + thread_pool_util::ThreadPool::task_id_t penRepeatTask {}; + + HSYNTHETICPOINTERDEVICE touch {}; + POINTER_TYPE_INFO touchInfo[10] {}; + UINT32 activeTouchSlots {}; + thread_pool_util::ThreadPool::task_id_t touchRepeatTask {}; + }; + + /** + * @brief Allocates a context to store per-client input data. + * @param input The global input context. + * @return A unique pointer to a per-client input data context. + */ + std::unique_ptr + allocate_client_input_context(input_t &input) { + return std::make_unique(input); + } + + /** + * @brief Compacts the touch slots into a contiguous block and updates the active count. + * @details Since this swaps entries around, all slot pointers/references are invalid after compaction. + * @param raw The client-specific input context. + */ + void + perform_touch_compaction(client_input_raw_t *raw) { + // Windows requires all active touches be contiguous when fed into InjectSyntheticPointerInput(). + UINT32 i; + for (i = 0; i < ARRAYSIZE(raw->touchInfo); i++) { + if (raw->touchInfo[i].touchInfo.pointerInfo.pointerFlags == POINTER_FLAG_NONE) { + // This is an empty slot. Look for a later entry to move into this slot. + for (UINT32 j = i + 1; j < ARRAYSIZE(raw->touchInfo); j++) { + if (raw->touchInfo[j].touchInfo.pointerInfo.pointerFlags != POINTER_FLAG_NONE) { + std::swap(raw->touchInfo[i], raw->touchInfo[j]); + break; + } + } + + // If we didn't find anything, we've reached the end of active slots. + if (raw->touchInfo[i].touchInfo.pointerInfo.pointerFlags == POINTER_FLAG_NONE) { + break; + } + } + } + + // Update the number of active touch slots + raw->activeTouchSlots = i; + } + + /** + * @brief Gets a pointer slot by client-relative pointer ID, claiming a new one if necessary. + * @param raw The raw client-specific input context. + * @param pointerId The client's pointer ID. + * @param eventType The LI_TOUCH_EVENT value from the client. + * @return A pointer to the slot entry. + */ + POINTER_TYPE_INFO * + pointer_by_id(client_input_raw_t *raw, uint32_t pointerId, uint8_t eventType) { + // Compact active touches into a single contiguous block + perform_touch_compaction(raw); + + // Try to find a matching pointer ID + for (UINT32 i = 0; i < ARRAYSIZE(raw->touchInfo); i++) { + if (raw->touchInfo[i].touchInfo.pointerInfo.pointerId == pointerId && + raw->touchInfo[i].touchInfo.pointerInfo.pointerFlags != POINTER_FLAG_NONE) { + if (eventType == LI_TOUCH_EVENT_DOWN && (raw->touchInfo[i].touchInfo.pointerInfo.pointerFlags & POINTER_FLAG_INCONTACT)) { + BOOST_LOG(warning) << "Pointer "sv << pointerId << " already down. Did the client drop an up/cancel event?"sv; + } + + return &raw->touchInfo[i]; + } + } + + if (eventType != LI_TOUCH_EVENT_HOVER && eventType != LI_TOUCH_EVENT_DOWN) { + BOOST_LOG(warning) << "Unexpected new pointer "sv << pointerId << " for event "sv << (uint32_t) eventType << ". Did the client drop a down/hover event?"sv; + } + + // If there was none, grab an unused entry and increment the active slot count + for (UINT32 i = 0; i < ARRAYSIZE(raw->touchInfo); i++) { + if (raw->touchInfo[i].touchInfo.pointerInfo.pointerFlags == POINTER_FLAG_NONE) { + raw->touchInfo[i].touchInfo.pointerInfo.pointerId = pointerId; + raw->activeTouchSlots = i + 1; + return &raw->touchInfo[i]; + } + } + + return nullptr; + } + + /** + * @brief Populate common `POINTER_INFO` members shared between pen and touch events. + * @param pointerInfo The pointer info to populate. + * @param touchPort The current viewport for translating to screen coordinates. + * @param eventType The type of touch/pen event. + * @param x The normalized 0.0-1.0 X coordinate. + * @param y The normalized 0.0-1.0 Y coordinate. + */ + void + populate_common_pointer_info(POINTER_INFO &pointerInfo, const touch_port_t &touchPort, uint8_t eventType, float x, float y) { + switch (eventType) { + case LI_TOUCH_EVENT_HOVER: + pointerInfo.pointerFlags &= ~POINTER_FLAG_INCONTACT; + pointerInfo.pointerFlags |= POINTER_FLAG_INRANGE | POINTER_FLAG_UPDATE; + pointerInfo.ptPixelLocation.x = x * touchPort.width + touchPort.offset_x; + pointerInfo.ptPixelLocation.y = y * touchPort.height + touchPort.offset_y; + break; + case LI_TOUCH_EVENT_DOWN: + pointerInfo.pointerFlags |= POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT | POINTER_FLAG_DOWN; + pointerInfo.ptPixelLocation.x = x * touchPort.width + touchPort.offset_x; + pointerInfo.ptPixelLocation.y = y * touchPort.height + touchPort.offset_y; + break; + case LI_TOUCH_EVENT_UP: + // We expect to get another LI_TOUCH_EVENT_HOVER if the pointer remains in range + pointerInfo.pointerFlags &= ~(POINTER_FLAG_INCONTACT | POINTER_FLAG_INRANGE); + pointerInfo.pointerFlags |= POINTER_FLAG_UP; + break; + case LI_TOUCH_EVENT_MOVE: + pointerInfo.pointerFlags |= POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT | POINTER_FLAG_UPDATE; + pointerInfo.ptPixelLocation.x = x * touchPort.width + touchPort.offset_x; + pointerInfo.ptPixelLocation.y = y * touchPort.height + touchPort.offset_y; + break; + case LI_TOUCH_EVENT_CANCEL: + case LI_TOUCH_EVENT_CANCEL_ALL: + // If we were in contact with the touch surface at the time of the cancellation, + // we'll set POINTER_FLAG_UP, otherwise set POINTER_FLAG_UPDATE. + if (pointerInfo.pointerFlags & POINTER_FLAG_INCONTACT) { + pointerInfo.pointerFlags |= POINTER_FLAG_UP; + } + else { + pointerInfo.pointerFlags |= POINTER_FLAG_UPDATE; + } + pointerInfo.pointerFlags &= ~(POINTER_FLAG_INCONTACT | POINTER_FLAG_INRANGE); + pointerInfo.pointerFlags |= POINTER_FLAG_CANCELED; + break; + case LI_TOUCH_EVENT_HOVER_LEAVE: + pointerInfo.pointerFlags &= ~(POINTER_FLAG_INCONTACT | POINTER_FLAG_INRANGE); + pointerInfo.pointerFlags |= POINTER_FLAG_UPDATE; + break; + case LI_TOUCH_EVENT_BUTTON_ONLY: + // On Windows, we can only pass buttons if we have an active pointer + if (pointerInfo.pointerFlags != POINTER_FLAG_NONE) { + pointerInfo.pointerFlags |= POINTER_FLAG_UPDATE; + } + break; + default: + BOOST_LOG(warning) << "Unknown touch event: "sv << (uint32_t) eventType; + break; + } + } + + // Active pointer interactions sent via InjectSyntheticPointerInput() seem to be automatically + // cancelled by Windows if not repeated/updated within about a second. To avoid this, refresh + // the injected input periodically. + constexpr auto ISPI_REPEAT_INTERVAL = 50ms; + + /** + * @brief Repeats the current touch state to avoid the interactions timing out. + * @param raw The raw client-specific input context. + */ + void + repeat_touch(client_input_raw_t *raw) { + if (!inject_synthetic_pointer_input(raw->global, raw->touch, raw->touchInfo, raw->activeTouchSlots)) { + auto err = GetLastError(); + BOOST_LOG(warning) << "Failed to refresh virtual touch input: "sv << err; + } + + raw->touchRepeatTask = task_pool.pushDelayed(repeat_touch, ISPI_REPEAT_INTERVAL, raw).task_id; + } + + /** + * @brief Repeats the current pen state to avoid the interactions timing out. + * @param raw The raw client-specific input context. + */ + void + repeat_pen(client_input_raw_t *raw) { + if (!inject_synthetic_pointer_input(raw->global, raw->pen, &raw->penInfo, 1)) { + auto err = GetLastError(); + BOOST_LOG(warning) << "Failed to refresh virtual pen input: "sv << err; + } + + raw->penRepeatTask = task_pool.pushDelayed(repeat_pen, ISPI_REPEAT_INTERVAL, raw).task_id; + } + + /** + * @brief Cancels all active touches. + * @param raw The raw client-specific input context. + */ + void + cancel_all_active_touches(client_input_raw_t *raw) { + // Cancel touch repeat callbacks + if (raw->touchRepeatTask) { + task_pool.cancel(raw->touchRepeatTask); + raw->touchRepeatTask = nullptr; + } + + // Compact touches to update activeTouchSlots + perform_touch_compaction(raw); + + // If we have active slots, cancel them all + if (raw->activeTouchSlots > 0) { + for (UINT32 i = 0; i < raw->activeTouchSlots; i++) { + populate_common_pointer_info(raw->touchInfo[i].touchInfo.pointerInfo, {}, LI_TOUCH_EVENT_CANCEL_ALL, 0.0f, 0.0f); + raw->touchInfo[i].touchInfo.touchMask = TOUCH_MASK_NONE; + } + if (!inject_synthetic_pointer_input(raw->global, raw->touch, raw->touchInfo, raw->activeTouchSlots)) { + auto err = GetLastError(); + BOOST_LOG(warning) << "Failed to cancel all virtual touch input: "sv << err; + } + } + + // Zero all touch state + std::memset(raw->touchInfo, 0, sizeof(raw->touchInfo)); + raw->activeTouchSlots = 0; + } + + // These are edge-triggered pointer state flags that should always be cleared next frame + constexpr auto EDGE_TRIGGERED_POINTER_FLAGS = POINTER_FLAG_DOWN | POINTER_FLAG_UP | POINTER_FLAG_CANCELED | POINTER_FLAG_UPDATE; + + /** + * @brief Sends a touch event to the OS. + * @param input The client-specific input context. + * @param touch_port The current viewport for translating to screen coordinates. + * @param touch The touch event. + */ + void + touch(client_input_t *input, const touch_port_t &touch_port, const touch_input_t &touch) { + auto raw = (client_input_raw_t *) input; + + // Bail if we're not running on an OS that supports virtual touch input + if (!raw->global->fnCreateSyntheticPointerDevice || + !raw->global->fnInjectSyntheticPointerInput || + !raw->global->fnDestroySyntheticPointerDevice) { + BOOST_LOG(warning) << "Touch input requires Windows 10 1809 or later"sv; + return; + } + + // If there's not already a virtual touch device, create one now + if (!raw->touch) { + if (touch.eventType != LI_TOUCH_EVENT_CANCEL_ALL) { + BOOST_LOG(info) << "Creating virtual touch input device"sv; + raw->touch = raw->global->fnCreateSyntheticPointerDevice(PT_TOUCH, ARRAYSIZE(raw->touchInfo), POINTER_FEEDBACK_DEFAULT); + if (!raw->touch) { + auto err = GetLastError(); + BOOST_LOG(warning) << "Failed to create virtual touch device: "sv << err; + return; + } + } + else { + // No need to cancel anything if we had no touch input device + return; + } + } + + // Cancel touch repeat callbacks + if (raw->touchRepeatTask) { + task_pool.cancel(raw->touchRepeatTask); + raw->touchRepeatTask = nullptr; + } + + // If this is a special request to cancel all touches, do that and return + if (touch.eventType == LI_TOUCH_EVENT_CANCEL_ALL) { + cancel_all_active_touches(raw); + return; + } + + // Find or allocate an entry for this touch pointer ID + auto pointer = pointer_by_id(raw, touch.pointerId, touch.eventType); + if (!pointer) { + BOOST_LOG(error) << "No unused pointer entries! Cancelling all active touches!"sv; + cancel_all_active_touches(raw); + pointer = pointer_by_id(raw, touch.pointerId, touch.eventType); + } + + pointer->type = PT_TOUCH; + + auto &touchInfo = pointer->touchInfo; + touchInfo.pointerInfo.pointerType = PT_TOUCH; + + // Populate shared pointer info fields + populate_common_pointer_info(touchInfo.pointerInfo, touch_port, touch.eventType, touch.x, touch.y); + + touchInfo.touchMask = TOUCH_MASK_NONE; + + // Pressure and contact area only apply to in-contact pointers. + // + // The clients also pass distance and tool size for hovers, but Windows doesn't + // provide APIs to receive that data. + if (touchInfo.pointerInfo.pointerFlags & POINTER_FLAG_INCONTACT) { + if (touch.pressureOrDistance != 0.0f) { + touchInfo.touchMask |= TOUCH_MASK_PRESSURE; + + // Convert the 0.0f..1.0f float to the 0..1024 range that Windows uses + touchInfo.pressure = (UINT32) (touch.pressureOrDistance * 1024); + } + else { + // The default touch pressure is 512 + touchInfo.pressure = 512; + } + + if (touch.contactAreaMajor != 0.0f && touch.contactAreaMinor != 0.0f) { + // For the purposes of contact area calculation, we will assume the touches + // are at a 45 degree angle if rotation is unknown. This will scale the major + // axis value by width and height equally. + float rotationAngleDegs = touch.rotation == LI_ROT_UNKNOWN ? 45 : touch.rotation; + + float majorAxisAngle = rotationAngleDegs * (M_PI / 180); + float minorAxisAngle = majorAxisAngle + (M_PI / 2); + + // Estimate the contact rectangle + float contactWidth = (std::cos(majorAxisAngle) * touch.contactAreaMajor) + (std::cos(minorAxisAngle) * touch.contactAreaMinor); + float contactHeight = (std::sin(majorAxisAngle) * touch.contactAreaMajor) + (std::sin(minorAxisAngle) * touch.contactAreaMinor); + + // Convert into screen coordinates centered at the touch location and constrained by screen dimensions + touchInfo.rcContact.left = std::max(touch_port.offset_x, touchInfo.pointerInfo.ptPixelLocation.x - std::floor(contactWidth / 2)); + touchInfo.rcContact.right = std::min(touch_port.offset_x + touch_port.width, touchInfo.pointerInfo.ptPixelLocation.x + std::ceil(contactWidth / 2)); + touchInfo.rcContact.top = std::max(touch_port.offset_y, touchInfo.pointerInfo.ptPixelLocation.y - std::floor(contactHeight / 2)); + touchInfo.rcContact.bottom = std::min(touch_port.offset_y + touch_port.height, touchInfo.pointerInfo.ptPixelLocation.y + std::ceil(contactHeight / 2)); + + touchInfo.touchMask |= TOUCH_MASK_CONTACTAREA; + } + } + else { + touchInfo.pressure = 0; + touchInfo.rcContact = {}; + } + + if (touch.rotation != LI_ROT_UNKNOWN) { + touchInfo.touchMask |= TOUCH_MASK_ORIENTATION; + touchInfo.orientation = touch.rotation; + } + else { + touchInfo.orientation = 0; + } + + if (!inject_synthetic_pointer_input(raw->global, raw->touch, raw->touchInfo, raw->activeTouchSlots)) { + auto err = GetLastError(); + BOOST_LOG(warning) << "Failed to inject virtual touch input: "sv << err; + return; + } + + // Clear pointer flags that should only remain set for one frame + touchInfo.pointerInfo.pointerFlags &= ~EDGE_TRIGGERED_POINTER_FLAGS; + + // If we still have an active touch, refresh the touch state periodically + if (raw->activeTouchSlots > 1 || touchInfo.pointerInfo.pointerFlags != POINTER_FLAG_NONE) { + raw->touchRepeatTask = task_pool.pushDelayed(repeat_touch, ISPI_REPEAT_INTERVAL, raw).task_id; + } + } + + /** + * @brief Sends a pen event to the OS. + * @param input The client-specific input context. + * @param touch_port The current viewport for translating to screen coordinates. + * @param pen The pen event. + */ + void + pen(client_input_t *input, const touch_port_t &touch_port, const pen_input_t &pen) { + auto raw = (client_input_raw_t *) input; + + // Bail if we're not running on an OS that supports virtual pen input + if (!raw->global->fnCreateSyntheticPointerDevice || + !raw->global->fnInjectSyntheticPointerInput || + !raw->global->fnDestroySyntheticPointerDevice) { + BOOST_LOG(warning) << "Pen input requires Windows 10 1809 or later"sv; + return; + } + + // If there's not already a virtual pen device, create one now + if (!raw->pen) { + if (pen.eventType != LI_TOUCH_EVENT_CANCEL_ALL) { + BOOST_LOG(info) << "Creating virtual pen input device"sv; + raw->pen = raw->global->fnCreateSyntheticPointerDevice(PT_PEN, 1, POINTER_FEEDBACK_DEFAULT); + if (!raw->pen) { + auto err = GetLastError(); + BOOST_LOG(warning) << "Failed to create virtual pen device: "sv << err; + return; + } + } + else { + // No need to cancel anything if we had no pen input device + return; + } + } + + // Cancel pen repeat callbacks + if (raw->penRepeatTask) { + task_pool.cancel(raw->penRepeatTask); + raw->penRepeatTask = nullptr; + } + + raw->penInfo.type = PT_PEN; + + auto &penInfo = raw->penInfo.penInfo; + penInfo.pointerInfo.pointerType = PT_PEN; + penInfo.pointerInfo.pointerId = 0; + + // Populate shared pointer info fields + populate_common_pointer_info(penInfo.pointerInfo, touch_port, pen.eventType, pen.x, pen.y); + + // Windows only supports a single pen button, so send all buttons as the barrel button + if (pen.penButtons) { + penInfo.penFlags |= PEN_FLAG_BARREL; + } + else { + penInfo.penFlags &= ~PEN_FLAG_BARREL; + } + + switch (pen.toolType) { + default: + case LI_TOOL_TYPE_PEN: + penInfo.penFlags &= ~PEN_FLAG_ERASER; + break; + case LI_TOOL_TYPE_ERASER: + penInfo.penFlags |= PEN_FLAG_ERASER; + break; + case LI_TOOL_TYPE_UNKNOWN: + // Leave tool flags alone + break; + } + + penInfo.penMask = PEN_MASK_NONE; + + // Windows doesn't support hover distance, so only pass pressure/distance when the pointer is in contact + if ((penInfo.pointerInfo.pointerFlags & POINTER_FLAG_INCONTACT) && pen.pressureOrDistance != 0.0f) { + penInfo.penMask |= PEN_MASK_PRESSURE; + + // Convert the 0.0f..1.0f float to the 0..1024 range that Windows uses + penInfo.pressure = (UINT32) (pen.pressureOrDistance * 1024); + } + else { + // The default pen pressure is 0 + penInfo.pressure = 0; + } + + if (pen.rotation != LI_ROT_UNKNOWN) { + penInfo.penMask |= PEN_MASK_ROTATION; + penInfo.rotation = pen.rotation; + } + else { + penInfo.rotation = 0; + } + + // We require rotation and tilt to perform the conversion to X and Y tilt angles + if (pen.tilt != LI_TILT_UNKNOWN && pen.rotation != LI_ROT_UNKNOWN) { + auto rotationRads = pen.rotation * (M_PI / 180.f); + auto tiltRads = pen.tilt * (M_PI / 180.f); + auto r = std::sin(tiltRads); + auto z = std::cos(tiltRads); + + // Convert polar coordinates into X and Y tilt angles + penInfo.penMask |= PEN_MASK_TILT_X | PEN_MASK_TILT_Y; + penInfo.tiltX = (INT32) (std::atan2(std::sin(-rotationRads) * r, z) * 180.f / M_PI); + penInfo.tiltY = (INT32) (std::atan2(std::cos(-rotationRads) * r, z) * 180.f / M_PI); + } + else { + penInfo.tiltX = 0; + penInfo.tiltY = 0; + } + + if (!inject_synthetic_pointer_input(raw->global, raw->pen, &raw->penInfo, 1)) { + auto err = GetLastError(); + BOOST_LOG(warning) << "Failed to inject virtual pen input: "sv << err; + return; + } + + // Clear pointer flags that should only remain set for one frame + penInfo.pointerInfo.pointerFlags &= ~EDGE_TRIGGERED_POINTER_FLAGS; + + // If we still have an active pen interaction, refresh the pen state periodically + if (penInfo.pointerInfo.pointerFlags != POINTER_FLAG_NONE) { + raw->penRepeatTask = task_pool.pushDelayed(repeat_pen, ISPI_REPEAT_INTERVAL, raw).task_id; + } + } + void unicode(input_t &input, char *utf8, int size) { // We can do no worse than one UTF-16 character per byte of UTF-8 @@ -423,15 +1182,74 @@ namespace platf { } } + /** + * @brief Creates a new virtual gamepad. + * @param input The global input context. + * @param id The gamepad ID. + * @param metadata Controller metadata from client (empty if none provided). + * @param feedback_queue The queue for posting messages back to the client. + * @return 0 on success. + */ int - alloc_gamepad(input_t &input, int nr, rumble_queue_t rumble_queue) { + alloc_gamepad(input_t &input, const gamepad_id_t &id, const gamepad_arrival_t &metadata, feedback_queue_t feedback_queue) { auto raw = (input_raw_t *) input.get(); if (!raw->vigem) { return 0; } - return raw->vigem->alloc_gamepad_interal(nr, rumble_queue, map(config::input.gamepad)); + VIGEM_TARGET_TYPE selectedGamepadType; + + if (config::input.gamepad == "x360"sv) { + BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be Xbox 360 controller (manual selection)"sv; + selectedGamepadType = Xbox360Wired; + } + else if (config::input.gamepad == "ds4"sv) { + BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be DualShock 4 controller (manual selection)"sv; + selectedGamepadType = DualShock4Wired; + } + else if (metadata.type == LI_CTYPE_PS) { + BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be DualShock 4 controller (auto-selected by client-reported type)"sv; + selectedGamepadType = DualShock4Wired; + } + else if (metadata.type == LI_CTYPE_XBOX) { + BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be Xbox 360 controller (auto-selected by client-reported type)"sv; + selectedGamepadType = Xbox360Wired; + } + else if (config::input.motion_as_ds4 && (metadata.capabilities & (LI_CCAP_ACCEL | LI_CCAP_GYRO))) { + BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be DualShock 4 controller (auto-selected by motion sensor presence)"sv; + selectedGamepadType = DualShock4Wired; + } + else if (config::input.touchpad_as_ds4 && (metadata.capabilities & LI_CCAP_TOUCHPAD)) { + BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be DualShock 4 controller (auto-selected by touchpad presence)"sv; + selectedGamepadType = DualShock4Wired; + } + else { + BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be Xbox 360 controller (default)"sv; + selectedGamepadType = Xbox360Wired; + } + + if (selectedGamepadType == Xbox360Wired) { + if (metadata.capabilities & (LI_CCAP_ACCEL | LI_CCAP_GYRO)) { + BOOST_LOG(warning) << "Gamepad " << id.globalIndex << " has motion sensors, but they are not usable when emulating an Xbox 360 controller"sv; + } + if (metadata.capabilities & LI_CCAP_TOUCHPAD) { + BOOST_LOG(warning) << "Gamepad " << id.globalIndex << " has a touchpad, but it is not usable when emulating an Xbox 360 controller"sv; + } + if (metadata.capabilities & LI_CCAP_RGB_LED) { + BOOST_LOG(warning) << "Gamepad " << id.globalIndex << " has an RGB LED, but it is not usable when emulating an Xbox 360 controller"sv; + } + } + else if (selectedGamepadType == DualShock4Wired) { + if (!(metadata.capabilities & (LI_CCAP_ACCEL | LI_CCAP_GYRO))) { + BOOST_LOG(warning) << "Gamepad " << id.globalIndex << " is emulating a DualShock 4 controller, but the client gamepad doesn't have motion sensors active"sv; + } + if (!(metadata.capabilities & LI_CCAP_TOUCHPAD)) { + BOOST_LOG(warning) << "Gamepad " << id.globalIndex << " is emulating a DualShock 4 controller, but the client gamepad doesn't have a touchpad"sv; + } + } + + return raw->vigem->alloc_gamepad_internal(id, feedback_queue, selectedGamepadType); } void @@ -445,11 +1263,51 @@ namespace platf { raw->vigem->free_target(nr); } - static VIGEM_ERROR - x360_update(client_t::pointer client, target_t::pointer gp, const gamepad_state_t &gamepad_state) { - auto &xusb = *(PXUSB_REPORT) &gamepad_state; + /** + * @brief Converts the standard button flags into X360 format. + * @param gamepad_state The gamepad button/axis state sent from the client. + * @return XUSB_BUTTON flags. + */ + static XUSB_BUTTON + x360_buttons(const gamepad_state_t &gamepad_state) { + int buttons {}; + + auto flags = gamepad_state.buttonFlags; + if (flags & DPAD_UP) buttons |= XUSB_GAMEPAD_DPAD_UP; + if (flags & DPAD_DOWN) buttons |= XUSB_GAMEPAD_DPAD_DOWN; + if (flags & DPAD_LEFT) buttons |= XUSB_GAMEPAD_DPAD_LEFT; + if (flags & DPAD_RIGHT) buttons |= XUSB_GAMEPAD_DPAD_RIGHT; + if (flags & START) buttons |= XUSB_GAMEPAD_START; + if (flags & BACK) buttons |= XUSB_GAMEPAD_BACK; + if (flags & LEFT_STICK) buttons |= XUSB_GAMEPAD_LEFT_THUMB; + if (flags & RIGHT_STICK) buttons |= XUSB_GAMEPAD_RIGHT_THUMB; + if (flags & LEFT_BUTTON) buttons |= XUSB_GAMEPAD_LEFT_SHOULDER; + if (flags & RIGHT_BUTTON) buttons |= XUSB_GAMEPAD_RIGHT_SHOULDER; + if (flags & (HOME | MISC_BUTTON)) buttons |= XUSB_GAMEPAD_GUIDE; + if (flags & A) buttons |= XUSB_GAMEPAD_A; + if (flags & B) buttons |= XUSB_GAMEPAD_B; + if (flags & X) buttons |= XUSB_GAMEPAD_X; + if (flags & Y) buttons |= XUSB_GAMEPAD_Y; + + return (XUSB_BUTTON) buttons; + } - return vigem_target_x360_update(client, gp, xusb); + /** + * @brief Updates the X360 input report with the provided gamepad state. + * @param gamepad The gamepad to update. + * @param gamepad_state The gamepad button/axis state sent from the client. + */ + static void + x360_update_state(gamepad_context_t &gamepad, const gamepad_state_t &gamepad_state) { + auto &report = gamepad.report.x360; + + report.wButtons = x360_buttons(gamepad_state); + report.bLeftTrigger = gamepad_state.lt; + report.bRightTrigger = gamepad_state.rt; + report.sThumbLX = gamepad_state.lsX; + report.sThumbLY = gamepad_state.lsY; + report.sThumbRX = gamepad_state.rsX; + report.sThumbRY = gamepad_state.rsY; } static DS4_DPAD_DIRECTIONS @@ -490,26 +1348,29 @@ namespace platf { return DS4_BUTTON_DPAD_NONE; } + /** + * @brief Converts the standard button flags into DS4 format. + * @param gamepad_state The gamepad button/axis state sent from the client. + * @return DS4_BUTTONS flags. + */ static DS4_BUTTONS ds4_buttons(const gamepad_state_t &gamepad_state) { int buttons {}; auto flags = gamepad_state.buttonFlags; - // clang-format off - if(flags & LEFT_STICK) buttons |= DS4_BUTTON_THUMB_LEFT; - if(flags & RIGHT_STICK) buttons |= DS4_BUTTON_THUMB_RIGHT; - if(flags & LEFT_BUTTON) buttons |= DS4_BUTTON_SHOULDER_LEFT; - if(flags & RIGHT_BUTTON) buttons |= DS4_BUTTON_SHOULDER_RIGHT; - if(flags & START) buttons |= DS4_BUTTON_OPTIONS; - if(flags & BACK) buttons |= DS4_BUTTON_SHARE; - if(flags & A) buttons |= DS4_BUTTON_CROSS; - if(flags & B) buttons |= DS4_BUTTON_CIRCLE; - if(flags & X) buttons |= DS4_BUTTON_SQUARE; - if(flags & Y) buttons |= DS4_BUTTON_TRIANGLE; - - if(gamepad_state.lt > 0) buttons |= DS4_BUTTON_TRIGGER_LEFT; - if(gamepad_state.rt > 0) buttons |= DS4_BUTTON_TRIGGER_RIGHT; - // clang-format on + if (flags & LEFT_STICK) buttons |= DS4_BUTTON_THUMB_LEFT; + if (flags & RIGHT_STICK) buttons |= DS4_BUTTON_THUMB_RIGHT; + if (flags & LEFT_BUTTON) buttons |= DS4_BUTTON_SHOULDER_LEFT; + if (flags & RIGHT_BUTTON) buttons |= DS4_BUTTON_SHOULDER_RIGHT; + if (flags & START) buttons |= DS4_BUTTON_OPTIONS; + if (flags & BACK) buttons |= DS4_BUTTON_SHARE; + if (flags & A) buttons |= DS4_BUTTON_CROSS; + if (flags & B) buttons |= DS4_BUTTON_CIRCLE; + if (flags & X) buttons |= DS4_BUTTON_SQUARE; + if (flags & Y) buttons |= DS4_BUTTON_TRIANGLE; + + if (gamepad_state.lt > 0) buttons |= DS4_BUTTON_TRIGGER_LEFT; + if (gamepad_state.rt > 0) buttons |= DS4_BUTTON_TRIGGER_RIGHT; return (DS4_BUTTONS) buttons; } @@ -520,6 +1381,12 @@ namespace platf { if (gamepad_state.buttonFlags & HOME) buttons |= DS4_SPECIAL_BUTTON_PS; + // Allow either PS4/PS5 clickpad button or Xbox Series X share button to activate DS4 clickpad + if (gamepad_state.buttonFlags & (TOUCHPAD_BUTTON | MISC_BUTTON)) buttons |= DS4_SPECIAL_BUTTON_TOUCHPAD; + + // Manual DS4 emulation: check if BACK button should also trigger DS4 touchpad click + if (config::input.gamepad == "ds4"sv && config::input.ds4_back_as_touchpad_click && (gamepad_state.buttonFlags & BACK)) buttons |= DS4_SPECIAL_BUTTON_TOUCHPAD; + return (DS4_SPECIAL_BUTTONS) buttons; } @@ -535,13 +1402,16 @@ namespace platf { return new_v == 0 ? 0xFF : (std::uint8_t) new_v; } - static VIGEM_ERROR - ds4_update(client_t::pointer client, target_t::pointer gp, const gamepad_state_t &gamepad_state) { - DS4_REPORT report; + /** + * @brief Updates the DS4 input report with the provided gamepad state. + * @param gamepad The gamepad to update. + * @param gamepad_state The gamepad button/axis state sent from the client. + */ + static void + ds4_update_state(gamepad_context_t &gamepad, const gamepad_state_t &gamepad_state) { + auto &report = gamepad.report.ds4.Report; - DS4_REPORT_INIT(&report); - DS4_SET_DPAD(&report, ds4_dpad(gamepad_state)); - report.wButtons |= ds4_buttons(gamepad_state); + report.wButtons = static_cast(ds4_buttons(gamepad_state)) | static_cast(ds4_dpad(gamepad_state)); report.bSpecial = ds4_special_buttons(gamepad_state); report.bTriggerL = gamepad_state.lt; @@ -552,10 +1422,50 @@ namespace platf { report.bThumbRX = to_ds4_triggerX(gamepad_state.rsX); report.bThumbRY = to_ds4_triggerY(gamepad_state.rsY); + } + + /** + * @brief Sends DS4 input with updated timestamps and repeats to keep timestamp updated. + * @details Some applications require updated timestamps values to register DS4 input. + * @param vigem The global ViGEm context object. + * @param nr The global gamepad index. + */ + void + ds4_update_ts_and_send(vigem_t *vigem, int nr) { + auto &gamepad = vigem->gamepads[nr]; + + // Cancel any pending updates. We will requeue one here when we're finished. + if (gamepad.repeat_task) { + task_pool.cancel(gamepad.repeat_task); + gamepad.repeat_task = 0; + } + + if (gamepad.gp && vigem_target_is_attached(gamepad.gp.get())) { + auto now = std::chrono::steady_clock::now(); + auto delta_ns = std::chrono::duration_cast(now - gamepad.last_report_ts); + + // Timestamp is reported in 5.333us units + gamepad.report.ds4.Report.wTimestamp += (uint16_t) (delta_ns.count() / 5333); - return vigem_target_ds4_update(client, gp, report); + // Send the report to the virtual device + auto status = vigem_target_ds4_update_ex(vigem->client.get(), gamepad.gp.get(), gamepad.report.ds4); + if (!VIGEM_SUCCESS(status)) { + BOOST_LOG(warning) << "Couldn't send gamepad input to ViGEm ["sv << util::hex(status).to_string_view() << ']'; + return; + } + + // Repeat at least every 100ms to keep the 16-bit timestamp field from overflowing + gamepad.last_report_ts = now; + gamepad.repeat_task = task_pool.pushDelayed(ds4_update_ts_and_send, 100ms, vigem, nr).task_id; + } } + /** + * @brief Updates virtual gamepad with the provided gamepad state. + * @param input The input context. + * @param nr The gamepad index to update. + * @param gamepad_state The gamepad button/axis state sent from the client. + */ void gamepad(input_t &input, int nr, const gamepad_state_t &gamepad_state) { auto vigem = ((input_raw_t *) input.get())->vigem; @@ -565,20 +1475,241 @@ namespace platf { return; } - auto &[_, gp] = vigem->gamepads[nr]; + auto &gamepad = vigem->gamepads[nr]; + if (!gamepad.gp) { + return; + } VIGEM_ERROR status; - if (vigem_target_get_type(gp.get()) == Xbox360Wired) { - status = x360_update(vigem->client.get(), gp.get(), gamepad_state); + if (vigem_target_get_type(gamepad.gp.get()) == Xbox360Wired) { + x360_update_state(gamepad, gamepad_state); + status = vigem_target_x360_update(vigem->client.get(), gamepad.gp.get(), gamepad.report.x360); + if (!VIGEM_SUCCESS(status)) { + BOOST_LOG(warning) << "Couldn't send gamepad input to ViGEm ["sv << util::hex(status).to_string_view() << ']'; + } + } + else { + ds4_update_state(gamepad, gamepad_state); + ds4_update_ts_and_send(vigem, nr); + } + } + + /** + * @brief Sends a gamepad touch event to the OS. + * @param input The global input context. + * @param touch The touch event. + */ + void + gamepad_touch(input_t &input, const gamepad_touch_t &touch) { + auto vigem = ((input_raw_t *) input.get())->vigem; + + // If there is no gamepad support + if (!vigem) { + return; + } + + auto &gamepad = vigem->gamepads[touch.id.globalIndex]; + if (!gamepad.gp) { + return; + } + + // Touch is only supported on DualShock 4 controllers + if (vigem_target_get_type(gamepad.gp.get()) != DualShock4Wired) { + return; + } + + auto &report = gamepad.report.ds4.Report; + + uint8_t pointerIndex; + if (touch.eventType == LI_TOUCH_EVENT_DOWN) { + if (gamepad.available_pointers & 0x1) { + // Reserve pointer index 0 for this touch + gamepad.pointer_id_map[touch.pointerId] = pointerIndex = 0; + gamepad.available_pointers &= ~(1 << pointerIndex); + + // Set pointer 0 down + report.sCurrentTouch.bIsUpTrackingNum1 &= ~0x80; + report.sCurrentTouch.bIsUpTrackingNum1++; + } + else if (gamepad.available_pointers & 0x2) { + // Reserve pointer index 1 for this touch + gamepad.pointer_id_map[touch.pointerId] = pointerIndex = 1; + gamepad.available_pointers &= ~(1 << pointerIndex); + + // Set pointer 1 down + report.sCurrentTouch.bIsUpTrackingNum2 &= ~0x80; + report.sCurrentTouch.bIsUpTrackingNum2++; + } + else { + BOOST_LOG(warning) << "No more free pointer indices! Did the client miss an touch up event?"sv; + return; + } + } + else if (touch.eventType == LI_TOUCH_EVENT_CANCEL_ALL) { + // Raise both pointers + report.sCurrentTouch.bIsUpTrackingNum1 |= 0x80; + report.sCurrentTouch.bIsUpTrackingNum2 |= 0x80; + + // Remove all pointer index mappings + gamepad.pointer_id_map.clear(); + + // All pointers are now available + gamepad.available_pointers = 0x3; } else { - status = ds4_update(vigem->client.get(), gp.get(), gamepad_state); + auto i = gamepad.pointer_id_map.find(touch.pointerId); + if (i == gamepad.pointer_id_map.end()) { + BOOST_LOG(warning) << "Pointer ID not found! Did the client miss a touch down event?"sv; + return; + } + + pointerIndex = (*i).second; + + if (touch.eventType == LI_TOUCH_EVENT_UP || touch.eventType == LI_TOUCH_EVENT_CANCEL) { + // Remove the pointer index mapping + gamepad.pointer_id_map.erase(i); + + // Set pointer up + if (pointerIndex == 0) { + report.sCurrentTouch.bIsUpTrackingNum1 |= 0x80; + } + else { + report.sCurrentTouch.bIsUpTrackingNum2 |= 0x80; + } + + // Free the pointer index + gamepad.available_pointers |= (1 << pointerIndex); + } + else if (touch.eventType != LI_TOUCH_EVENT_MOVE) { + BOOST_LOG(warning) << "Unsupported touch event for gamepad: "sv << (uint32_t) touch.eventType; + return; + } + } + + // Touchpad is 1920x943 according to ViGEm + uint16_t x = touch.x * 1920; + uint16_t y = touch.y * 943; + uint8_t touchData[] = { + (uint8_t) (x & 0xFF), // Low 8 bits of X + (uint8_t) (((x >> 8) & 0x0F) | ((y & 0x0F) << 4)), // High 4 bits of X and low 4 bits of Y + (uint8_t) (((y >> 4) & 0xFF)) // High 8 bits of Y + }; + + report.sCurrentTouch.bPacketCounter++; + if (touch.eventType != LI_TOUCH_EVENT_CANCEL_ALL) { + if (pointerIndex == 0) { + memcpy(report.sCurrentTouch.bTouchData1, touchData, sizeof(touchData)); + } + else { + memcpy(report.sCurrentTouch.bTouchData2, touchData, sizeof(touchData)); + } + } + + ds4_update_ts_and_send(vigem, touch.id.globalIndex); + } + + /** + * @brief Sends a gamepad motion event to the OS. + * @param input The global input context. + * @param motion The motion event. + */ + void + gamepad_motion(input_t &input, const gamepad_motion_t &motion) { + auto vigem = ((input_raw_t *) input.get())->vigem; + + // If there is no gamepad support + if (!vigem) { + return; } - if (!VIGEM_SUCCESS(status)) { - BOOST_LOG(warning) << "Couldn't send gamepad input to ViGEm ["sv << util::hex(status).to_string_view() << ']'; + auto &gamepad = vigem->gamepads[motion.id.globalIndex]; + if (!gamepad.gp) { + return; + } + + // Motion is only supported on DualShock 4 controllers + if (vigem_target_get_type(gamepad.gp.get()) != DualShock4Wired) { + return; + } + + ds4_update_motion(gamepad, motion.motionType, motion.x, motion.y, motion.z); + ds4_update_ts_and_send(vigem, motion.id.globalIndex); + } + + /** + * @brief Sends a gamepad battery event to the OS. + * @param input The global input context. + * @param battery The battery event. + */ + void + gamepad_battery(input_t &input, const gamepad_battery_t &battery) { + auto vigem = ((input_raw_t *) input.get())->vigem; + + // If there is no gamepad support + if (!vigem) { + return; + } + + auto &gamepad = vigem->gamepads[battery.id.globalIndex]; + if (!gamepad.gp) { + return; + } + + // Battery is only supported on DualShock 4 controllers + if (vigem_target_get_type(gamepad.gp.get()) != DualShock4Wired) { + return; + } + + // For details on the report format of these battery level fields, see: + // https://github.com/torvalds/linux/blob/946c6b59c56dc6e7d8364a8959cb36bf6d10bc37/drivers/hid/hid-playstation.c#L2305-L2314 + + auto &report = gamepad.report.ds4.Report; + + // Update the battery state if it is known + switch (battery.state) { + case LI_BATTERY_STATE_CHARGING: + case LI_BATTERY_STATE_DISCHARGING: + if (battery.state == LI_BATTERY_STATE_CHARGING) { + report.bBatteryLvlSpecial |= 0x10; // Connected via USB + } + else { + report.bBatteryLvlSpecial &= ~0x10; // Not connected via USB + } + + // If there was a special battery status set before, clear that and + // initialize the battery level to 50%. It will be overwritten below + // if the actual percentage is known. + if ((report.bBatteryLvlSpecial & 0xF) > 0xA) { + report.bBatteryLvlSpecial = (report.bBatteryLvlSpecial & ~0xF) | 0x5; + } + break; + + case LI_BATTERY_STATE_FULL: + report.bBatteryLvlSpecial = 0x1B; // USB + Battery Full + report.bBatteryLvl = 0xFF; + break; + + case LI_BATTERY_STATE_NOT_PRESENT: + case LI_BATTERY_STATE_NOT_CHARGING: + report.bBatteryLvlSpecial = 0x1F; // USB + Charging Error + break; + + default: + break; } + + // Update the battery level if it is known + if (battery.percentage != LI_BATTERY_PERCENTAGE_UNKNOWN) { + report.bBatteryLvl = battery.percentage * 255 / 100; + + // Don't overwrite low nibble if there's a special status there (see above) + if ((report.bBatteryLvlSpecial & 0x10) && (report.bBatteryLvlSpecial & 0xF) <= 0xA) { + report.bBatteryLvlSpecial = (report.bBatteryLvlSpecial & ~0xF) | ((battery.percentage + 5) / 10); + } + } + + ds4_update_ts_and_send(vigem, battery.id.globalIndex); } void @@ -588,13 +1719,43 @@ namespace platf { delete input; } + /** + * @brief Gets the supported gamepads for this platform backend. + * @return Vector of gamepad type strings. + */ std::vector & supported_gamepads() { // ds4 == ps4 static std::vector gps { - "x360"sv, "ds4"sv, "ps4"sv + "auto"sv, "x360"sv, "ds4"sv, "ps4"sv }; return gps; } + + /** + * @brief Returns the supported platform capabilities to advertise to the client. + * @return Capability flags. + */ + platform_caps::caps_t + get_capabilities() { + platform_caps::caps_t caps = 0; + + // We support controller touchpad input as long as we're not emulating X360 + if (config::input.gamepad != "x360"sv) { + caps |= platform_caps::controller_touch; + } + + // We support pen and touch input on Win10 1809+ + if (GetProcAddress(GetModuleHandleA("user32.dll"), "CreateSyntheticPointerDevice") != nullptr) { + if (config::input.native_pen_touch) { + caps |= platform_caps::pen_touch; + } + } + else { + BOOST_LOG(warning) << "Touch input requires Windows 10 1809 or later"sv; + } + + return caps; + } } // namespace platf diff --git a/src/platform/windows/keylayout.h b/src/platform/windows/keylayout.h new file mode 100644 index 00000000000..55dfa284e91 --- /dev/null +++ b/src/platform/windows/keylayout.h @@ -0,0 +1,271 @@ +/** + * @file src/platform/windows/keylayout.h + * @brief Keyboard layout mapping for scancode translation + */ +#pragma once + +#include +#include + +namespace platf { + // Virtual Key to Scan Code mapping for the US English layout (00000409). + // GameStream uses this as the canonical key layout for scancode conversion. + constexpr std::array::max() + 1> VK_TO_SCANCODE_MAP { + 0, /* 0x00 */ + 0, /* 0x01 */ + 0, /* 0x02 */ + 70, /* 0x03 */ + 0, /* 0x04 */ + 0, /* 0x05 */ + 0, /* 0x06 */ + 0, /* 0x07 */ + 14, /* 0x08 */ + 15, /* 0x09 */ + 0, /* 0x0a */ + 0, /* 0x0b */ + 76, /* 0x0c */ + 28, /* 0x0d */ + 0, /* 0x0e */ + 0, /* 0x0f */ + 42, /* 0x10 */ + 29, /* 0x11 */ + 56, /* 0x12 */ + 0, /* 0x13 */ + 58, /* 0x14 */ + 0, /* 0x15 */ + 0, /* 0x16 */ + 0, /* 0x17 */ + 0, /* 0x18 */ + 0, /* 0x19 */ + 0, /* 0x1a */ + 1, /* 0x1b */ + 0, /* 0x1c */ + 0, /* 0x1d */ + 0, /* 0x1e */ + 0, /* 0x1f */ + 57, /* 0x20 */ + 73, /* 0x21 */ + 81, /* 0x22 */ + 79, /* 0x23 */ + 71, /* 0x24 */ + 75, /* 0x25 */ + 72, /* 0x26 */ + 77, /* 0x27 */ + 80, /* 0x28 */ + 0, /* 0x29 */ + 0, /* 0x2a */ + 0, /* 0x2b */ + 84, /* 0x2c */ + 82, /* 0x2d */ + 83, /* 0x2e */ + 99, /* 0x2f */ + 11, /* 0x30 */ + 2, /* 0x31 */ + 3, /* 0x32 */ + 4, /* 0x33 */ + 5, /* 0x34 */ + 6, /* 0x35 */ + 7, /* 0x36 */ + 8, /* 0x37 */ + 9, /* 0x38 */ + 10, /* 0x39 */ + 0, /* 0x3a */ + 0, /* 0x3b */ + 0, /* 0x3c */ + 0, /* 0x3d */ + 0, /* 0x3e */ + 0, /* 0x3f */ + 0, /* 0x40 */ + 30, /* 0x41 */ + 48, /* 0x42 */ + 46, /* 0x43 */ + 32, /* 0x44 */ + 18, /* 0x45 */ + 33, /* 0x46 */ + 34, /* 0x47 */ + 35, /* 0x48 */ + 23, /* 0x49 */ + 36, /* 0x4a */ + 37, /* 0x4b */ + 38, /* 0x4c */ + 50, /* 0x4d */ + 49, /* 0x4e */ + 24, /* 0x4f */ + 25, /* 0x50 */ + 16, /* 0x51 */ + 19, /* 0x52 */ + 31, /* 0x53 */ + 20, /* 0x54 */ + 22, /* 0x55 */ + 47, /* 0x56 */ + 17, /* 0x57 */ + 45, /* 0x58 */ + 21, /* 0x59 */ + 44, /* 0x5a */ + 91, /* 0x5b */ + 92, /* 0x5c */ + 93, /* 0x5d */ + 0, /* 0x5e */ + 95, /* 0x5f */ + 82, /* 0x60 */ + 79, /* 0x61 */ + 80, /* 0x62 */ + 81, /* 0x63 */ + 75, /* 0x64 */ + 76, /* 0x65 */ + 77, /* 0x66 */ + 71, /* 0x67 */ + 72, /* 0x68 */ + 73, /* 0x69 */ + 55, /* 0x6a */ + 78, /* 0x6b */ + 0, /* 0x6c */ + 74, /* 0x6d */ + 83, /* 0x6e */ + 53, /* 0x6f */ + 59, /* 0x70 */ + 60, /* 0x71 */ + 61, /* 0x72 */ + 62, /* 0x73 */ + 63, /* 0x74 */ + 64, /* 0x75 */ + 65, /* 0x76 */ + 66, /* 0x77 */ + 67, /* 0x78 */ + 68, /* 0x79 */ + 87, /* 0x7a */ + 88, /* 0x7b */ + 100, /* 0x7c */ + 101, /* 0x7d */ + 102, /* 0x7e */ + 103, /* 0x7f */ + 104, /* 0x80 */ + 105, /* 0x81 */ + 106, /* 0x82 */ + 107, /* 0x83 */ + 108, /* 0x84 */ + 109, /* 0x85 */ + 110, /* 0x86 */ + 118, /* 0x87 */ + 0, /* 0x88 */ + 0, /* 0x89 */ + 0, /* 0x8a */ + 0, /* 0x8b */ + 0, /* 0x8c */ + 0, /* 0x8d */ + 0, /* 0x8e */ + 0, /* 0x8f */ + 69, /* 0x90 */ + 70, /* 0x91 */ + 0, /* 0x92 */ + 0, /* 0x93 */ + 0, /* 0x94 */ + 0, /* 0x95 */ + 0, /* 0x96 */ + 0, /* 0x97 */ + 0, /* 0x98 */ + 0, /* 0x99 */ + 0, /* 0x9a */ + 0, /* 0x9b */ + 0, /* 0x9c */ + 0, /* 0x9d */ + 0, /* 0x9e */ + 0, /* 0x9f */ + 42, /* 0xa0 */ + 54, /* 0xa1 */ + 29, /* 0xa2 */ + 29, /* 0xa3 */ + 56, /* 0xa4 */ + 56, /* 0xa5 */ + 106, /* 0xa6 */ + 105, /* 0xa7 */ + 103, /* 0xa8 */ + 104, /* 0xa9 */ + 101, /* 0xaa */ + 102, /* 0xab */ + 50, /* 0xac */ + 32, /* 0xad */ + 46, /* 0xae */ + 48, /* 0xaf */ + 25, /* 0xb0 */ + 16, /* 0xb1 */ + 36, /* 0xb2 */ + 34, /* 0xb3 */ + 108, /* 0xb4 */ + 109, /* 0xb5 */ + 107, /* 0xb6 */ + 33, /* 0xb7 */ + 0, /* 0xb8 */ + 0, /* 0xb9 */ + 39, /* 0xba */ + 13, /* 0xbb */ + 51, /* 0xbc */ + 12, /* 0xbd */ + 52, /* 0xbe */ + 53, /* 0xbf */ + 41, /* 0xc0 */ + 115, /* 0xc1 */ + 126, /* 0xc2 */ + 0, /* 0xc3 */ + 0, /* 0xc4 */ + 0, /* 0xc5 */ + 0, /* 0xc6 */ + 0, /* 0xc7 */ + 0, /* 0xc8 */ + 0, /* 0xc9 */ + 0, /* 0xca */ + 0, /* 0xcb */ + 0, /* 0xcc */ + 0, /* 0xcd */ + 0, /* 0xce */ + 0, /* 0xcf */ + 0, /* 0xd0 */ + 0, /* 0xd1 */ + 0, /* 0xd2 */ + 0, /* 0xd3 */ + 0, /* 0xd4 */ + 0, /* 0xd5 */ + 0, /* 0xd6 */ + 0, /* 0xd7 */ + 0, /* 0xd8 */ + 0, /* 0xd9 */ + 0, /* 0xda */ + 26, /* 0xdb */ + 43, /* 0xdc */ + 27, /* 0xdd */ + 40, /* 0xde */ + 0, /* 0xdf */ + 0, /* 0xe0 */ + 0, /* 0xe1 */ + 86, /* 0xe2 */ + 0, /* 0xe3 */ + 0, /* 0xe4 */ + 0, /* 0xe5 */ + 0, /* 0xe6 */ + 0, /* 0xe7 */ + 0, /* 0xe8 */ + 113, /* 0xe9 */ + 92, /* 0xea */ + 123, /* 0xeb */ + 0, /* 0xec */ + 111, /* 0xed */ + 90, /* 0xee */ + 0, /* 0xef */ + 0, /* 0xf0 */ + 91, /* 0xf1 */ + 0, /* 0xf2 */ + 95, /* 0xf3 */ + 0, /* 0xf4 */ + 94, /* 0xf5 */ + 0, /* 0xf6 */ + 0, /* 0xf7 */ + 0, /* 0xf8 */ + 93, /* 0xf9 */ + 0, /* 0xfa */ + 98, /* 0xfb */ + 0, /* 0xfc */ + 0, /* 0xfd */ + 0, /* 0xfe */ + 0, /* 0xff */ + }; +} // namespace platf diff --git a/src/platform/windows/misc.cpp b/src/platform/windows/misc.cpp index d16c1d6bdbb..e136de66a70 100644 --- a/src/platform/windows/misc.cpp +++ b/src/platform/windows/misc.cpp @@ -2,15 +2,16 @@ * @file src/platform/windows/misc.cpp * @brief todo */ -#include #include #include #include +#include #include #include #include #include +#include // prevent clang format from "optimizing" the header include order // clang-format off @@ -28,19 +29,32 @@ #include // clang-format on -#include "src/main.h" +// Boost overrides NTDDI_VERSION, so we re-override it here +#undef NTDDI_VERSION +#define NTDDI_VERSION NTDDI_WIN10 +#include + +#include "misc.h" + +#include "src/entry_handler.h" +#include "src/globals.h" +#include "src/logging.h" #include "src/platform/common.h" #include "src/utility.h" #include +#include "nvprefs/nvprefs_interface.h" + // UDP_SEND_MSG_SIZE was added in the Windows 10 20H1 SDK #ifndef UDP_SEND_MSG_SIZE #define UDP_SEND_MSG_SIZE 2 #endif -// MinGW headers are missing qWAVE stuff -typedef UINT32 QOS_FLOWID, *PQOS_FLOWID; -#define QOS_NON_ADAPTIVE_FLOW 0x00000002 +// PROC_THREAD_ATTRIBUTE_JOB_LIST is currently missing from MinGW headers +#ifndef PROC_THREAD_ATTRIBUTE_JOB_LIST + #define PROC_THREAD_ATTRIBUTE_JOB_LIST ProcThreadAttributeValue(13, FALSE, TRUE, FALSE) +#endif + #include #ifndef WLAN_API_MAKE_VERSION @@ -53,8 +67,6 @@ using namespace std::literals; namespace platf { using adapteraddrs_t = util::c_ptr; - static std::wstring_convert, wchar_t> converter; - bool enabled_mouse_keys = false; MOUSEKEYS previous_mouse_keys_state; @@ -284,7 +296,7 @@ namespace platf { // Parse the environment block and populate env for (auto c = (PWCHAR) env_block; *c != UNICODE_NULL; c += wcslen(c) + 1) { // Environment variable entries end with a null-terminator, so std::wstring() will get an entire entry. - std::string env_tuple = converter.to_bytes(std::wstring { c }); + std::string env_tuple = to_utf8(std::wstring { c }); std::string env_name = env_tuple.substr(0, env_tuple.find('=')); std::string env_val = env_tuple.substr(env_tuple.find('=') + 1); @@ -358,7 +370,7 @@ namespace platf { for (const auto &entry : env) { auto name = entry.get_name(); auto value = entry.to_string(); - size += converter.from_bytes(name).length() + 1 /* L'=' */ + converter.from_bytes(value).length() + 1 /* L'\0' */; + size += from_utf8(name).length() + 1 /* L'=' */ + from_utf8(value).length() + 1 /* L'\0' */; } size += 1 /* L'\0' */; @@ -370,9 +382,9 @@ namespace platf { auto value = entry.to_string(); // Construct the NAME=VAL\0 string - append_string_to_environment_block(env_block, offset, converter.from_bytes(name)); + append_string_to_environment_block(env_block, offset, from_utf8(name)); env_block[offset++] = L'='; - append_string_to_environment_block(env_block, offset, converter.from_bytes(value)); + append_string_to_environment_block(env_block, offset, from_utf8(value)); env_block[offset++] = L'\0'; } @@ -412,11 +424,10 @@ namespace platf { * @param cmd The command that was used to launch the process. * @param ec A reference to an `std::error_code` object that will store any error that occurred during the launch. * @param process_info A reference to a `PROCESS_INFORMATION` structure that contains information about the new process. - * @param group A pointer to a `bp::group` object that will add the new process to its group, if not null. * @return A `bp::child` object representing the new process, or an empty `bp::child` object if the launch failed. */ bp::child - create_boost_child_from_results(bool process_launched, const std::string &cmd, std::error_code &ec, PROCESS_INFORMATION &process_info, bp::group *group) { + create_boost_child_from_results(bool process_launched, const std::string &cmd, std::error_code &ec, PROCESS_INFORMATION &process_info) { // Use RAII to ensure the process is closed when we're done with it, even if there was an error. auto close_process_handles = util::fail_guard([process_launched, process_info]() { if (process_launched) { @@ -433,11 +444,6 @@ namespace platf { if (process_launched) { // If the launch was successful, create a new bp::child object representing the new process auto child = bp::child((bp::pid_t) process_info.dwProcessId); - if (group) { - // If a group was provided, add the new process to the group - group->add(child); - } - BOOST_LOG(info) << cmd << " running with PID "sv << child.id(); return child; } @@ -483,7 +489,7 @@ namespace platf { auto winerror = GetLastError(); // Log the failure of reverting to self and its error code BOOST_LOG(fatal) << "Failed to revert to self after impersonation: "sv << winerror; - std::abort(); + DebugBreak(); } return ec; @@ -492,17 +498,18 @@ namespace platf { /** * @brief A function to create a `STARTUPINFOEXW` structure for launching a process. * @param file A pointer to a `FILE` object that will be used as the standard output and error for the new process, or null if not needed. + * @param job A job object handle to insert the new process into. This pointer must remain valid for the life of this startup info! * @param ec A reference to a `std::error_code` object that will store any error that occurred during the creation of the structure. * @return A `STARTUPINFOEXW` structure that contains information about how to launch the new process. */ STARTUPINFOEXW - create_startup_info(FILE *file, std::error_code &ec) { + create_startup_info(FILE *file, HANDLE *job, std::error_code &ec) { // Initialize a zeroed-out STARTUPINFOEXW structure and set its size STARTUPINFOEXW startup_info = {}; startup_info.StartupInfo.cb = sizeof(startup_info); - // Allocate a process attribute list with space for 1 element - startup_info.lpAttributeList = allocate_proc_thread_attr_list(1); + // Allocate a process attribute list with space for 2 elements + startup_info.lpAttributeList = allocate_proc_thread_attr_list(2); if (startup_info.lpAttributeList == NULL) { // If the allocation failed, set ec to an appropriate error code and return the structure ec = std::make_error_code(std::errc::not_enough_memory); @@ -533,9 +540,357 @@ namespace platf { NULL); } + if (job) { + // Atomically insert the new process into the specified job. + // + // Note: The value we point to here must be valid for the lifetime of the attribute list, + // so we take a HANDLE* instead of just a HANDLE to use the caller's stack storage. + UpdateProcThreadAttribute(startup_info.lpAttributeList, + 0, + PROC_THREAD_ATTRIBUTE_JOB_LIST, + job, + sizeof(*job), + NULL, + NULL); + } + return startup_info; } + /** + * @brief This function overrides HKEY_CURRENT_USER and HKEY_CLASSES_ROOT using the provided token. + * @param token The primary token identifying the user to use, or `NULL` to restore original keys. + * @return `true` if the override or restore operation was successful. + */ + bool + override_per_user_predefined_keys(HANDLE token) { + HKEY user_classes_root = NULL; + if (token) { + auto err = RegOpenUserClassesRoot(token, 0, GENERIC_ALL, &user_classes_root); + if (err != ERROR_SUCCESS) { + BOOST_LOG(error) << "Failed to open classes root for target user: "sv << err; + return false; + } + } + auto close_classes_root = util::fail_guard([user_classes_root]() { + if (user_classes_root) { + RegCloseKey(user_classes_root); + } + }); + + HKEY user_key = NULL; + if (token) { + impersonate_current_user(token, [&]() { + // RegOpenCurrentUser() doesn't take a token. It assumes we're impersonating the desired user. + auto err = RegOpenCurrentUser(GENERIC_ALL, &user_key); + if (err != ERROR_SUCCESS) { + BOOST_LOG(error) << "Failed to open user key for target user: "sv << err; + user_key = NULL; + } + }); + if (!user_key) { + return false; + } + } + auto close_user = util::fail_guard([user_key]() { + if (user_key) { + RegCloseKey(user_key); + } + }); + + auto err = RegOverridePredefKey(HKEY_CLASSES_ROOT, user_classes_root); + if (err != ERROR_SUCCESS) { + BOOST_LOG(error) << "Failed to override HKEY_CLASSES_ROOT: "sv << err; + return false; + } + + err = RegOverridePredefKey(HKEY_CURRENT_USER, user_key); + if (err != ERROR_SUCCESS) { + BOOST_LOG(error) << "Failed to override HKEY_CURRENT_USER: "sv << err; + RegOverridePredefKey(HKEY_CLASSES_ROOT, NULL); + return false; + } + + return true; + } + + /** + * @brief This function quotes/escapes an argument according to the Windows parsing convention. + * @param argument The raw argument to process. + * @return An argument string suitable for use by CreateProcess(). + */ + std::wstring + escape_argument(const std::wstring &argument) { + // If there are no characters requiring quoting/escaping, we're done + if (argument.find_first_of(L" \t\n\v\"") == argument.npos) { + return argument; + } + + // The algorithm implemented here comes from a MSDN blog post: + // https://web.archive.org/web/20120201194949/http://blogs.msdn.com/b/twistylittlepassagesallalike/archive/2011/04/23/everyone-quotes-arguments-the-wrong-way.aspx + std::wstring escaped_arg; + escaped_arg.push_back(L'"'); + for (auto it = argument.begin();; it++) { + auto backslash_count = 0U; + while (it != argument.end() && *it == L'\\') { + it++; + backslash_count++; + } + + if (it == argument.end()) { + escaped_arg.append(backslash_count * 2, L'\\'); + break; + } + else if (*it == L'"') { + escaped_arg.append(backslash_count * 2 + 1, L'\\'); + } + else { + escaped_arg.append(backslash_count, L'\\'); + } + + escaped_arg.push_back(*it); + } + escaped_arg.push_back(L'"'); + return escaped_arg; + } + + /** + * @brief This function escapes an argument according to cmd's parsing convention. + * @param argument An argument already escaped by `escape_argument()`. + * @return An argument string suitable for use by cmd.exe. + */ + std::wstring + escape_argument_for_cmd(const std::wstring &argument) { + // Start with the original string and modify from there + std::wstring escaped_arg = argument; + + // Look for the next cmd metacharacter + size_t match_pos = 0; + while ((match_pos = escaped_arg.find_first_of(L"()%!^\"<>&|", match_pos)) != std::wstring::npos) { + // Insert an escape character and skip past the match + escaped_arg.insert(match_pos, 1, L'^'); + match_pos += 2; + } + + return escaped_arg; + } + + /** + * @brief This function resolves the given raw command into a proper command string for CreateProcess(). + * @details This converts URLs and non-executable file paths into a runnable command like ShellExecute(). + * @param raw_cmd The raw command provided by the user. + * @param working_dir The working directory for the new process. + * @param token The user token currently being impersonated or `NULL` if running as ourselves. + * @param creation_flags The creation flags for CreateProcess(), which may be modified by this function. + * @return A command string suitable for use by CreateProcess(). + */ + std::wstring + resolve_command_string(const std::string &raw_cmd, const std::wstring &working_dir, HANDLE token, DWORD &creation_flags) { + std::wstring raw_cmd_w = from_utf8(raw_cmd); + + // First, convert the given command into parts so we can get the executable/file/URL without parameters + auto raw_cmd_parts = boost::program_options::split_winmain(raw_cmd_w); + if (raw_cmd_parts.empty()) { + // This is highly unexpected, but we'll just return the raw string and hope for the best. + BOOST_LOG(warning) << "Failed to split command string: "sv << raw_cmd; + return from_utf8(raw_cmd); + } + + auto raw_target = raw_cmd_parts.at(0); + std::wstring lookup_string; + HRESULT res; + + if (PathIsURLW(raw_target.c_str())) { + std::array scheme; + + DWORD out_len = scheme.size(); + res = UrlGetPartW(raw_target.c_str(), scheme.data(), &out_len, URL_PART_SCHEME, 0); + if (res != S_OK) { + BOOST_LOG(warning) << "Failed to extract URL scheme from URL: "sv << raw_target << " ["sv << util::hex(res).to_string_view() << ']'; + return from_utf8(raw_cmd); + } + + // If the target is a URL, the class is found using the URL scheme (prior to and not including the ':') + lookup_string = scheme.data(); + } + else { + // If the target is not a URL, assume it's a regular file path + auto extension = PathFindExtensionW(raw_target.c_str()); + if (extension == nullptr || *extension == 0) { + // If the file has no extension, assume it's a command and allow CreateProcess() + // to try to find it via PATH + return from_utf8(raw_cmd); + } + else if (boost::iequals(extension, L".exe")) { + // If the file has an .exe extension, we will bypass the resolution here and + // directly pass the unmodified command string to CreateProcess(). The argument + // escaping rules are subtly different between CreateProcess() and ShellExecute(), + // and we want to preserve backwards compatibility with older configs. + return from_utf8(raw_cmd); + } + + // For regular files, the class is found using the file extension (including the dot) + lookup_string = extension; + } + + std::array shell_command_string; + bool needs_cmd_escaping = false; + { + // Overriding these predefined keys affects process-wide state, so serialize all calls + // to ensure the handle state is consistent while we perform the command query. + static std::mutex per_user_key_mutex; + auto lg = std::lock_guard(per_user_key_mutex); + + // Override HKEY_CLASSES_ROOT and HKEY_CURRENT_USER to ensure we query the correct class info + if (!override_per_user_predefined_keys(token)) { + return from_utf8(raw_cmd); + } + + // Find the command string for the specified class + DWORD out_len = shell_command_string.size(); + res = AssocQueryStringW(ASSOCF_NOTRUNCATE, ASSOCSTR_COMMAND, lookup_string.c_str(), L"open", shell_command_string.data(), &out_len); + + // In some cases (UWP apps), we might not have a command for this target. If that happens, + // we'll have to launch via cmd.exe. This prevents proper job tracking, but that was already + // broken for UWP apps anyway due to how they are started by Windows. Even 'start /wait' + // doesn't work properly for UWP, so really no termination tracking seems to work at all. + // + // FIXME: Maybe we can improve this in the future. + if (res == HRESULT_FROM_WIN32(ERROR_NO_ASSOCIATION)) { + BOOST_LOG(warning) << "Using trampoline to handle target: "sv << raw_cmd; + std::wcscpy(shell_command_string.data(), L"cmd.exe /c start \"\" /wait \"%1\" %*"); + needs_cmd_escaping = true; + + // We must suppress the console window that would otherwise appear when starting cmd.exe. + creation_flags &= ~CREATE_NEW_CONSOLE; + creation_flags |= CREATE_NO_WINDOW; + + res = S_OK; + } + + // Reset per-user keys back to the original value + override_per_user_predefined_keys(NULL); + } + + if (res != S_OK) { + BOOST_LOG(warning) << "Failed to query command string for raw command: "sv << raw_cmd << " ["sv << util::hex(res).to_string_view() << ']'; + return from_utf8(raw_cmd); + } + + // Finally, construct the real command string that will be passed into CreateProcess(). + // We support common substitutions (%*, %1, %2, %L, %W, %V, etc), but there are other + // uncommon ones that are unsupported here. + // + // https://web.archive.org/web/20111002101214/http://msdn.microsoft.com/en-us/library/windows/desktop/cc144101(v=vs.85).aspx + std::wstring cmd_string { shell_command_string.data() }; + size_t match_pos = 0; + while ((match_pos = cmd_string.find_first_of(L'%', match_pos)) != std::wstring::npos) { + std::wstring match_replacement; + + // If no additional character exists after the match, the dangling '%' is stripped + if (match_pos + 1 == cmd_string.size()) { + cmd_string.erase(match_pos, 1); + break; + } + + // Shell command replacements are strictly '%' followed by a single non-'%' character + auto next_char = std::tolower(cmd_string.at(match_pos + 1)); + switch (next_char) { + // Escape character + case L'%': + match_replacement = L'%'; + break; + + // Argument replacements + case L'0': + case L'1': + case L'2': + case L'3': + case L'4': + case L'5': + case L'6': + case L'7': + case L'8': + case L'9': { + // Arguments numbers are 1-based, except for %0 which is equivalent to %1 + int index = next_char - L'0'; + if (next_char != L'0') { + index--; + } + + // Replace with the matching argument, or nothing if the index is invalid + if (index < raw_cmd_parts.size()) { + match_replacement = raw_cmd_parts.at(index); + } + break; + } + + // All arguments following the target + case L'*': + for (int i = 1; i < raw_cmd_parts.size(); i++) { + // Insert a space before arguments after the first one + if (i > 1) { + match_replacement += L' '; + } + + // Argument escaping applies only to %*, not the single substitutions like %2 + auto escaped_argument = escape_argument(raw_cmd_parts.at(i)); + if (needs_cmd_escaping) { + // If we're using the cmd.exe trampoline, we'll need to add additional escaping + escaped_argument = escape_argument_for_cmd(escaped_argument); + } + match_replacement += escaped_argument; + } + break; + + // Long file path of target + case L'l': + case L'd': + case L'v': { + std::array path; + std::array other_dirs { working_dir.c_str(), nullptr }; + + // PathFindOnPath() is a little gross because it uses the same + // buffer for input and output, so we need to copy our input + // into the path array. + std::wcsncpy(path.data(), raw_target.c_str(), path.size()); + if (path[path.size() - 1] != 0) { + // The path was so long it was truncated by this copy. We'll + // assume it was an absolute path (likely) and use it unmodified. + match_replacement = raw_target; + } + // See if we can find the path on our search path or working directory + else if (PathFindOnPathW(path.data(), other_dirs.data())) { + match_replacement = std::wstring { path.data() }; + } + else { + // We couldn't find the target, so we'll just hope for the best + match_replacement = raw_target; + } + break; + } + + // Working directory + case L'w': + match_replacement = working_dir; + break; + + default: + BOOST_LOG(warning) << "Unsupported argument replacement: %%" << next_char; + break; + } + + // Replace the % and following character with the match replacement + cmd_string.replace(match_pos, 2, match_replacement); + + // Skip beyond the match replacement itself to prevent recursive replacement + match_pos += match_replacement.size(); + } + + BOOST_LOG(info) << "Resolved user-provided command '"sv << raw_cmd << "' to '"sv << cmd_string << '\''; + return cmd_string; + } + /** * @brief Run a command on the users profile. * @@ -552,15 +907,17 @@ namespace platf { * @return A `bp::child` object representing the new process, or an empty `bp::child` object if the launch fails. */ bp::child - run_command(bool elevated, bool interactive, const std::string &cmd, boost::filesystem::path &working_dir, bp::environment &env, FILE *file, std::error_code &ec, bp::group *group) { - BOOL ret; - // Convert cmd, env, and working_dir to the appropriate character sets for Win32 APIs - std::wstring wcmd = converter.from_bytes(cmd); - std::wstring start_dir = converter.from_bytes(working_dir.string()); - - STARTUPINFOEXW startup_info = create_startup_info(file, ec); + run_command(bool elevated, bool interactive, const std::string &cmd, boost::filesystem::path &working_dir, const bp::environment &env, FILE *file, std::error_code &ec, bp::group *group) { + std::wstring start_dir = from_utf8(working_dir.string()); + HANDLE job = group ? group->native_handle() : nullptr; + STARTUPINFOEXW startup_info = create_startup_info(file, job ? &job : nullptr, ec); PROCESS_INFORMATION process_info; + // Clone the environment to create a local copy. Boost.Process (bp) shares the environment with all spawned processes. + // Since we're going to modify the 'env' variable by merging user-specific environment variables into it, + // we make a clone to prevent side effects to the shared environment. + bp::environment cloned_env = env; + if (ec) { // In the event that startup_info failed, return a blank child process. return bp::child(); @@ -576,6 +933,32 @@ namespace platf { // Create a new console for interactive processes and use no console for non-interactive processes creation_flags |= interactive ? CREATE_NEW_CONSOLE : CREATE_NO_WINDOW; + // Find the PATH variable in our environment block using a case-insensitive search + auto sunshine_wenv = boost::this_process::wenvironment(); + std::wstring path_var_name { L"PATH" }; + std::wstring old_path_val; + auto itr = std::find_if(sunshine_wenv.cbegin(), sunshine_wenv.cend(), [&](const auto &e) { return boost::iequals(e.get_name(), path_var_name); }); + if (itr != sunshine_wenv.cend()) { + // Use the existing variable if it exists, since Boost treats these as case-sensitive. + path_var_name = itr->get_name(); + old_path_val = sunshine_wenv[path_var_name].to_string(); + } + + // Temporarily prepend the specified working directory to PATH to ensure CreateProcess() + // will (preferentially) find binaries that reside in the working directory. + sunshine_wenv[path_var_name].assign(start_dir + L";" + old_path_val); + + // Restore the old PATH value for our process when we're done here + auto restore_path = util::fail_guard([&]() { + if (old_path_val.empty()) { + sunshine_wenv[path_var_name].clear(); + } + else { + sunshine_wenv[path_var_name].assign(old_path_val); + } + }); + + BOOL ret; if (is_running_as_system()) { // Duplicate the current user's token HANDLE user_token = retrieve_users_token(elevated); @@ -591,14 +974,15 @@ namespace platf { }); // Populate env with user-specific environment variables - if (!merge_user_environment_block(env, user_token)) { + if (!merge_user_environment_block(cloned_env, user_token)) { ec = std::make_error_code(std::errc::not_enough_memory); return bp::child(); } // Open the process as the current user account, elevation is handled in the token itself. ec = impersonate_current_user(user_token, [&]() { - std::wstring env_block = create_environment_block(env); + std::wstring env_block = create_environment_block(cloned_env); + std::wstring wcmd = resolve_command_string(cmd, start_dir, user_token, creation_flags); ret = CreateProcessAsUserW(user_token, NULL, (LPWSTR) wcmd.c_str(), @@ -626,12 +1010,13 @@ namespace platf { }); // Populate env with user-specific environment variables - if (!merge_user_environment_block(env, process_token)) { + if (!merge_user_environment_block(cloned_env, process_token)) { ec = std::make_error_code(std::errc::not_enough_memory); return bp::child(); } - std::wstring env_block = create_environment_block(env); + std::wstring env_block = create_environment_block(cloned_env); + std::wstring wcmd = resolve_command_string(cmd, start_dir, NULL, creation_flags); ret = CreateProcessW(NULL, (LPWSTR) wcmd.c_str(), NULL, @@ -645,7 +1030,7 @@ namespace platf { } // Use the results of the launch to create a bp::child object - return create_boost_child_from_results(ret, cmd, ec, process_info, group); + return create_boost_child_from_results(ret, cmd, ec, process_info); } /** @@ -654,15 +1039,11 @@ namespace platf { */ void open_url(const std::string &url) { - // set working dir to Windows system directory - auto working_dir = boost::filesystem::path(std::getenv("SystemRoot")); - boost::process::environment _env = boost::this_process::environment(); + auto working_dir = boost::filesystem::path(); std::error_code ec; - // Launch this as a non-interactive non-elevated command to avoid an extra console window - std::string cmd = R"(cmd /C "start )" + url + R"(")"; - auto child = run_command(false, false, cmd, working_dir, _env, nullptr, ec, nullptr); + auto child = run_command(false, false, url, working_dir, _env, nullptr, ec, nullptr); if (ec) { BOOST_LOG(warning) << "Couldn't open url ["sv << url << "]: System: "sv << ec.message(); } @@ -740,6 +1121,16 @@ namespace platf { // Promote ourselves to high priority class SetPriorityClass(GetCurrentProcess(), HIGH_PRIORITY_CLASS); + // Modify NVIDIA control panel settings again, in case they have been changed externally since sunshine launch + if (nvprefs_instance.load()) { + if (!nvprefs_instance.owning_undo_file()) { + nvprefs_instance.restore_from_and_delete_undo_file_if_exists(); + } + nvprefs_instance.modify_application_profile(); + nvprefs_instance.modify_global_profile(); + nvprefs_instance.unload(); + } + // Enable low latency mode on all connected WLAN NICs if wlanapi.dll is available if (fn_WlanOpenHandle) { DWORD negotiated_version; @@ -751,7 +1142,7 @@ namespace platf { for (DWORD i = 0; i < wlan_interface_list->dwNumberOfItems; i++) { if (wlan_interface_list->InterfaceInfo[i].isState == wlan_interface_state_connected) { // Enable media streaming mode for 802.11 wireless interfaces to reduce latency and - // unneccessary background scanning operations that cause packet loss and jitter. + // unnecessary background scanning operations that cause packet loss and jitter. // // https://docs.microsoft.com/en-us/windows-hardware/drivers/network/oid-wdi-set-connection-quality // https://docs.microsoft.com/en-us/previous-versions/windows/hardware/wireless/native-802-11-media-streaming @@ -875,6 +1266,106 @@ namespace platf { lifetime::exit_sunshine(0, true); } + struct enum_wnd_context_t { + std::set process_ids; + bool requested_exit; + }; + + static BOOL CALLBACK + prgrp_enum_windows(HWND hwnd, LPARAM lParam) { + auto enum_ctx = (enum_wnd_context_t *) lParam; + + // Find the owner PID of this window + DWORD wnd_process_id; + if (!GetWindowThreadProcessId(hwnd, &wnd_process_id)) { + // Continue enumeration + return TRUE; + } + + // Check if this window is owned by a process we want to terminate + if (enum_ctx->process_ids.find(wnd_process_id) != enum_ctx->process_ids.end()) { + // Send an async WM_CLOSE message to this window + if (SendNotifyMessageW(hwnd, WM_CLOSE, 0, 0)) { + BOOST_LOG(debug) << "Sent WM_CLOSE to PID: "sv << wnd_process_id; + enum_ctx->requested_exit = true; + } + else { + auto error = GetLastError(); + BOOST_LOG(warning) << "Failed to send WM_CLOSE to PID ["sv << wnd_process_id << "]: " << error; + } + } + + // Continue enumeration + return TRUE; + } + + /** + * @brief Attempt to gracefully terminate a process group. + * @param native_handle The job object handle. + * @return true if termination was successfully requested. + */ + bool + request_process_group_exit(std::uintptr_t native_handle) { + auto job_handle = (HANDLE) native_handle; + + // Get list of all processes in our job object + bool success; + DWORD required_length = sizeof(JOBOBJECT_BASIC_PROCESS_ID_LIST); + auto process_id_list = (PJOBOBJECT_BASIC_PROCESS_ID_LIST) calloc(1, required_length); + auto fg = util::fail_guard([&process_id_list]() { + free(process_id_list); + }); + while (!(success = QueryInformationJobObject(job_handle, JobObjectBasicProcessIdList, + process_id_list, required_length, &required_length)) && + GetLastError() == ERROR_MORE_DATA) { + free(process_id_list); + process_id_list = (PJOBOBJECT_BASIC_PROCESS_ID_LIST) calloc(1, required_length); + if (!process_id_list) { + return false; + } + } + + if (!success) { + auto err = GetLastError(); + BOOST_LOG(warning) << "Failed to enumerate processes in group: "sv << err; + return false; + } + else if (process_id_list->NumberOfProcessIdsInList == 0) { + // If all processes are already dead, treat it as a success + return true; + } + + enum_wnd_context_t enum_ctx = {}; + enum_ctx.requested_exit = false; + for (DWORD i = 0; i < process_id_list->NumberOfProcessIdsInList; i++) { + enum_ctx.process_ids.emplace(process_id_list->ProcessIdList[i]); + } + + // Enumerate all windows belonging to processes in the list + EnumWindows(prgrp_enum_windows, (LPARAM) &enum_ctx); + + // Return success if we told at least one window to close + return enum_ctx.requested_exit; + } + + /** + * @brief Checks if a process group still has running children. + * @param native_handle The job object handle. + * @return true if processes are still running. + */ + bool + process_group_running(std::uintptr_t native_handle) { + JOBOBJECT_BASIC_ACCOUNTING_INFORMATION accounting_info; + + if (!QueryInformationJobObject((HANDLE) native_handle, JobObjectBasicAccountingInformation, &accounting_info, sizeof(accounting_info), nullptr)) { + auto err = GetLastError(); + BOOST_LOG(error) << "Failed to get job accounting info: "sv << err; + return false; + } + + return accounting_info.ActiveProcesses != 0; + } + SOCKADDR_IN to_sockaddr(boost::asio::ip::address_v4 address, uint16_t port) { SOCKADDR_IN saddr_v4 = {}; @@ -909,19 +1400,19 @@ namespace platf { WSAMSG msg; // Convert the target address into a SOCKADDR - SOCKADDR_IN saddr_v4; - SOCKADDR_IN6 saddr_v6; + SOCKADDR_IN taddr_v4; + SOCKADDR_IN6 taddr_v6; if (send_info.target_address.is_v6()) { - saddr_v6 = to_sockaddr(send_info.target_address.to_v6(), send_info.target_port); + taddr_v6 = to_sockaddr(send_info.target_address.to_v6(), send_info.target_port); - msg.name = (PSOCKADDR) &saddr_v6; - msg.namelen = sizeof(saddr_v6); + msg.name = (PSOCKADDR) &taddr_v6; + msg.namelen = sizeof(taddr_v6); } else { - saddr_v4 = to_sockaddr(send_info.target_address.to_v4(), send_info.target_port); + taddr_v4 = to_sockaddr(send_info.target_address.to_v4(), send_info.target_port); - msg.name = (PSOCKADDR) &saddr_v4; - msg.namelen = sizeof(saddr_v4); + msg.name = (PSOCKADDR) &taddr_v4; + msg.namelen = sizeof(taddr_v4); } WSABUF buf; @@ -932,25 +1423,137 @@ namespace platf { msg.dwBufferCount = 1; msg.dwFlags = 0; - char cmbuf[WSA_CMSG_SPACE(sizeof(DWORD))]; + // At most, one DWORD option and one PKTINFO option + char cmbuf[WSA_CMSG_SPACE(sizeof(DWORD)) + + std::max(WSA_CMSG_SPACE(sizeof(IN6_PKTINFO)), WSA_CMSG_SPACE(sizeof(IN_PKTINFO)))] = {}; + ULONG cmbuflen = 0; + msg.Control.buf = cmbuf; - msg.Control.len = 0; + msg.Control.len = sizeof(cmbuf); + + auto cm = WSA_CMSG_FIRSTHDR(&msg); + if (send_info.source_address.is_v6()) { + IN6_PKTINFO pktInfo; + + SOCKADDR_IN6 saddr_v6 = to_sockaddr(send_info.source_address.to_v6(), 0); + pktInfo.ipi6_addr = saddr_v6.sin6_addr; + pktInfo.ipi6_ifindex = 0; + + cmbuflen += WSA_CMSG_SPACE(sizeof(pktInfo)); + + cm->cmsg_level = IPPROTO_IPV6; + cm->cmsg_type = IPV6_PKTINFO; + cm->cmsg_len = WSA_CMSG_LEN(sizeof(pktInfo)); + memcpy(WSA_CMSG_DATA(cm), &pktInfo, sizeof(pktInfo)); + } + else { + IN_PKTINFO pktInfo; + + SOCKADDR_IN saddr_v4 = to_sockaddr(send_info.source_address.to_v4(), 0); + pktInfo.ipi_addr = saddr_v4.sin_addr; + pktInfo.ipi_ifindex = 0; + + cmbuflen += WSA_CMSG_SPACE(sizeof(pktInfo)); + + cm->cmsg_level = IPPROTO_IP; + cm->cmsg_type = IP_PKTINFO; + cm->cmsg_len = WSA_CMSG_LEN(sizeof(pktInfo)); + memcpy(WSA_CMSG_DATA(cm), &pktInfo, sizeof(pktInfo)); + } if (send_info.block_count > 1) { - msg.Control.len += WSA_CMSG_SPACE(sizeof(DWORD)); + cmbuflen += WSA_CMSG_SPACE(sizeof(DWORD)); - auto cm = WSA_CMSG_FIRSTHDR(&msg); + cm = WSA_CMSG_NXTHDR(&msg, cm); cm->cmsg_level = IPPROTO_UDP; cm->cmsg_type = UDP_SEND_MSG_SIZE; cm->cmsg_len = WSA_CMSG_LEN(sizeof(DWORD)); *((DWORD *) WSA_CMSG_DATA(cm)) = send_info.block_size; } + msg.Control.len = cmbuflen; + // If USO is not supported, this will fail and the caller will fall back to unbatched sends. DWORD bytes_sent; return WSASendMsg((SOCKET) send_info.native_socket, &msg, 1, &bytes_sent, nullptr, nullptr) != SOCKET_ERROR; } + bool + send(send_info_t &send_info) { + WSAMSG msg; + + // Convert the target address into a SOCKADDR + SOCKADDR_IN taddr_v4; + SOCKADDR_IN6 taddr_v6; + if (send_info.target_address.is_v6()) { + taddr_v6 = to_sockaddr(send_info.target_address.to_v6(), send_info.target_port); + + msg.name = (PSOCKADDR) &taddr_v6; + msg.namelen = sizeof(taddr_v6); + } + else { + taddr_v4 = to_sockaddr(send_info.target_address.to_v4(), send_info.target_port); + + msg.name = (PSOCKADDR) &taddr_v4; + msg.namelen = sizeof(taddr_v4); + } + + WSABUF buf; + buf.buf = (char *) send_info.buffer; + buf.len = send_info.size; + + msg.lpBuffers = &buf; + msg.dwBufferCount = 1; + msg.dwFlags = 0; + + char cmbuf[std::max(WSA_CMSG_SPACE(sizeof(IN6_PKTINFO)), WSA_CMSG_SPACE(sizeof(IN_PKTINFO)))] = {}; + ULONG cmbuflen = 0; + + msg.Control.buf = cmbuf; + msg.Control.len = sizeof(cmbuf); + + auto cm = WSA_CMSG_FIRSTHDR(&msg); + if (send_info.source_address.is_v6()) { + IN6_PKTINFO pktInfo; + + SOCKADDR_IN6 saddr_v6 = to_sockaddr(send_info.source_address.to_v6(), 0); + pktInfo.ipi6_addr = saddr_v6.sin6_addr; + pktInfo.ipi6_ifindex = 0; + + cmbuflen += WSA_CMSG_SPACE(sizeof(pktInfo)); + + cm->cmsg_level = IPPROTO_IPV6; + cm->cmsg_type = IPV6_PKTINFO; + cm->cmsg_len = WSA_CMSG_LEN(sizeof(pktInfo)); + memcpy(WSA_CMSG_DATA(cm), &pktInfo, sizeof(pktInfo)); + } + else { + IN_PKTINFO pktInfo; + + SOCKADDR_IN saddr_v4 = to_sockaddr(send_info.source_address.to_v4(), 0); + pktInfo.ipi_addr = saddr_v4.sin_addr; + pktInfo.ipi_ifindex = 0; + + cmbuflen += WSA_CMSG_SPACE(sizeof(pktInfo)); + + cm->cmsg_level = IPPROTO_IP; + cm->cmsg_type = IP_PKTINFO; + cm->cmsg_len = WSA_CMSG_LEN(sizeof(pktInfo)); + memcpy(WSA_CMSG_DATA(cm), &pktInfo, sizeof(pktInfo)); + } + + msg.Control.len = cmbuflen; + + DWORD bytes_sent; + if (WSASendMsg((SOCKET) send_info.native_socket, &msg, 1, &bytes_sent, nullptr, nullptr) == SOCKET_ERROR) { + auto winerr = WSAGetLastError(); + BOOST_LOG(warning) << "WSASendMsg() failed: "sv << winerr; + return false; + } + + return true; + } + class qos_t: public deinit_t { public: qos_t(QOS_FLOWID flow_id): @@ -967,11 +1570,25 @@ namespace platf { QOS_FLOWID flow_id; }; + /** + * @brief Enables QoS on the given socket for traffic to the specified destination. + * @param native_socket The native socket handle. + * @param address The destination address for traffic sent on this socket. + * @param port The destination port for traffic sent on this socket. + * @param data_type The type of traffic sent on this socket. + * @param dscp_tagging Specifies whether to enable DSCP tagging on outgoing traffic. + */ std::unique_ptr - enable_socket_qos(uintptr_t native_socket, boost::asio::ip::address &address, uint16_t port, qos_data_type_e data_type) { + enable_socket_qos(uintptr_t native_socket, boost::asio::ip::address &address, uint16_t port, qos_data_type_e data_type, bool dscp_tagging) { SOCKADDR_IN saddr_v4; SOCKADDR_IN6 saddr_v6; PSOCKADDR dest_addr; + bool using_connect_hack = false; + + // Windows doesn't support any concept of traffic priority without DSCP tagging + if (!dscp_tagging) { + return nullptr; + } static std::once_flag load_qwave_once_flag; std::call_once(load_qwave_once_flag, []() { @@ -1010,9 +1627,40 @@ namespace platf { return nullptr; } + auto disconnect_fg = util::fail_guard([&]() { + if (using_connect_hack) { + SOCKADDR_IN6 empty = {}; + empty.sin6_family = AF_INET6; + if (connect((SOCKET) native_socket, (PSOCKADDR) &empty, sizeof(empty)) < 0) { + auto wsaerr = WSAGetLastError(); + BOOST_LOG(error) << "qWAVE dual-stack workaround failed: "sv << wsaerr; + } + } + }); + if (address.is_v6()) { - saddr_v6 = to_sockaddr(address.to_v6(), port); + auto address_v6 = address.to_v6(); + + saddr_v6 = to_sockaddr(address_v6, port); dest_addr = (PSOCKADDR) &saddr_v6; + + // qWAVE doesn't properly support IPv4-mapped IPv6 addresses, nor does it + // correctly support IPv4 addresses on a dual-stack socket (despite MSDN's + // claims to the contrary). To get proper QoS tagging when hosting in dual + // stack mode, we will temporarily connect() the socket to allow qWAVE to + // successfully initialize a flow, then disconnect it again so WSASendMsg() + // works later on. + if (address_v6.is_v4_mapped()) { + if (connect((SOCKET) native_socket, (PSOCKADDR) &saddr_v6, sizeof(saddr_v6)) < 0) { + auto wsaerr = WSAGetLastError(); + BOOST_LOG(error) << "qWAVE dual-stack workaround failed: "sv << wsaerr; + } + else { + BOOST_LOG(debug) << "Using qWAVE connect() workaround for QoS tagging"sv; + using_connect_hack = true; + dest_addr = nullptr; + } + } } else { saddr_v4 = to_sockaddr(address.to_v4(), port); @@ -1043,8 +1691,8 @@ namespace platf { } int64_t qpc_counter() { - LARGE_INTEGER performace_counter; - if (QueryPerformanceCounter(&performace_counter)) return performace_counter.QuadPart; + LARGE_INTEGER performance_counter; + if (QueryPerformanceCounter(&performance_counter)) return performance_counter.QuadPart; return 0; } @@ -1062,4 +1710,70 @@ namespace platf { } return {}; } + + /** + * @brief Converts a UTF-8 string into a UTF-16 wide string. + * @param string The UTF-8 string. + * @return The converted UTF-16 wide string. + */ + std::wstring + from_utf8(const std::string &string) { + // No conversion needed if the string is empty + if (string.empty()) { + return {}; + } + + // Get the output size required to store the string + auto output_size = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, string.data(), string.size(), nullptr, 0); + if (output_size == 0) { + auto winerr = GetLastError(); + BOOST_LOG(error) << "Failed to get UTF-16 buffer size: "sv << winerr; + return {}; + } + + // Perform the conversion + std::wstring output(output_size, L'\0'); + output_size = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, string.data(), string.size(), output.data(), output.size()); + if (output_size == 0) { + auto winerr = GetLastError(); + BOOST_LOG(error) << "Failed to convert string to UTF-16: "sv << winerr; + return {}; + } + + return output; + } + + /** + * @brief Converts a UTF-16 wide string into a UTF-8 string. + * @param string The UTF-16 wide string. + * @return The converted UTF-8 string. + */ + std::string + to_utf8(const std::wstring &string) { + // No conversion needed if the string is empty + if (string.empty()) { + return {}; + } + + // Get the output size required to store the string + auto output_size = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, string.data(), string.size(), + nullptr, 0, nullptr, nullptr); + if (output_size == 0) { + auto winerr = GetLastError(); + BOOST_LOG(error) << "Failed to get UTF-8 buffer size: "sv << winerr; + return {}; + } + + // Perform the conversion + std::string output(output_size, '\0'); + output_size = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, string.data(), string.size(), + output.data(), output.size(), nullptr, nullptr); + if (output_size == 0) { + auto winerr = GetLastError(); + BOOST_LOG(error) << "Failed to convert string to UTF-8: "sv << winerr; + return {}; + } + + return output; + } } // namespace platf diff --git a/src/platform/windows/misc.h b/src/platform/windows/misc.h index 9228ce59fe7..5a3e29b0257 100644 --- a/src/platform/windows/misc.h +++ b/src/platform/windows/misc.h @@ -20,4 +20,20 @@ namespace platf { std::chrono::nanoseconds qpc_time_difference(int64_t performance_counter1, int64_t performance_counter2); + + /** + * @brief Converts a UTF-8 string into a UTF-16 wide string. + * @param string The UTF-8 string. + * @return The converted UTF-16 wide string. + */ + std::wstring + from_utf8(const std::string &string); + + /** + * @brief Converts a UTF-16 wide string into a UTF-8 string. + * @param string The UTF-16 wide string. + * @return The converted UTF-8 string. + */ + std::string + to_utf8(const std::wstring &string); } // namespace platf diff --git a/src/platform/windows/nvprefs/driver_settings.cpp b/src/platform/windows/nvprefs/driver_settings.cpp new file mode 100644 index 00000000000..2fbda6dc698 --- /dev/null +++ b/src/platform/windows/nvprefs/driver_settings.cpp @@ -0,0 +1,310 @@ +// local includes +#include "driver_settings.h" +#include "nvprefs_common.h" + +namespace { + + const auto sunshine_application_profile_name = L"SunshineStream"; + const auto sunshine_application_path = L"sunshine.exe"; + + void + nvapi_error_message(NvAPI_Status status) { + NvAPI_ShortString message = {}; + NvAPI_GetErrorMessage(status, message); + nvprefs::error_message(std::string("NvAPI error: ") + message); + } + + void + fill_nvapi_string(NvAPI_UnicodeString &dest, const wchar_t *src) { + static_assert(sizeof(NvU16) == sizeof(wchar_t)); + memcpy_s(dest, NVAPI_UNICODE_STRING_MAX * sizeof(NvU16), src, (wcslen(src) + 1) * sizeof(wchar_t)); + } + +} // namespace + +namespace nvprefs { + + driver_settings_t::~driver_settings_t() { + if (session_handle) { + NvAPI_DRS_DestroySession(session_handle); + } + } + + bool + driver_settings_t::init() { + if (session_handle) return true; + + NvAPI_Status status; + + status = NvAPI_Initialize(); + if (status != NVAPI_OK) { + info_message("NvAPI_Initialize() failed, ignore if you don't have NVIDIA video card"); + return false; + } + + status = NvAPI_DRS_CreateSession(&session_handle); + if (status != NVAPI_OK) { + nvapi_error_message(status); + error_message("NvAPI_DRS_CreateSession() failed"); + return false; + } + + return load_settings(); + } + + void + driver_settings_t::destroy() { + if (session_handle) { + NvAPI_DRS_DestroySession(session_handle); + session_handle = 0; + } + NvAPI_Unload(); + } + + bool + driver_settings_t::load_settings() { + if (!session_handle) return false; + + NvAPI_Status status = NvAPI_DRS_LoadSettings(session_handle); + if (status != NVAPI_OK) { + nvapi_error_message(status); + error_message("NvAPI_DRS_LoadSettings() failed"); + destroy(); + return false; + } + + return true; + } + + bool + driver_settings_t::save_settings() { + if (!session_handle) return false; + + NvAPI_Status status = NvAPI_DRS_SaveSettings(session_handle); + if (status != NVAPI_OK) { + nvapi_error_message(status); + error_message("NvAPI_DRS_SaveSettings() failed"); + return false; + } + + return true; + } + + bool + driver_settings_t::restore_global_profile_to_undo(const undo_data_t &undo_data) { + if (!session_handle) return false; + + const auto &swapchain_data = undo_data.get_opengl_swapchain(); + if (swapchain_data) { + NvAPI_Status status; + + NvDRSProfileHandle profile_handle = 0; + status = NvAPI_DRS_GetBaseProfile(session_handle, &profile_handle); + if (status != NVAPI_OK) { + nvapi_error_message(status); + error_message("NvAPI_DRS_GetBaseProfile() failed"); + return false; + } + + NVDRS_SETTING setting = {}; + setting.version = NVDRS_SETTING_VER; + status = NvAPI_DRS_GetSetting(session_handle, profile_handle, OGL_CPL_PREFER_DXPRESENT_ID, &setting); + + if (status == NVAPI_OK && setting.settingLocation == NVDRS_CURRENT_PROFILE_LOCATION && setting.u32CurrentValue == swapchain_data->our_value) { + if (swapchain_data->undo_value) { + setting = {}; + setting.version = NVDRS_SETTING_VER1; + setting.settingId = OGL_CPL_PREFER_DXPRESENT_ID; + setting.settingType = NVDRS_DWORD_TYPE; + setting.settingLocation = NVDRS_CURRENT_PROFILE_LOCATION; + setting.u32CurrentValue = *swapchain_data->undo_value; + + status = NvAPI_DRS_SetSetting(session_handle, profile_handle, &setting); + + if (status != NVAPI_OK) { + nvapi_error_message(status); + error_message("NvAPI_DRS_SetSetting() OGL_CPL_PREFER_DXPRESENT failed"); + return false; + } + } + else { + status = NvAPI_DRS_DeleteProfileSetting(session_handle, profile_handle, OGL_CPL_PREFER_DXPRESENT_ID); + + if (status != NVAPI_OK && status != NVAPI_SETTING_NOT_FOUND) { + nvapi_error_message(status); + error_message("NvAPI_DRS_DeleteProfileSetting() OGL_CPL_PREFER_DXPRESENT failed"); + return false; + } + } + + info_message("Restored OGL_CPL_PREFER_DXPRESENT for base profile"); + } + else if (status == NVAPI_OK || status == NVAPI_SETTING_NOT_FOUND) { + info_message("OGL_CPL_PREFER_DXPRESENT has been changed from our value in base profile, not restoring"); + } + else { + error_message("NvAPI_DRS_GetSetting() OGL_CPL_PREFER_DXPRESENT failed"); + return false; + } + } + + return true; + } + + bool + driver_settings_t::check_and_modify_global_profile(std::optional &undo_data) { + if (!session_handle) return false; + + undo_data.reset(); + NvAPI_Status status; + + if (!get_nvprefs_options().opengl_vulkan_on_dxgi) { + // User requested to leave OpenGL/Vulkan DXGI swapchain setting alone + return true; + } + + NvDRSProfileHandle profile_handle = 0; + status = NvAPI_DRS_GetBaseProfile(session_handle, &profile_handle); + if (status != NVAPI_OK) { + nvapi_error_message(status); + error_message("NvAPI_DRS_GetBaseProfile() failed"); + return false; + } + + NVDRS_SETTING setting = {}; + setting.version = NVDRS_SETTING_VER; + status = NvAPI_DRS_GetSetting(session_handle, profile_handle, OGL_CPL_PREFER_DXPRESENT_ID, &setting); + + // Remember current OpenGL/Vulkan DXGI swapchain setting and change it if needed + if (status == NVAPI_SETTING_NOT_FOUND || (status == NVAPI_OK && setting.u32CurrentValue != OGL_CPL_PREFER_DXPRESENT_PREFER_ENABLED)) { + undo_data = undo_data_t(); + if (status == NVAPI_OK) { + undo_data->set_opengl_swapchain(OGL_CPL_PREFER_DXPRESENT_PREFER_ENABLED, setting.u32CurrentValue); + } + else { + undo_data->set_opengl_swapchain(OGL_CPL_PREFER_DXPRESENT_PREFER_ENABLED, std::nullopt); + } + + setting = {}; + setting.version = NVDRS_SETTING_VER1; + setting.settingId = OGL_CPL_PREFER_DXPRESENT_ID; + setting.settingType = NVDRS_DWORD_TYPE; + setting.settingLocation = NVDRS_CURRENT_PROFILE_LOCATION; + setting.u32CurrentValue = OGL_CPL_PREFER_DXPRESENT_PREFER_ENABLED; + + status = NvAPI_DRS_SetSetting(session_handle, profile_handle, &setting); + if (status != NVAPI_OK) { + nvapi_error_message(status); + error_message("NvAPI_DRS_SetSetting() OGL_CPL_PREFER_DXPRESENT failed"); + return false; + } + + info_message("Changed OGL_CPL_PREFER_DXPRESENT to OGL_CPL_PREFER_DXPRESENT_PREFER_ENABLED for base profile"); + } + else if (status != NVAPI_OK) { + nvapi_error_message(status); + error_message("NvAPI_DRS_GetSetting() OGL_CPL_PREFER_DXPRESENT failed"); + return false; + } + + return true; + } + + bool + driver_settings_t::check_and_modify_application_profile(bool &modified) { + if (!session_handle) return false; + + modified = false; + NvAPI_Status status; + + NvAPI_UnicodeString profile_name = {}; + fill_nvapi_string(profile_name, sunshine_application_profile_name); + + NvDRSProfileHandle profile_handle = 0; + status = NvAPI_DRS_FindProfileByName(session_handle, profile_name, &profile_handle); + + if (status != NVAPI_OK) { + // Create application profile if missing + NVDRS_PROFILE profile = {}; + profile.version = NVDRS_PROFILE_VER1; + fill_nvapi_string(profile.profileName, sunshine_application_profile_name); + status = NvAPI_DRS_CreateProfile(session_handle, &profile, &profile_handle); + if (status != NVAPI_OK) { + nvapi_error_message(status); + error_message("NvAPI_DRS_CreateProfile() failed"); + return false; + } + modified = true; + } + + NvAPI_UnicodeString sunshine_path = {}; + fill_nvapi_string(sunshine_path, sunshine_application_path); + + NVDRS_APPLICATION application = {}; + application.version = NVDRS_APPLICATION_VER_V1; + status = NvAPI_DRS_GetApplicationInfo(session_handle, profile_handle, sunshine_path, &application); + + if (status != NVAPI_OK) { + // Add application to application profile if missing + application.version = NVDRS_APPLICATION_VER_V1; + application.isPredefined = 0; + fill_nvapi_string(application.appName, sunshine_application_path); + fill_nvapi_string(application.userFriendlyName, sunshine_application_path); + fill_nvapi_string(application.launcher, L""); + + status = NvAPI_DRS_CreateApplication(session_handle, profile_handle, &application); + if (status != NVAPI_OK) { + nvapi_error_message(status); + error_message("NvAPI_DRS_CreateApplication() failed"); + return false; + } + modified = true; + } + + NVDRS_SETTING setting = {}; + setting.version = NVDRS_SETTING_VER1; + status = NvAPI_DRS_GetSetting(session_handle, profile_handle, PREFERRED_PSTATE_ID, &setting); + + if (!get_nvprefs_options().sunshine_high_power_mode) { + if (status == NVAPI_OK && + setting.settingLocation == NVDRS_CURRENT_PROFILE_LOCATION) { + // User requested to not use high power mode for sunshine.exe, + // remove the setting from application profile if it's been set previously + + status = NvAPI_DRS_DeleteProfileSetting(session_handle, profile_handle, PREFERRED_PSTATE_ID); + if (status != NVAPI_OK && status != NVAPI_SETTING_NOT_FOUND) { + nvapi_error_message(status); + error_message("NvAPI_DRS_DeleteProfileSetting() PREFERRED_PSTATE failed"); + return false; + } + modified = true; + + info_message(std::wstring(L"Removed PREFERRED_PSTATE for ") + sunshine_application_path); + } + } + else if (status != NVAPI_OK || + setting.settingLocation != NVDRS_CURRENT_PROFILE_LOCATION || + setting.u32CurrentValue != PREFERRED_PSTATE_PREFER_MAX) { + // Set power setting if needed + setting = {}; + setting.version = NVDRS_SETTING_VER1; + setting.settingId = PREFERRED_PSTATE_ID; + setting.settingType = NVDRS_DWORD_TYPE; + setting.settingLocation = NVDRS_CURRENT_PROFILE_LOCATION; + setting.u32CurrentValue = PREFERRED_PSTATE_PREFER_MAX; + + status = NvAPI_DRS_SetSetting(session_handle, profile_handle, &setting); + if (status != NVAPI_OK) { + nvapi_error_message(status); + error_message("NvAPI_DRS_SetSetting() PREFERRED_PSTATE failed"); + return false; + } + modified = true; + + info_message(std::wstring(L"Changed PREFERRED_PSTATE to PREFERRED_PSTATE_PREFER_MAX for ") + sunshine_application_path); + } + + return true; + } + +} // namespace nvprefs diff --git a/src/platform/windows/nvprefs/driver_settings.h b/src/platform/windows/nvprefs/driver_settings.h new file mode 100644 index 00000000000..8e10098bae1 --- /dev/null +++ b/src/platform/windows/nvprefs/driver_settings.h @@ -0,0 +1,45 @@ +#pragma once + +// nvapi headers +// disable clang-format header reordering +// as needs types from +// clang-format off +#include +#include +// clang-format on + +// local includes +#include "undo_data.h" + +namespace nvprefs { + + class driver_settings_t { + public: + ~driver_settings_t(); + + bool + init(); + + void + destroy(); + + bool + load_settings(); + + bool + save_settings(); + + bool + restore_global_profile_to_undo(const undo_data_t &undo_data); + + bool + check_and_modify_global_profile(std::optional &undo_data); + + bool + check_and_modify_application_profile(bool &modified); + + private: + NvDRSSessionHandle session_handle = 0; + }; + +} // namespace nvprefs diff --git a/src/platform/windows/nvprefs/nvapi_opensource_wrapper.cpp b/src/platform/windows/nvprefs/nvapi_opensource_wrapper.cpp new file mode 100644 index 00000000000..3465754839c --- /dev/null +++ b/src/platform/windows/nvprefs/nvapi_opensource_wrapper.cpp @@ -0,0 +1,133 @@ +// standard library headers +#include + +// local includes +#include "driver_settings.h" +#include "nvprefs_common.h" + +// special nvapi header that should be the last include +#include + +namespace { + + std::map interfaces; + HMODULE dll = NULL; + + template + NvAPI_Status + call_interface(const char *name, Args... args) { + auto func = (Func *) interfaces[name]; + + if (!func) { + return interfaces.empty() ? NVAPI_API_NOT_INITIALIZED : NVAPI_NOT_SUPPORTED; + } + + return func(args...); + } + +} // namespace + +#undef NVAPI_INTERFACE +#define NVAPI_INTERFACE NvAPI_Status __cdecl + +extern void *__cdecl nvapi_QueryInterface(NvU32 id); + +NVAPI_INTERFACE +NvAPI_Initialize() { + if (dll) return NVAPI_OK; + +#ifdef _WIN64 + auto dll_name = "nvapi64.dll"; +#else + auto dll_name = "nvapi.dll"; +#endif + + if ((dll = LoadLibraryEx(dll_name, NULL, LOAD_LIBRARY_SEARCH_SYSTEM32))) { + if (auto query_interface = (decltype(nvapi_QueryInterface) *) GetProcAddress(dll, "nvapi_QueryInterface")) { + for (const auto &item : nvapi_interface_table) { + interfaces[item.func] = query_interface(item.id); + } + return NVAPI_OK; + } + } + + NvAPI_Unload(); + return NVAPI_LIBRARY_NOT_FOUND; +} + +NVAPI_INTERFACE +NvAPI_Unload() { + if (dll) { + interfaces.clear(); + FreeLibrary(dll); + dll = NULL; + } + return NVAPI_OK; +} + +NVAPI_INTERFACE +NvAPI_GetErrorMessage(NvAPI_Status nr, NvAPI_ShortString szDesc) { + return call_interface("NvAPI_GetErrorMessage", nr, szDesc); +} + +// This is only a subset of NvAPI_DRS_* functions, more can be added if needed + +NVAPI_INTERFACE +NvAPI_DRS_CreateSession(NvDRSSessionHandle *phSession) { + return call_interface("NvAPI_DRS_CreateSession", phSession); +} + +NVAPI_INTERFACE +NvAPI_DRS_DestroySession(NvDRSSessionHandle hSession) { + return call_interface("NvAPI_DRS_DestroySession", hSession); +} + +NVAPI_INTERFACE +NvAPI_DRS_LoadSettings(NvDRSSessionHandle hSession) { + return call_interface("NvAPI_DRS_LoadSettings", hSession); +} + +NVAPI_INTERFACE +NvAPI_DRS_SaveSettings(NvDRSSessionHandle hSession) { + return call_interface("NvAPI_DRS_SaveSettings", hSession); +} + +NVAPI_INTERFACE +NvAPI_DRS_CreateProfile(NvDRSSessionHandle hSession, NVDRS_PROFILE *pProfileInfo, NvDRSProfileHandle *phProfile) { + return call_interface("NvAPI_DRS_CreateProfile", hSession, pProfileInfo, phProfile); +} + +NVAPI_INTERFACE +NvAPI_DRS_FindProfileByName(NvDRSSessionHandle hSession, NvAPI_UnicodeString profileName, NvDRSProfileHandle *phProfile) { + return call_interface("NvAPI_DRS_FindProfileByName", hSession, profileName, phProfile); +} + +NVAPI_INTERFACE +NvAPI_DRS_CreateApplication(NvDRSSessionHandle hSession, NvDRSProfileHandle hProfile, NVDRS_APPLICATION *pApplication) { + return call_interface("NvAPI_DRS_CreateApplication", hSession, hProfile, pApplication); +} + +NVAPI_INTERFACE +NvAPI_DRS_GetApplicationInfo(NvDRSSessionHandle hSession, NvDRSProfileHandle hProfile, NvAPI_UnicodeString appName, NVDRS_APPLICATION *pApplication) { + return call_interface("NvAPI_DRS_GetApplicationInfo", hSession, hProfile, appName, pApplication); +} + +NVAPI_INTERFACE +NvAPI_DRS_SetSetting(NvDRSSessionHandle hSession, NvDRSProfileHandle hProfile, NVDRS_SETTING *pSetting) { + return call_interface("NvAPI_DRS_SetSetting", hSession, hProfile, pSetting); +} + +NVAPI_INTERFACE +NvAPI_DRS_GetSetting(NvDRSSessionHandle hSession, NvDRSProfileHandle hProfile, NvU32 settingId, NVDRS_SETTING *pSetting) { + return call_interface("NvAPI_DRS_GetSetting", hSession, hProfile, settingId, pSetting); +} + +NVAPI_INTERFACE +NvAPI_DRS_DeleteProfileSetting(NvDRSSessionHandle hSession, NvDRSProfileHandle hProfile, NvU32 settingId) { + return call_interface("NvAPI_DRS_DeleteProfileSetting", hSession, hProfile, settingId); +} + +NVAPI_INTERFACE +NvAPI_DRS_GetBaseProfile(NvDRSSessionHandle hSession, NvDRSProfileHandle *phProfile) { + return call_interface("NvAPI_DRS_GetBaseProfile", hSession, phProfile); +} diff --git a/src/platform/windows/nvprefs/nvprefs_common.cpp b/src/platform/windows/nvprefs/nvprefs_common.cpp new file mode 100644 index 00000000000..367cfc896d9 --- /dev/null +++ b/src/platform/windows/nvprefs/nvprefs_common.cpp @@ -0,0 +1,38 @@ +// local includes +#include "nvprefs_common.h" +#include "src/logging.h" + +// read user override preferences from global sunshine config +#include "src/config.h" + +namespace nvprefs { + + void + info_message(const std::wstring &message) { + BOOST_LOG(info) << "nvprefs: " << message; + } + + void + info_message(const std::string &message) { + BOOST_LOG(info) << "nvprefs: " << message; + } + + void + error_message(const std::wstring &message) { + BOOST_LOG(error) << "nvprefs: " << message; + } + + void + error_message(const std::string &message) { + BOOST_LOG(error) << "nvprefs: " << message; + } + + nvprefs_options + get_nvprefs_options() { + nvprefs_options options; + options.opengl_vulkan_on_dxgi = config::video.nv_opengl_vulkan_on_dxgi; + options.sunshine_high_power_mode = config::video.nv_sunshine_high_power_mode; + return options; + } + +} // namespace nvprefs diff --git a/src/platform/windows/nvprefs/nvprefs_common.h b/src/platform/windows/nvprefs/nvprefs_common.h new file mode 100644 index 00000000000..2b286d9e8ad --- /dev/null +++ b/src/platform/windows/nvprefs/nvprefs_common.h @@ -0,0 +1,56 @@ +#pragma once + +// sunshine utility header for generic smart pointers +#include "src/utility.h" + +// winapi headers +// disable clang-format header reordering +// clang-format off +#include +#include +// clang-format on + +namespace nvprefs { + + struct safe_handle: public util::safe_ptr_v2 { + using util::safe_ptr_v2::safe_ptr_v2; + explicit + operator bool() const { + auto handle = get(); + return handle != NULL && handle != INVALID_HANDLE_VALUE; + } + }; + + struct safe_hlocal_deleter { + void + operator()(void *p) { + LocalFree(p); + } + }; + + template + using safe_hlocal = util::uniq_ptr, safe_hlocal_deleter>; + + using safe_sid = util::safe_ptr_v2; + + void + info_message(const std::wstring &message); + + void + info_message(const std::string &message); + + void + error_message(const std::wstring &message); + + void + error_message(const std::string &message); + + struct nvprefs_options { + bool opengl_vulkan_on_dxgi = true; + bool sunshine_high_power_mode = true; + }; + + nvprefs_options + get_nvprefs_options(); + +} // namespace nvprefs diff --git a/src/platform/windows/nvprefs/nvprefs_interface.cpp b/src/platform/windows/nvprefs/nvprefs_interface.cpp new file mode 100644 index 00000000000..628248ad619 --- /dev/null +++ b/src/platform/windows/nvprefs/nvprefs_interface.cpp @@ -0,0 +1,225 @@ +// standard includes +#include + +// local includes +#include "driver_settings.h" +#include "nvprefs_interface.h" +#include "undo_file.h" + +namespace { + + const auto sunshine_program_data_folder = "Sunshine"; + const auto nvprefs_undo_file_name = "nvprefs_undo.json"; + +} // namespace + +namespace nvprefs { + + struct nvprefs_interface::impl { + bool loaded = false; + driver_settings_t driver_settings; + std::filesystem::path undo_folder_path; + std::filesystem::path undo_file_path; + std::optional undo_data; + std::optional undo_file; + }; + + nvprefs_interface::nvprefs_interface(): + pimpl(new impl()) { + } + + nvprefs_interface::~nvprefs_interface() { + if (owning_undo_file() && load()) { + restore_global_profile(); + } + unload(); + } + + bool + nvprefs_interface::load() { + if (!pimpl->loaded) { + // Check %ProgramData% variable, need it for storing undo file + wchar_t program_data_env[MAX_PATH]; + auto get_env_result = GetEnvironmentVariableW(L"ProgramData", program_data_env, MAX_PATH); + if (get_env_result == 0 || get_env_result >= MAX_PATH || !std::filesystem::is_directory(program_data_env)) { + error_message("Missing or malformed %ProgramData% environment variable"); + return false; + } + + // Prepare undo file path variables + pimpl->undo_folder_path = std::filesystem::path(program_data_env) / sunshine_program_data_folder; + pimpl->undo_file_path = pimpl->undo_folder_path / nvprefs_undo_file_name; + + // Dynamically load nvapi library and load driver settings + pimpl->loaded = pimpl->driver_settings.init(); + } + + return pimpl->loaded; + } + + void + nvprefs_interface::unload() { + if (pimpl->loaded) { + // Unload dynamically loaded nvapi library + pimpl->driver_settings.destroy(); + pimpl->loaded = false; + } + } + + bool + nvprefs_interface::restore_from_and_delete_undo_file_if_exists() { + if (!pimpl->loaded) return false; + + // Check for undo file from previous improper termination + bool access_denied = false; + if (auto undo_file = undo_file_t::open_existing_file(pimpl->undo_file_path, access_denied)) { + // Try to restore from the undo file + info_message("Opened undo file from previous improper termination"); + if (auto undo_data = undo_file->read_undo_data()) { + if (pimpl->driver_settings.restore_global_profile_to_undo(*undo_data) && pimpl->driver_settings.save_settings()) { + info_message("Restored global profile settings from undo file - deleting the file"); + } + else { + error_message("Failed to restore global profile settings from undo file, deleting the file anyway"); + } + } + else { + error_message("Coulnd't read undo file, deleting the file anyway"); + } + + if (!undo_file->delete_file()) { + error_message("Couldn't delete undo file"); + return false; + } + } + else if (access_denied) { + error_message("Couldn't open undo file from previous improper termination, or confirm that there's no such file"); + return false; + } + + return true; + } + + bool + nvprefs_interface::modify_application_profile() { + if (!pimpl->loaded) return false; + + // Modify and save sunshine.exe application profile settings, if needed + bool modified = false; + if (!pimpl->driver_settings.check_and_modify_application_profile(modified)) { + error_message("Failed to modify application profile settings"); + return false; + } + else if (modified) { + if (pimpl->driver_settings.save_settings()) { + info_message("Modified application profile settings"); + } + else { + error_message("Couldn't save application profile settings"); + return false; + } + } + else { + info_message("No need to modify application profile settings"); + } + + return true; + } + + bool + nvprefs_interface::modify_global_profile() { + if (!pimpl->loaded) return false; + + // Modify but not save global profile settings, if needed + std::optional undo_data; + if (!pimpl->driver_settings.check_and_modify_global_profile(undo_data)) { + error_message("Couldn't modify global profile settings"); + return false; + } + else if (!undo_data) { + info_message("No need to modify global profile settings"); + return true; + } + + auto make_undo_and_commit = [&]() -> bool { + // Create and lock undo file if it hasn't been done yet + if (!pimpl->undo_file) { + // Prepare Sunshine folder in ProgramData if it doesn't exist + if (!CreateDirectoryW(pimpl->undo_folder_path.c_str(), nullptr) && GetLastError() != ERROR_ALREADY_EXISTS) { + error_message("Couldn't create undo folder"); + return false; + } + + // Create undo file to handle improper termination of nvprefs.exe + pimpl->undo_file = undo_file_t::create_new_file(pimpl->undo_file_path); + if (!pimpl->undo_file) { + error_message("Couldn't create undo file"); + return false; + } + } + + assert(undo_data); + if (pimpl->undo_data) { + // Merge undo data if settings has been modified externally since our last modification + pimpl->undo_data->merge(*undo_data); + } + else { + pimpl->undo_data = undo_data; + } + + // Write undo data to undo file + if (!pimpl->undo_file->write_undo_data(*pimpl->undo_data)) { + error_message("Couldn't write to undo file - deleting the file"); + if (!pimpl->undo_file->delete_file()) { + error_message("Couldn't delete undo file"); + } + return false; + } + + // Save global profile settings + if (!pimpl->driver_settings.save_settings()) { + error_message("Couldn't save global profile settings"); + return false; + } + + return true; + }; + + if (!make_undo_and_commit()) { + // Revert settings modifications + pimpl->driver_settings.load_settings(); + return false; + } + + return true; + } + + bool + nvprefs_interface::owning_undo_file() { + return pimpl->undo_file.has_value(); + } + + bool + nvprefs_interface::restore_global_profile() { + if (!pimpl->loaded || !pimpl->undo_data || !pimpl->undo_file) return false; + + // Restore global profile settings with undo data + if (pimpl->driver_settings.restore_global_profile_to_undo(*pimpl->undo_data) && + pimpl->driver_settings.save_settings()) { + // Global profile settings sucessfully restored, can delete undo file + if (!pimpl->undo_file->delete_file()) { + error_message("Couldn't delete undo file"); + return false; + } + pimpl->undo_data = std::nullopt; + pimpl->undo_file = std::nullopt; + } + else { + error_message("Couldn't restore global profile settings"); + return false; + } + + return true; + } + +} // namespace nvprefs diff --git a/src/platform/windows/nvprefs/nvprefs_interface.h b/src/platform/windows/nvprefs/nvprefs_interface.h new file mode 100644 index 00000000000..583e72c540d --- /dev/null +++ b/src/platform/windows/nvprefs/nvprefs_interface.h @@ -0,0 +1,39 @@ +#pragma once + +// standard library headers +#include + +namespace nvprefs { + + class nvprefs_interface { + public: + nvprefs_interface(); + ~nvprefs_interface(); + + bool + load(); + + void + unload(); + + bool + restore_from_and_delete_undo_file_if_exists(); + + bool + modify_application_profile(); + + bool + modify_global_profile(); + + bool + owning_undo_file(); + + bool + restore_global_profile(); + + private: + struct impl; + std::unique_ptr pimpl; + }; + +} // namespace nvprefs diff --git a/src/platform/windows/nvprefs/undo_data.cpp b/src/platform/windows/nvprefs/undo_data.cpp new file mode 100644 index 00000000000..388b02cface --- /dev/null +++ b/src/platform/windows/nvprefs/undo_data.cpp @@ -0,0 +1,118 @@ +// external includes +#include + +// local includes +#include "nvprefs_common.h" +#include "undo_data.h" + +using json = nlohmann::json; + +// Separate namespace for ADL, otherwise we need to define json +// functions in the same namespace as our types +namespace nlohmann { + using data_t = nvprefs::undo_data_t::data_t; + using opengl_swapchain_t = data_t::opengl_swapchain_t; + + template + struct adl_serializer> { + static void + to_json(json &j, const std::optional &opt) { + if (opt == std::nullopt) { + j = nullptr; + } + else { + j = *opt; + } + } + + static void + from_json(const json &j, std::optional &opt) { + if (j.is_null()) { + opt = std::nullopt; + } + else { + opt = j.template get(); + } + } + }; + + template <> + struct adl_serializer { + static void + to_json(json &j, const data_t &data) { + j = json { { "opengl_swapchain", data.opengl_swapchain } }; + } + + static void + from_json(const json &j, data_t &data) { + j.at("opengl_swapchain").get_to(data.opengl_swapchain); + } + }; + + template <> + struct adl_serializer { + static void + to_json(json &j, const opengl_swapchain_t &opengl_swapchain) { + j = json { + { "our_value", opengl_swapchain.our_value }, + { "undo_value", opengl_swapchain.undo_value } + }; + } + + static void + from_json(const json &j, opengl_swapchain_t &opengl_swapchain) { + j.at("our_value").get_to(opengl_swapchain.our_value); + j.at("undo_value").get_to(opengl_swapchain.undo_value); + } + }; +} // namespace nlohmann + +namespace nvprefs { + + void + undo_data_t::set_opengl_swapchain(uint32_t our_value, std::optional undo_value) { + data.opengl_swapchain = data_t::opengl_swapchain_t { + our_value, + undo_value + }; + } + + std::optional + undo_data_t::get_opengl_swapchain() const { + return data.opengl_swapchain; + } + + std::string + undo_data_t::write() const { + try { + // Keep this assignment otherwise data will be treated as an array due to + // initializer list shenanigangs. + const json json_data = data; + return json_data.dump(); + } + catch (const std::exception &err) { + error_message(std::string { "failed to serialize json data" }); + return {}; + } + } + + void + undo_data_t::read(const std::vector &buffer) { + try { + data = json::parse(std::begin(buffer), std::end(buffer)); + } + catch (const std::exception &err) { + error_message(std::string { "failed to parse json data: " } + err.what()); + data = {}; + } + } + + void + undo_data_t::merge(const undo_data_t &newer_data) { + const auto &swapchain_data = newer_data.get_opengl_swapchain(); + if (swapchain_data) { + set_opengl_swapchain(swapchain_data->our_value, swapchain_data->undo_value); + } + } + +} // namespace nvprefs diff --git a/src/platform/windows/nvprefs/undo_data.h b/src/platform/windows/nvprefs/undo_data.h new file mode 100644 index 00000000000..d5f30251348 --- /dev/null +++ b/src/platform/windows/nvprefs/undo_data.h @@ -0,0 +1,41 @@ +#pragma once + +// standard library headers +#include +#include +#include +#include + +namespace nvprefs { + + class undo_data_t { + public: + struct data_t { + struct opengl_swapchain_t { + uint32_t our_value; + std::optional undo_value; + }; + + std::optional opengl_swapchain; + }; + + void + set_opengl_swapchain(uint32_t our_value, std::optional undo_value); + + std::optional + get_opengl_swapchain() const; + + std::string + write() const; + + void + read(const std::vector &buffer); + + void + merge(const undo_data_t &newer_data); + + private: + data_t data; + }; + +} // namespace nvprefs diff --git a/src/platform/windows/nvprefs/undo_file.cpp b/src/platform/windows/nvprefs/undo_file.cpp new file mode 100644 index 00000000000..9f2648ab997 --- /dev/null +++ b/src/platform/windows/nvprefs/undo_file.cpp @@ -0,0 +1,153 @@ +// local includes +#include "undo_file.h" + +namespace { + + using namespace nvprefs; + + DWORD + relax_permissions(HANDLE file_handle) { + PACL old_dacl = nullptr; + + safe_hlocal sd; + DWORD status = GetSecurityInfo(file_handle, SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, nullptr, nullptr, &old_dacl, nullptr, &sd); + if (status != ERROR_SUCCESS) return status; + + safe_sid users_sid; + SID_IDENTIFIER_AUTHORITY nt_authorithy = SECURITY_NT_AUTHORITY; + if (!AllocateAndInitializeSid(&nt_authorithy, 2, SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_USERS, 0, 0, 0, 0, 0, 0, &users_sid)) { + return GetLastError(); + } + + EXPLICIT_ACCESS ea = {}; + ea.grfAccessPermissions = GENERIC_READ | GENERIC_WRITE | DELETE; + ea.grfAccessMode = GRANT_ACCESS; + ea.grfInheritance = NO_INHERITANCE; + ea.Trustee.TrusteeForm = TRUSTEE_IS_SID; + ea.Trustee.ptstrName = (LPTSTR) users_sid.get(); + + safe_hlocal new_dacl; + status = SetEntriesInAcl(1, &ea, old_dacl, &new_dacl); + if (status != ERROR_SUCCESS) return status; + + status = SetSecurityInfo(file_handle, SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, nullptr, nullptr, new_dacl.get(), nullptr); + if (status != ERROR_SUCCESS) return status; + + return 0; + } + +} // namespace + +namespace nvprefs { + + std::optional + undo_file_t::open_existing_file(std::filesystem::path file_path, bool &access_denied) { + undo_file_t file; + file.file_handle.reset(CreateFileW(file_path.c_str(), GENERIC_READ | DELETE, 0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL)); + if (file.file_handle) { + access_denied = false; + return file; + } + else { + auto last_error = GetLastError(); + access_denied = (last_error != ERROR_FILE_NOT_FOUND && last_error != ERROR_PATH_NOT_FOUND); + return std::nullopt; + } + } + + std::optional + undo_file_t::create_new_file(std::filesystem::path file_path) { + undo_file_t file; + file.file_handle.reset(CreateFileW(file_path.c_str(), GENERIC_WRITE | STANDARD_RIGHTS_ALL, 0, nullptr, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL)); + + if (file.file_handle) { + // give GENERIC_READ, GENERIC_WRITE and DELETE permissions to Users group + if (relax_permissions(file.file_handle.get()) != 0) { + error_message("Failed to relax permissions on undo file"); + } + return file; + } + else { + return std::nullopt; + } + } + + bool + undo_file_t::delete_file() { + if (!file_handle) return false; + + FILE_DISPOSITION_INFO delete_file_info = { TRUE }; + if (SetFileInformationByHandle(file_handle.get(), FileDispositionInfo, &delete_file_info, sizeof(delete_file_info))) { + file_handle.reset(); + return true; + } + else { + return false; + } + } + + bool + undo_file_t::write_undo_data(const undo_data_t &undo_data) { + if (!file_handle) return false; + + std::string buffer; + try { + buffer = undo_data.write(); + } + catch (...) { + error_message("Couldn't serialize undo data"); + return false; + } + + if (!SetFilePointerEx(file_handle.get(), {}, nullptr, FILE_BEGIN) || !SetEndOfFile(file_handle.get())) { + error_message("Couldn't clear undo file"); + return false; + } + + DWORD bytes_written = 0; + if (!WriteFile(file_handle.get(), buffer.data(), buffer.size(), &bytes_written, nullptr) || bytes_written != buffer.size()) { + error_message("Couldn't write undo file"); + return false; + } + + if (!FlushFileBuffers(file_handle.get())) { + error_message("Failed to flush undo file"); + } + + return true; + } + + std::optional + undo_file_t::read_undo_data() { + if (!file_handle) return std::nullopt; + + LARGE_INTEGER file_size; + if (!GetFileSizeEx(file_handle.get(), &file_size)) { + error_message("Couldn't get undo file size"); + return std::nullopt; + } + + if ((size_t) file_size.QuadPart > 1024) { + error_message("Undo file size is unexpectedly large, aborting"); + return std::nullopt; + } + + std::vector buffer(file_size.QuadPart); + DWORD bytes_read = 0; + if (!ReadFile(file_handle.get(), buffer.data(), buffer.size(), &bytes_read, nullptr) || bytes_read != buffer.size()) { + error_message("Couldn't read undo file"); + return std::nullopt; + } + + undo_data_t undo_data; + try { + undo_data.read(buffer); + } + catch (...) { + error_message("Couldn't parse undo file"); + return std::nullopt; + } + return undo_data; + } + +} // namespace nvprefs diff --git a/src/platform/windows/nvprefs/undo_file.h b/src/platform/windows/nvprefs/undo_file.h new file mode 100644 index 00000000000..acbfbae2757 --- /dev/null +++ b/src/platform/windows/nvprefs/undo_file.h @@ -0,0 +1,34 @@ +#pragma once + +// standard library headers +#include + +// local includes +#include "nvprefs_common.h" +#include "undo_data.h" + +namespace nvprefs { + + class undo_file_t { + public: + static std::optional + open_existing_file(std::filesystem::path file_path, bool &access_denied); + + static std::optional + create_new_file(std::filesystem::path file_path); + + bool + delete_file(); + + bool + write_undo_data(const undo_data_t &undo_data); + + std::optional + read_undo_data(); + + private: + undo_file_t() = default; + safe_handle file_handle; + }; + +} // namespace nvprefs diff --git a/src/platform/windows/publish.cpp b/src/platform/windows/publish.cpp index 3d9383f7e49..131bb5ac11f 100644 --- a/src/platform/windows/publish.cpp +++ b/src/platform/windows/publish.cpp @@ -13,7 +13,7 @@ #include "misc.h" #include "src/config.h" -#include "src/main.h" +#include "src/logging.h" #include "src/network.h" #include "src/nvhttp.h" #include "src/platform/common.h" @@ -29,9 +29,8 @@ using namespace std::literals; #define SV(quote) __SV(quote) extern "C" { -constexpr auto DNS_REQUEST_PENDING = 9506L; - #ifndef __MINGW32__ +constexpr auto DNS_REQUEST_PENDING = 9506L; constexpr auto DNS_QUERY_REQUEST_VERSION1 = 0x1; constexpr auto DNS_QUERY_RESULTS_VERSION1 = 0x1; #endif @@ -108,18 +107,31 @@ namespace platf::publish { service(bool enable, PDNS_SERVICE_INSTANCE &existing_instance) { auto alarm = safe::make_alarm(); - std::wstring_convert, wchar_t> converter; - std::wstring name { SERVICE_INSTANCE_NAME.data(), SERVICE_INSTANCE_NAME.size() }; std::wstring domain { SERVICE_TYPE_DOMAIN.data(), SERVICE_TYPE_DOMAIN.size() }; - auto host = converter.from_bytes(boost::asio::ip::host_name() + ".local"); + auto host = from_utf8(boost::asio::ip::host_name() + ".local"); DNS_SERVICE_INSTANCE instance {}; instance.pszInstanceName = name.data(); - instance.wPort = map_port(nvhttp::PORT_HTTP); + instance.wPort = net::map_port(nvhttp::PORT_HTTP); instance.pszHostName = host.data(); + // Setting these values ensures Windows mDNS answers comply with RFC 1035. + // If these are unset, Windows will send a TXT record that has zero strings, + // which is illegal. Setting them to a single empty value causes Windows to + // send a single empty string for the TXT record, which is the correct thing + // to do when advertising a service without any TXT strings. + // + // Most clients aren't strictly checking TXT record compliance with RFC 1035, + // but Apple's mDNS resolver does and rejects the entire answer if an invalid + // TXT record is present. + PWCHAR keys[] = { nullptr }; + PWCHAR values[] = { nullptr }; + instance.dwPropertyCount = 1; + instance.keys = keys; + instance.values = values; + DNS_SERVICE_REGISTER_REQUEST req {}; req.Version = DNS_QUERY_REQUEST_VERSION1; req.pQueryContext = alarm.get(); diff --git a/src/platform/windows/windows.rs.in b/src/platform/windows/windows.rs.in index 1a56eeffdf3..3b1877461ea 100644 --- a/src/platform/windows/windows.rs.in +++ b/src/platform/windows/windows.rs.in @@ -1 +1,36 @@ -SuperDuperAmazing ICON DISCARDABLE "@SUNSHINE_ICON_PATH@" \ No newline at end of file +#include "winver.h" +VS_VERSION_INFO VERSIONINFO +FILEVERSION @PROJECT_VERSION_MAJOR@,@PROJECT_VERSION_MINOR@,@PROJECT_VERSION_PATCH@,0 +PRODUCTVERSION @PROJECT_VERSION_MAJOR@,@PROJECT_VERSION_MINOR@,@PROJECT_VERSION_PATCH@,0 +FILEOS VOS__WINDOWS32 +FILETYPE VFT_APP +FILESUBTYPE VFT2_UNKNOWN +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904E4" + BEGIN + VALUE "CompanyName", "LizardByte\0" + VALUE "FileDescription", "Sunshine\0" + VALUE "FileVersion", "@PROJECT_VERSION@\0" + VALUE "InternalName", "Sunshine\0" + VALUE "LegalCopyright", "https://raw.githubusercontent.com/LizardByte/Sunshine/master/LICENSE\0" + VALUE "ProductName", "Sunshine\0" + VALUE "ProductVersion", "@PROJECT_VERSION@\0" + END + END + + BLOCK "VarFileInfo" + BEGIN + /* The following line should only be modified for localized versions. */ + /* It consists of any number of WORD,WORD pairs, with each pair */ + /* describing a language,codepage combination supported by the file. */ + /* */ + /* For example, a file might have values "0x409,1252" indicating that it */ + /* supports English language (0x409) in the Windows ANSI codepage (1252). */ + + VALUE "Translation", 0x409, 1252 + + END +END +SuperDuperAmazing ICON DISCARDABLE "@SUNSHINE_ICON_PATH@" diff --git a/src/process.cpp b/src/process.cpp index 1a0f8e53e7b..32af14ebd9d 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -1,6 +1,6 @@ /** * @file src/process.cpp - * @brief todo + * @brief Handles the startup and shutdown of the apps started by a streaming Session. */ #define BOOST_BIND_GLOBAL_PLACEHOLDERS @@ -8,6 +8,7 @@ #include #include +#include #include #include @@ -22,11 +23,15 @@ #include "config.h" #include "crypto.h" -#include "main.h" +#include "logging.h" #include "platform/common.h" +#include "system_tray.h" #include "utility.h" #ifdef _WIN32 + // from_utf8() string conversion function + #include "platform/windows/misc.h" + // _SH constants for _wfsopen() #include #endif @@ -56,17 +61,51 @@ namespace proc { return std::make_unique(); } + /** + * @brief Terminates all child processes in a process group. + * @param proc The child process itself. + * @param group The group of all children in the process tree. + * @param exit_timeout The timeout to wait for the process group to gracefully exit. + */ void - process_end(bp::child &proc, bp::group &proc_handle) { - if (!proc.running()) { - return; - } + terminate_process_group(bp::child &proc, bp::group &group, std::chrono::seconds exit_timeout) { + if (group.valid() && platf::process_group_running((std::uintptr_t) group.native_handle())) { + if (exit_timeout.count() > 0) { + // Request processes in the group to exit gracefully + if (platf::request_process_group_exit((std::uintptr_t) group.native_handle())) { + // If the request was successful, wait for a little while for them to exit. + BOOST_LOG(info) << "Successfully requested the app to exit. Waiting up to "sv << exit_timeout.count() << " seconds for it to close."sv; + + // group::wait_for() and similar functions are broken and deprecated, so we use a simple polling loop + while (platf::process_group_running((std::uintptr_t) group.native_handle()) && (--exit_timeout).count() >= 0) { + std::this_thread::sleep_for(1s); + } - BOOST_LOG(debug) << "Force termination Child-Process"sv; - proc_handle.terminate(); + if (exit_timeout.count() < 0) { + BOOST_LOG(warning) << "App did not fully exit within the timeout. Terminating the app's remaining processes."sv; + } + else { + BOOST_LOG(info) << "All app processes have successfully exited."sv; + } + } + else { + BOOST_LOG(info) << "App did not respond to a graceful termination request. Forcefully terminating the app's processes."sv; + } + } + else { + BOOST_LOG(info) << "No graceful exit timeout was specified for this app. Forcefully terminating the app's processes."sv; + } + + // We always call terminate() even if we waited successfully for all processes above. + // This ensures the process group state is consistent with the OS in boost. + group.terminate(); + group.detach(); + } - // avoid zombie process - proc.wait(); + if (proc.valid()) { + // avoid zombie process + proc.detach(); + } } boost::filesystem::path @@ -82,7 +121,12 @@ namespace proc { return boost::filesystem::path(); } - BOOST_LOG(debug) << "Parsed executable ["sv << parts.at(0) << "] from command ["sv << cmd << ']'; + BOOST_LOG(debug) << "Parsed target ["sv << parts.at(0) << "] from command ["sv << cmd << ']'; + + // If the target is a URL, don't parse any further here + if (parts.at(0).find("://") != std::string::npos) { + return boost::filesystem::path(); + } // If the cmd path is not an absolute path, resolve it using our PATH variable boost::filesystem::path cmd_path(parts.at(0)); @@ -94,14 +138,14 @@ namespace proc { } } - BOOST_LOG(debug) << "Resolved executable ["sv << parts.at(0) << "] to path ["sv << cmd_path << ']'; + BOOST_LOG(debug) << "Resolved target ["sv << parts.at(0) << "] to path ["sv << cmd_path << ']'; // Now that we have a complete path, we can just use parent_path() return cmd_path.parent_path(); } int - proc_t::execute(int app_id) { + proc_t::execute(int app_id, std::shared_ptr launch_session) { // Ensure starting from a clean slate terminate(); @@ -116,16 +160,38 @@ namespace proc { _app_id = app_id; _app = *iter; - _app_prep_begin = std::begin(_app.prep_cmds); _app_prep_it = _app_prep_begin; + // Add Stream-specific environment variables + _env["SUNSHINE_APP_ID"] = std::to_string(_app_id); + _env["SUNSHINE_APP_NAME"] = _app.name; + _env["SUNSHINE_CLIENT_WIDTH"] = std::to_string(launch_session->width); + _env["SUNSHINE_CLIENT_HEIGHT"] = std::to_string(launch_session->height); + _env["SUNSHINE_CLIENT_FPS"] = std::to_string(launch_session->fps); + _env["SUNSHINE_CLIENT_HDR"] = launch_session->enable_hdr ? "true" : "false"; + _env["SUNSHINE_CLIENT_GCMAP"] = std::to_string(launch_session->gcmap); + _env["SUNSHINE_CLIENT_HOST_AUDIO"] = launch_session->host_audio ? "true" : "false"; + _env["SUNSHINE_CLIENT_ENABLE_SOPS"] = launch_session->enable_sops ? "true" : "false"; + int channelCount = launch_session->surround_info & (65535); + switch (channelCount) { + case 2: + _env["SUNSHINE_CLIENT_AUDIO_CONFIGURATION"] = "2.0"; + break; + case 6: + _env["SUNSHINE_CLIENT_AUDIO_CONFIGURATION"] = "5.1"; + break; + case 8: + _env["SUNSHINE_CLIENT_AUDIO_CONFIGURATION"] = "7.1"; + break; + } + _env["SUNSHINE_CLIENT_AUDIO_SURROUND_PARAMS"] = launch_session->surround_params; + if (!_app.output.empty() && _app.output != "null"sv) { #ifdef _WIN32 // fopen() interprets the filename as an ANSI string on Windows, so we must convert it // to UTF-16 and use the wchar_t variants for proper Unicode log file path support. - std::wstring_convert, wchar_t> converter; - auto woutput = converter.from_bytes(_app.output); + auto woutput = platf::from_utf8(_app.output); // Use _SH_DENYNO to allow us to open this log file again for writing even if it is // still open from a previous execution. This is required to handle the case of a @@ -158,13 +224,17 @@ namespace proc { if (ec) { BOOST_LOG(error) << "Couldn't run ["sv << cmd.do_cmd << "]: System: "sv << ec.message(); - return -1; + // We don't want any prep commands failing launch of the desktop. + // This is to prevent the issue where users reboot their PC and need to log in with Sunshine. + // permission_denied is typically returned when the user impersonation fails, which can happen when user is not signed in yet. + if (!(_app.cmd.empty() && ec == std::errc::permission_denied)) { + return -1; + } } child.wait(); auto ret = child.exit_code(); - - if (ret != 0) { + if (ret != 0 && ec != std::errc::permission_denied) { BOOST_LOG(error) << '[' << cmd.do_cmd << "] failed with code ["sv << ret << ']'; return -1; } @@ -193,13 +263,15 @@ namespace proc { find_working_directory(_app.cmd, _env) : boost::filesystem::path(_app.working_dir); BOOST_LOG(info) << "Executing: ["sv << _app.cmd << "] in ["sv << working_dir << ']'; - _process = platf::run_command(_app.elevated, true, _app.cmd, working_dir, _env, _pipe.get(), ec, &_process_handle); + _process = platf::run_command(_app.elevated, true, _app.cmd, working_dir, _env, _pipe.get(), ec, &_process_group); if (ec) { BOOST_LOG(warning) << "Couldn't run ["sv << _app.cmd << "]: System: "sv << ec.message(); return -1; } } + _app_launch_time = std::chrono::steady_clock::now(); + fg.disable(); return 0; @@ -207,12 +279,28 @@ namespace proc { int proc_t::running() { - if (placebo || _process.running()) { + if (placebo) { + return _app_id; + } + else if (_app.wait_all && _process_group && platf::process_group_running((std::uintptr_t) _process_group.native_handle())) { + // The app is still running if any process in the group is still running + return _app_id; + } + else if (_process.running()) { + // The app is still running only if the initial process launched is still running + return _app_id; + } + else if (_app.auto_detach && _process.native_exit_code() == 0 && + std::chrono::steady_clock::now() - _app_launch_time < 5s) { + BOOST_LOG(info) << "App exited gracefully within 5 seconds of launch. Treating the app as a detached command."sv; + BOOST_LOG(info) << "Adjust this behavior in the Applications tab or apps.json if this is not what you want."sv; + placebo = true; return _app_id; } // Perform cleanup actions now if needed if (_process) { + BOOST_LOG(info) << "App exited with code ["sv << _process.native_exit_code() << ']'; terminate(); } @@ -222,13 +310,10 @@ namespace proc { void proc_t::terminate() { std::error_code ec; - - // Ensure child process is terminated placebo = false; - process_end(_process, _process_handle); + terminate_process_group(_process, _process_group, _app.exit_timeout); _process = bp::child(); - _process_handle = bp::group(); - _app_id = -1; + _process_group = bp::group(); for (; _app_prep_it != _app_prep_begin; --_app_prep_it) { auto &cmd = *(_app_prep_it - 1); @@ -256,6 +341,17 @@ namespace proc { } _pipe.reset(); +#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 + bool has_run = _app_id > 0; + + // Only show the Stopped notification if we actually have an app to stop + // Since terminate() is always run when a new app has started + if (proc::proc.get_last_run_app_name().length() > 0 && has_run) { + system_tray::update_tray_stopped(proc::proc.get_last_run_app_name()); + } +#endif + + _app_id = -1; } const std::vector & @@ -281,6 +377,11 @@ namespace proc { return validate_app_image_path(app_image_path); } + std::string + proc_t::get_last_run_app_name() { + return _app.name; + } + proc_t::~proc_t() { // It's not safe to call terminate() here because our proc_t is a static variable // that may be destroyed after the Boost loggers have been destroyed. Instead, @@ -510,6 +611,9 @@ namespace proc { auto image_path = app_node.get_optional("image-path"s); auto working_dir = app_node.get_optional("working-dir"s); auto elevated = app_node.get_optional("elevated"s); + auto auto_detach = app_node.get_optional("auto-detach"s); + auto wait_all = app_node.get_optional("wait-all"s); + auto exit_timeout = app_node.get_optional("exit-timeout"s); std::vector prep_cmds; if (!exclude_global_prep.value_or(false)) { @@ -561,6 +665,12 @@ namespace proc { if (working_dir) { ctx.working_dir = parse_env_val(this_env, *working_dir); +#ifdef _WIN32 + // The working directory, unlike the command itself, should not be quoted + // when it contains spaces. Unlike POSIX, Windows forbids quotes in paths, + // so we can safely strip them all out here to avoid confusing the user. + boost::erase_all(ctx.working_dir, "\""); +#endif } if (image_path) { @@ -568,6 +678,9 @@ namespace proc { } ctx.elevated = elevated.value_or(false); + ctx.auto_detach = auto_detach.value_or(true); + ctx.wait_all = wait_all.value_or(true); + ctx.exit_timeout = std::chrono::seconds { exit_timeout.value_or(5) }; auto possible_ids = calculate_app_id(name, ctx.image_path, i++); if (ids.count(std::get<0>(possible_ids)) == 0) { diff --git a/src/process.h b/src/process.h index 038e86f0e82..c8754992652 100644 --- a/src/process.h +++ b/src/process.h @@ -15,6 +15,7 @@ #include "config.h" #include "platform/common.h" +#include "rtsp.h" #include "utility.h" namespace proc { @@ -37,10 +38,16 @@ namespace proc { std::vector prep_cmds; /** - * Some applications, such as Steam, - * either exit quickly, or keep running indefinitely. - * Steam.exe is one such application. - * That is why some applications need be run and forgotten about + * Some applications, such as Steam, either exit quickly, or keep running indefinitely. + * + * Apps that launch normal child processes and terminate will be handled by the process + * grouping logic (wait_all). However, apps that launch child processes indirectly or + * into another process group (such as UWP apps) can only be handled by the auto-detach + * heuristic which catches processes that exit 0 very quickly, but we won't have proper + * process tracking for those. + * + * For cases where users just want to kick off a background process and never manage the + * lifetime of that process, they can use detached commands for that. */ std::vector detached; @@ -51,6 +58,9 @@ namespace proc { std::string image_path; std::string id; bool elevated; + bool auto_detach; + bool wait_all; + std::chrono::seconds exit_timeout; }; class proc_t { @@ -65,7 +75,7 @@ namespace proc { _apps(std::move(apps)) {} int - execute(int app_id); + execute(int app_id, std::shared_ptr launch_session); /** * @return _app_id if a process is running, otherwise returns 0 @@ -81,7 +91,8 @@ namespace proc { get_apps(); std::string get_app_image(int app_id); - + std::string + get_last_run_app_name(); void terminate(); @@ -91,12 +102,13 @@ namespace proc { boost::process::environment _env; std::vector _apps; ctx_t _app; + std::chrono::steady_clock::time_point _app_launch_time; // If no command associated with _app_id, yet it's still running bool placebo {}; boost::process::child _process; - boost::process::group _process_handle; + boost::process::group _process_group; file_t _pipe; std::vector::const_iterator _app_prep_it; diff --git a/src/rtsp.cpp b/src/rtsp.cpp index a2e4a2fc3b1..d690c0c7c3f 100644 --- a/src/rtsp.cpp +++ b/src/rtsp.cpp @@ -5,18 +5,21 @@ #define BOOST_BIND_GLOBAL_PLACEHOLDERS extern "C" { +#include #include } #include #include +#include #include #include #include "config.h" +#include "globals.h" #include "input.h" -#include "main.h" +#include "logging.h" #include "network.h" #include "rtsp.h" #include "stream.h" @@ -40,51 +43,225 @@ namespace rtsp_stream { delete msg; } +#pragma pack(push, 1) + + struct encrypted_rtsp_header_t { + // We set the MSB in encrypted RTSP messages to allow format-agnostic + // parsing code to be able to tell encrypted from plaintext messages. + static constexpr std::uint32_t ENCRYPTED_MESSAGE_TYPE_BIT = 0x80000000; + + uint8_t * + payload() { + return (uint8_t *) (this + 1); + } + + std::uint32_t + payload_length() { + return util::endian::big(typeAndLength) & ~ENCRYPTED_MESSAGE_TYPE_BIT; + } + + bool + is_encrypted() { + return !!(util::endian::big(typeAndLength) & ENCRYPTED_MESSAGE_TYPE_BIT); + } + + // This field is the length of the payload + ENCRYPTED_MESSAGE_TYPE_BIT in big-endian + std::uint32_t typeAndLength; + + // This field is the number used to initialize the bottom 4 bytes of the AES IV in big-endian + std::uint32_t sequenceNumber; + + // This field is the AES GCM authentication tag + std::uint8_t tag[16]; + }; + +#pragma pack(pop) + class rtsp_server_t; using msg_t = util::safe_ptr; - using cmd_func_t = std::function; + using cmd_func_t = std::function; void print_msg(PRTSP_MESSAGE msg); void - cmd_not_found(tcp::socket &sock, msg_t &&req); + cmd_not_found(tcp::socket &sock, launch_session_t &, msg_t &&req); void - respond(tcp::socket &sock, POPTION_ITEM options, int statuscode, const char *status_msg, int seqn, const std::string_view &payload); + respond(tcp::socket &sock, launch_session_t &session, POPTION_ITEM options, int statuscode, const char *status_msg, int seqn, const std::string_view &payload); class socket_t: public std::enable_shared_from_this { public: - socket_t(boost::asio::io_service &ios, std::function &&handle_data_fn): + socket_t(boost::asio::io_service &ios, std::function &&handle_data_fn): handle_data_fn { std::move(handle_data_fn) }, sock { ios } {} + /** + * @brief Queues an asynchronous read to begin the next message. + */ void read() { - if (begin == std::end(msg_buf)) { + if (begin == std::end(msg_buf) || (session->rtsp_cipher && begin + sizeof(encrypted_rtsp_header_t) >= std::end(msg_buf))) { BOOST_LOG(error) << "RTSP: read(): Exceeded maximum rtsp packet size: "sv << msg_buf.size(); - respond(sock, nullptr, 400, "BAD REQUEST", 0, {}); + respond(sock, *session, nullptr, 400, "BAD REQUEST", 0, {}); - sock.close(); + boost::system::error_code ec; + sock.close(ec); return; } - sock.async_read_some( - boost::asio::buffer(begin, (std::size_t)(std::end(msg_buf) - begin)), + if (session->rtsp_cipher) { + // For encrypted RTSP, we will read the the entire header first + boost::asio::async_read(sock, + boost::asio::buffer(begin, sizeof(encrypted_rtsp_header_t)), + boost::bind( + &socket_t::handle_read_encrypted_header, shared_from_this(), + boost::asio::placeholders::error, + boost::asio::placeholders::bytes_transferred)); + } + else { + sock.async_read_some( + boost::asio::buffer(begin, (std::size_t)(std::end(msg_buf) - begin)), + boost::bind( + &socket_t::handle_read_plaintext, shared_from_this(), + boost::asio::placeholders::error, + boost::asio::placeholders::bytes_transferred)); + } + } + + /** + * @brief Handles the initial read of the header of an encrypted message. + * @param socket The socket the message was received on. + * @param ec The error code of the read operation. + * @param bytes The number of bytes read. + */ + static void + handle_read_encrypted_header(std::shared_ptr &socket, const boost::system::error_code &ec, std::size_t bytes) { + BOOST_LOG(debug) << "handle_read_encrypted_header(): Handle read of size: "sv << bytes << " bytes"sv; + + auto sock_close = util::fail_guard([&socket]() { + boost::system::error_code ec; + socket->sock.close(ec); + + if (ec) { + BOOST_LOG(error) << "RTSP: handle_read_encrypted_header(): Couldn't close tcp socket: "sv << ec.message(); + } + }); + + if (ec || bytes < sizeof(encrypted_rtsp_header_t)) { + BOOST_LOG(error) << "RTSP: handle_read_encrypted_header(): Couldn't read from tcp socket: "sv << ec.message(); + + respond(socket->sock, *socket->session, nullptr, 400, "BAD REQUEST", 0, {}); + return; + } + + auto header = (encrypted_rtsp_header_t *) socket->begin; + if (!header->is_encrypted()) { + BOOST_LOG(error) << "RTSP: handle_read_encrypted_header(): Rejecting unencrypted RTSP message"sv; + + respond(socket->sock, *socket->session, nullptr, 400, "BAD REQUEST", 0, {}); + return; + } + + auto payload_length = header->payload_length(); + + // Check if we have enough space to read this message + if (socket->begin + sizeof(*header) + payload_length >= std::end(socket->msg_buf)) { + BOOST_LOG(error) << "RTSP: handle_read_encrypted_header(): Exceeded maximum rtsp packet size: "sv << socket->msg_buf.size(); + + respond(socket->sock, *socket->session, nullptr, 400, "BAD REQUEST", 0, {}); + return; + } + + sock_close.disable(); + + // Read the remainder of the header and full encrypted payload + boost::asio::async_read(socket->sock, + boost::asio::buffer(socket->begin + bytes, payload_length), boost::bind( - &socket_t::handle_read, shared_from_this(), + &socket_t::handle_read_encrypted_message, socket->shared_from_this(), boost::asio::placeholders::error, boost::asio::placeholders::bytes_transferred)); } + /** + * @brief Handles the final read of the content of an encrypted message. + * @param socket The socket the message was received on. + * @param ec The error code of the read operation. + * @param bytes The number of bytes read. + */ + static void + handle_read_encrypted_message(std::shared_ptr &socket, const boost::system::error_code &ec, std::size_t bytes) { + BOOST_LOG(debug) << "handle_read_encrypted(): Handle read of size: "sv << bytes << " bytes"sv; + + auto sock_close = util::fail_guard([&socket]() { + boost::system::error_code ec; + socket->sock.close(ec); + + if (ec) { + BOOST_LOG(error) << "RTSP: handle_read_encrypted_message(): Couldn't close tcp socket: "sv << ec.message(); + } + }); + + auto header = (encrypted_rtsp_header_t *) socket->begin; + auto payload_length = header->payload_length(); + auto seq = util::endian::big(header->sequenceNumber); + + if (ec || bytes < payload_length) { + BOOST_LOG(error) << "RTSP: handle_read_encrypted(): Couldn't read from tcp socket: "sv << ec.message(); + + respond(socket->sock, *socket->session, nullptr, 400, "BAD REQUEST", 0, {}); + return; + } + + // We use the deterministic IV construction algorithm specified in NIST SP 800-38D + // Section 8.2.1. The sequence number is our "invocation" field and the 'RC' in the + // high bytes is the "fixed" field. Because each client provides their own unique + // key, our values in the fixed field need only uniquely identify each independent + // use of the client's key with AES-GCM in our code. + // + // The sequence number is 32 bits long which allows for 2^32 RTSP messages to be + // received from each client before the IV repeats. + crypto::aes_t iv(12); + std::copy_n((uint8_t *) &seq, sizeof(seq), std::begin(iv)); + iv[10] = 'C'; // Client originated + iv[11] = 'R'; // RTSP + + std::vector plaintext; + if (socket->session->rtsp_cipher->decrypt(std::string_view { (const char *) header->tag, sizeof(header->tag) + bytes }, plaintext, &iv)) { + BOOST_LOG(error) << "Failed to verify RTSP message tag"sv; + + respond(socket->sock, *socket->session, nullptr, 400, "BAD REQUEST", 0, {}); + return; + } + + msg_t req { new msg_t::element_type {} }; + if (auto status = parseRtspMessage(req.get(), (char *) plaintext.data(), plaintext.size())) { + BOOST_LOG(error) << "Malformed RTSP message: ["sv << status << ']'; + + respond(socket->sock, *socket->session, nullptr, 400, "BAD REQUEST", 0, {}); + return; + } + + sock_close.disable(); + + print_msg(req.get()); + + socket->handle_data(std::move(req)); + } + + /** + * @brief Queues an asynchronous read of the payload portion of a plaintext message. + */ void - read_payload() { + read_plaintext_payload() { if (begin == std::end(msg_buf)) { - BOOST_LOG(error) << "RTSP: read_payload(): Exceeded maximum rtsp packet size: "sv << msg_buf.size(); + BOOST_LOG(error) << "RTSP: read_plaintext_payload(): Exceeded maximum rtsp packet size: "sv << msg_buf.size(); - respond(sock, nullptr, 400, "BAD REQUEST", 0, {}); + respond(sock, *session, nullptr, 400, "BAD REQUEST", 0, {}); - sock.close(); + boost::system::error_code ec; + sock.close(ec); return; } @@ -92,26 +269,32 @@ namespace rtsp_stream { sock.async_read_some( boost::asio::buffer(begin, (std::size_t)(std::end(msg_buf) - begin)), boost::bind( - &socket_t::handle_payload, shared_from_this(), + &socket_t::handle_plaintext_payload, shared_from_this(), boost::asio::placeholders::error, boost::asio::placeholders::bytes_transferred)); } + /** + * @brief Handles the read of the payload portion of a plaintext message. + * @param socket The socket the message was received on. + * @param ec The error code of the read operation. + * @param bytes The number of bytes read. + */ static void - handle_payload(std::shared_ptr &socket, const boost::system::error_code &ec, std::size_t bytes) { - BOOST_LOG(debug) << "handle_payload(): Handle read of size: "sv << bytes << " bytes"sv; + handle_plaintext_payload(std::shared_ptr &socket, const boost::system::error_code &ec, std::size_t bytes) { + BOOST_LOG(debug) << "handle_plaintext_payload(): Handle read of size: "sv << bytes << " bytes"sv; auto sock_close = util::fail_guard([&socket]() { boost::system::error_code ec; socket->sock.close(ec); if (ec) { - BOOST_LOG(error) << "RTSP: handle_payload(): Couldn't close tcp socket: "sv << ec.message(); + BOOST_LOG(error) << "RTSP: handle_plaintext_payload(): Couldn't close tcp socket: "sv << ec.message(); } }); if (ec) { - BOOST_LOG(error) << "RTSP: handle_payload(): Couldn't read from tcp socket: "sv << ec.message(); + BOOST_LOG(error) << "RTSP: handle_plaintext_payload(): Couldn't read from tcp socket: "sv << ec.message(); return; } @@ -121,14 +304,14 @@ namespace rtsp_stream { if (auto status = parseRtspMessage(req.get(), socket->msg_buf.data(), (std::size_t)(end - socket->msg_buf.data()))) { BOOST_LOG(error) << "Malformed RTSP message: ["sv << status << ']'; - respond(socket->sock, nullptr, 400, "BAD REQUEST", req->sequenceNumber, {}); + respond(socket->sock, *socket->session, nullptr, 400, "BAD REQUEST", 0, {}); return; } sock_close.disable(); auto fg = util::fail_guard([&socket]() { - socket->read_payload(); + socket->read_plaintext_payload(); }); auto content_length = 0; @@ -160,18 +343,24 @@ namespace rtsp_stream { socket->begin = end; } + /** + * @brief Handles the read of the header portion of a plaintext message. + * @param socket The socket the message was received on. + * @param ec The error code of the read operation. + * @param bytes The number of bytes read. + */ static void - handle_read(std::shared_ptr &socket, const boost::system::error_code &ec, std::size_t bytes) { - BOOST_LOG(debug) << "handle_read(): Handle read of size: "sv << bytes << " bytes"sv; + handle_read_plaintext(std::shared_ptr &socket, const boost::system::error_code &ec, std::size_t bytes) { + BOOST_LOG(debug) << "handle_read_plaintext(): Handle read of size: "sv << bytes << " bytes"sv; if (ec) { - BOOST_LOG(error) << "RTSP: handle_read(): Couldn't read from tcp socket: "sv << ec.message(); + BOOST_LOG(error) << "RTSP: handle_read_plaintext(): Couldn't read from tcp socket: "sv << ec.message(); boost::system::error_code ec; socket->sock.close(ec); if (ec) { - BOOST_LOG(error) << "RTSP: handle_read(): Couldn't close tcp socket: "sv << ec.message(); + BOOST_LOG(error) << "RTSP: handle_read_plaintext(): Couldn't close tcp socket: "sv << ec.message(); } return; @@ -200,15 +389,15 @@ namespace rtsp_stream { buf_size = end - socket->begin; fg.disable(); - handle_payload(socket, ec, buf_size); + handle_plaintext_payload(socket, ec, buf_size); } void handle_data(msg_t &&req) { - handle_data_fn(sock, std::move(req)); + handle_data_fn(sock, *session, std::move(req)); } - std::function handle_data_fn; + std::function handle_data_fn; tcp::socket sock; @@ -216,6 +405,8 @@ namespace rtsp_stream { char *crlf; char *begin = msg_buf.data(); + + std::shared_ptr session; }; class rtsp_server_t { @@ -225,7 +416,7 @@ namespace rtsp_stream { } int - bind(std::uint16_t port, boost::system::error_code &ec) { + bind(net::af_e af, std::uint16_t port, boost::system::error_code &ec) { { auto lg = _session_slots.lock(); @@ -233,14 +424,14 @@ namespace rtsp_stream { _slot_count = config::stream.channels; } - acceptor.open(tcp::v4(), ec); + acceptor.open(af == net::IPV4 ? tcp::v4() : tcp::v6(), ec); if (ec) { return -1; } acceptor.set_option(boost::asio::socket_base::reuse_address { true }); - acceptor.bind(tcp::endpoint(tcp::v4(), port), ec); + acceptor.bind(tcp::endpoint(af == net::IPV4 ? tcp::v4() : tcp::v6(), port), ec); if (ec) { return -1; } @@ -250,8 +441,8 @@ namespace rtsp_stream { return -1; } - next_socket = std::make_shared(ios, [this](tcp::socket &sock, msg_t &&msg) { - handle_msg(sock, std::move(msg)); + next_socket = std::make_shared(ios, [this](tcp::socket &sock, launch_session_t &session, msg_t &&msg) { + handle_msg(sock, session, std::move(msg)); }); acceptor.async_accept(next_socket->sock, [this](const auto &ec) { @@ -268,16 +459,17 @@ namespace rtsp_stream { } void - handle_msg(tcp::socket &sock, msg_t &&req) { + handle_msg(tcp::socket &sock, launch_session_t &session, msg_t &&req) { auto func = _map_cmd_cb.find(req->message.request.command); if (func != std::end(_map_cmd_cb)) { - func->second(this, sock, std::move(req)); + func->second(this, sock, session, std::move(req)); } else { - cmd_not_found(sock, std::move(req)); + cmd_not_found(sock, session, std::move(req)); } - sock.shutdown(boost::asio::socket_base::shutdown_type::shutdown_both); + boost::system::error_code ec; + sock.shutdown(boost::asio::socket_base::shutdown_type::shutdown_both, ec); } void @@ -291,12 +483,26 @@ namespace rtsp_stream { } auto socket = std::move(next_socket); - socket->read(); - next_socket = std::make_shared(ios, [this](tcp::socket &sock, msg_t &&msg) { - handle_msg(sock, std::move(msg)); - }); + auto launch_session { launch_event.view(0s) }; + if (launch_session) { + // Associate the current RTSP session with this socket and start reading + socket->session = launch_session; + socket->read(); + } + else { + // This can happen due to normal things like port scanning, so let's not make these visible by default + BOOST_LOG(debug) << "No pending session for incoming RTSP connection"sv; + // If there is no session pending, close the connection immediately + boost::system::error_code ec; + socket->sock.close(ec); + } + + // Queue another asynchronous accept for the next incoming connection + next_socket = std::make_shared(ios, [this](tcp::socket &sock, launch_session_t &session, msg_t &&msg) { + handle_msg(sock, session, std::move(msg)); + }); acceptor.async_accept(next_socket->sock, [this](const auto &ec) { handle_accept(ec); }); @@ -307,18 +513,43 @@ namespace rtsp_stream { _map_cmd_cb.emplace(type, std::move(cb)); } + /** + * @brief Launch a new streaming session. + * @note If the client does not begin streaming within the ping_timeout, + * the session will be discarded. + * @param launch_session Streaming session information. + */ void - session_raise(rtsp_stream::launch_session_t launch_session) { + session_raise(std::shared_ptr launch_session) { auto now = std::chrono::steady_clock::now(); // If a launch event is still pending, don't overwrite it. if (raised_timeout > now && launch_event.peek()) { return; } - raised_timeout = now + 10s; + raised_timeout = now + config::stream.ping_timeout; --_slot_count; - launch_event.raise(launch_session); + launch_event.raise(std::move(launch_session)); + } + + /** + * @brief Clear state for the oldest launch session. + * @param launch_session_id The ID of the session to clear. + */ + void + session_clear(uint32_t launch_session_id) { + // We currently only support a single pending RTSP session, + // so the ID should always match the one for that session. + auto launch_session = launch_event.view(0s); + if (launch_session) { + if (launch_session->id != launch_session_id) { + BOOST_LOG(error) << "Attempted to clear unexpected session: "sv << launch_session_id << " vs "sv << launch_session->id; + } + else { + launch_event.pop(); + } + } } int @@ -326,14 +557,24 @@ namespace rtsp_stream { return config::stream.channels - _slot_count; } - safe::event_t launch_event; - + safe::event_t> launch_event; + + /** + * @brief Clear launch sessions. + * @param all If true, clear all sessions. Otherwise, only clear timed out and stopped sessions. + * + * EXAMPLES: + * ```cpp + * clear(false); + * ``` + */ void clear(bool all = true) { // if a launch event timed out --> Remove it. if (raised_timeout < std::chrono::steady_clock::now()) { auto discarded = launch_event.pop(0s); if (discarded) { + BOOST_LOG(debug) << "Event timeout: "sv << discarded->unique_id; ++_slot_count; } } @@ -396,8 +637,17 @@ namespace rtsp_stream { rtsp_server_t server {}; void - launch_session_raise(rtsp_stream::launch_session_t launch_session) { - server.session_raise(launch_session); + launch_session_raise(std::shared_ptr launch_session) { + server.session_raise(std::move(launch_session)); + } + + /** + * @brief Clear state for the specified launch session. + * @param launch_session_id The ID of the session to clear. + */ + void + launch_session_clear(uint32_t launch_session_id) { + server.session_clear(launch_session_id); } int @@ -426,7 +676,7 @@ namespace rtsp_stream { } void - respond(tcp::socket &sock, msg_t &resp) { + respond(tcp::socket &sock, launch_session_t &session, msg_t &resp) { auto payload = std::make_pair(resp->payload, resp->payloadLength); // Restore response message for proper destruction @@ -446,30 +696,70 @@ namespace rtsp_stream { << std::string_view { payload.first, (std::size_t) payload.second } << std::endl << "---End Response---"sv << std::endl; - std::string_view tmp_resp { raw_resp.get(), (size_t) serialized_len }; - - if (send(sock, tmp_resp)) { - return; + // Encrypt the RTSP message if encryption is enabled + if (session.rtsp_cipher) { + // We use the deterministic IV construction algorithm specified in NIST SP 800-38D + // Section 8.2.1. The sequence number is our "invocation" field and the 'RH' in the + // high bytes is the "fixed" field. Because each client provides their own unique + // key, our values in the fixed field need only uniquely identify each independent + // use of the client's key with AES-GCM in our code. + // + // The sequence number is 32 bits long which allows for 2^32 RTSP messages to be + // sent to each client before the IV repeats. + crypto::aes_t iv(12); + session.rtsp_iv_counter++; + std::copy_n((uint8_t *) &session.rtsp_iv_counter, sizeof(session.rtsp_iv_counter), std::begin(iv)); + iv[10] = 'H'; // Host originated + iv[11] = 'R'; // RTSP + + // Allocate the message with an empty header and reserved space for the payload + auto payload_length = serialized_len + payload.second; + std::vector message(sizeof(encrypted_rtsp_header_t)); + message.reserve(message.size() + payload_length); + + // Copy the complete plaintext into the message + std::copy_n(raw_resp.get(), serialized_len, std::back_inserter(message)); + std::copy_n(payload.first, payload.second, std::back_inserter(message)); + + // Initialize the message header + auto header = (encrypted_rtsp_header_t *) message.data(); + header->typeAndLength = util::endian::big(encrypted_rtsp_header_t::ENCRYPTED_MESSAGE_TYPE_BIT + payload_length); + header->sequenceNumber = util::endian::big(session.rtsp_iv_counter); + + // Encrypt the RTSP message in place + session.rtsp_cipher->encrypt(std::string_view { (const char *) header->payload(), (std::size_t) payload_length }, header->tag, &iv); + + // Send the full encrypted message + send(sock, std::string_view { (char *) message.data(), message.size() }); } + else { + std::string_view tmp_resp { raw_resp.get(), (size_t) serialized_len }; + + // Send the plaintext RTSP message header + if (send(sock, tmp_resp)) { + return; + } - send(sock, std::string_view { payload.first, (std::size_t) payload.second }); + // Send the plaintext RTSP message payload (if present) + send(sock, std::string_view { payload.first, (std::size_t) payload.second }); + } } void - respond(tcp::socket &sock, POPTION_ITEM options, int statuscode, const char *status_msg, int seqn, const std::string_view &payload) { + respond(tcp::socket &sock, launch_session_t &session, POPTION_ITEM options, int statuscode, const char *status_msg, int seqn, const std::string_view &payload) { msg_t resp { new msg_t::element_type }; createRtspResponse(resp.get(), nullptr, 0, const_cast("RTSP/1.0"), statuscode, const_cast(status_msg), seqn, options, const_cast(payload.data()), (int) payload.size()); - respond(sock, resp); + respond(sock, session, resp); } void - cmd_not_found(tcp::socket &sock, msg_t &&req) { - respond(sock, nullptr, 404, "NOT FOUND", req->sequenceNumber, {}); + cmd_not_found(tcp::socket &sock, launch_session_t &session, msg_t &&req) { + respond(sock, session, nullptr, 404, "NOT FOUND", req->sequenceNumber, {}); } void - cmd_option(rtsp_server_t *server, tcp::socket &sock, msg_t &&req) { + cmd_option(rtsp_server_t *server, tcp::socket &sock, launch_session_t &session, msg_t &&req) { OPTION_ITEM option {}; // I know these string literals will not be modified @@ -478,11 +768,11 @@ namespace rtsp_stream { auto seqn_str = std::to_string(req->sequenceNumber); option.content = const_cast(seqn_str.c_str()); - respond(sock, &option, 200, "OK", req->sequenceNumber, {}); + respond(sock, session, &option, 200, "OK", req->sequenceNumber, {}); } void - cmd_describe(rtsp_server_t *server, tcp::socket &sock, msg_t &&req) { + cmd_describe(rtsp_server_t *server, tcp::socket &sock, launch_session_t &session, msg_t &&req) { OPTION_ITEM option {}; // I know these string literals will not be modified @@ -492,10 +782,49 @@ namespace rtsp_stream { option.content = const_cast(seqn_str.c_str()); std::stringstream ss; + + // Tell the client about our supported features + ss << "a=x-ss-general.featureFlags:" << (uint32_t) platf::get_capabilities() << std::endl; + + // Always request new control stream encryption if the client supports it + uint32_t encryption_flags_supported = SS_ENC_CONTROL_V2 | SS_ENC_AUDIO; + uint32_t encryption_flags_requested = SS_ENC_CONTROL_V2; + + // Determine the encryption desired for this remote endpoint + auto encryption_mode = net::encryption_mode_for_address(sock.remote_endpoint().address()); + if (encryption_mode != config::ENCRYPTION_MODE_NEVER) { + // Advertise support for video encryption if it's not disabled + encryption_flags_supported |= SS_ENC_VIDEO; + + // If it's mandatory, also request it to enable use if the client + // didn't explicitly opt in, but it otherwise has support. + if (encryption_mode == config::ENCRYPTION_MODE_MANDATORY) { + encryption_flags_requested |= SS_ENC_VIDEO | SS_ENC_AUDIO; + } + } + + // Report supported and required encryption flags + ss << "a=x-ss-general.encryptionSupported:" << encryption_flags_supported << std::endl; + ss << "a=x-ss-general.encryptionRequested:" << encryption_flags_requested << std::endl; + + if (video::last_encoder_probe_supported_ref_frames_invalidation) { + ss << "a=x-nv-video[0].refPicInvalidation:1"sv << std::endl; + } + if (video::active_hevc_mode != 1) { ss << "sprop-parameter-sets=AAAAAU"sv << std::endl; } + if (video::active_av1_mode != 1) { + ss << "a=rtpmap:98 AV1/90000"sv << std::endl; + } + + if (!session.surround_params.empty()) { + // If we have our own surround parameters, advertise them twice first + ss << "a=fmtp:97 surround-params="sv << session.surround_params << std::endl; + ss << "a=fmtp:97 surround-params="sv << session.surround_params << std::endl; + } + for (int x = 0; x < audio::MAX_STREAM_CONFIG; ++x) { auto &stream_config = audio::stream_configs[x]; std::uint8_t mapping[platf::speaker::MAX_SPEAKERS]; @@ -523,16 +852,17 @@ namespace rtsp_stream { ss << std::endl; } - respond(sock, &option, 200, "OK", req->sequenceNumber, ss.str()); + respond(sock, session, &option, 200, "OK", req->sequenceNumber, ss.str()); } void - cmd_setup(rtsp_server_t *server, tcp::socket &sock, msg_t &&req) { - OPTION_ITEM options[3] {}; + cmd_setup(rtsp_server_t *server, tcp::socket &sock, launch_session_t &session, msg_t &&req) { + OPTION_ITEM options[4] {}; auto &seqn = options[0]; auto &session_option = options[1]; auto &port_option = options[2]; + auto &payload_option = options[3]; seqn.option = const_cast("CSeq"); @@ -546,16 +876,16 @@ namespace rtsp_stream { std::uint16_t port; if (type == "audio"sv) { - port = map_port(stream::AUDIO_STREAM_PORT); + port = net::map_port(stream::AUDIO_STREAM_PORT); } else if (type == "video"sv) { - port = map_port(stream::VIDEO_STREAM_PORT); + port = net::map_port(stream::VIDEO_STREAM_PORT); } else if (type == "control"sv) { - port = map_port(stream::CONTROL_PORT); + port = net::map_port(stream::CONTROL_PORT); } else { - cmd_not_found(sock, std::move(req)); + cmd_not_found(sock, session, std::move(req)); return; } @@ -573,11 +903,24 @@ namespace rtsp_stream { port_option.option = const_cast("Transport"); port_option.content = port_value.data(); - respond(sock, &seqn, 200, "OK", req->sequenceNumber, {}); + // Send identifiers that will be echoed in the other connections + auto connect_data = std::to_string(session.control_connect_data); + if (type == "control"sv) { + payload_option.option = const_cast("X-SS-Connect-Data"); + payload_option.content = connect_data.data(); + } + else { + payload_option.option = const_cast("X-SS-Ping-Payload"); + payload_option.content = session.av_ping_payload.data(); + } + + port_option.next = &payload_option; + + respond(sock, session, &seqn, 200, "OK", req->sequenceNumber, {}); } void - cmd_announce(rtsp_server_t *server, tcp::socket &sock, msg_t &&req) { + cmd_announce(rtsp_server_t *server, tcp::socket &sock, launch_session_t &session, msg_t &&req) { OPTION_ITEM option {}; // I know these string literals will not be modified @@ -586,14 +929,6 @@ namespace rtsp_stream { auto seqn_str = std::to_string(req->sequenceNumber); option.content = const_cast(seqn_str.c_str()); - if (!server->launch_event.peek()) { - // /launch has not been used - - respond(sock, &option, 503, "Service Unavailable", req->sequenceNumber, {}); - return; - } - auto launch_session { server->launch_event.pop() }; - std::string_view payload { req->payload, (size_t) req->payloadLength }; std::vector lines; @@ -644,12 +979,16 @@ namespace rtsp_stream { args.try_emplace("x-nv-general.useReliableUdp"sv, "1"sv); args.try_emplace("x-nv-vqos[0].fec.minRequiredFecPackets"sv, "0"sv); args.try_emplace("x-nv-general.featureFlags"sv, "135"sv); + args.try_emplace("x-ml-general.featureFlags"sv, "0"sv); args.try_emplace("x-nv-vqos[0].qosTrafficType"sv, "5"sv); args.try_emplace("x-nv-aqos.qosTrafficType"sv, "4"sv); + args.try_emplace("x-ml-video.configuredBitrateKbps"sv, "0"sv); + args.try_emplace("x-ss-general.encryptionEnabled"sv, "0"sv); stream::config_t config; - config.audio.flags[audio::config_t::HOST_AUDIO] = launch_session->host_audio; + std::int64_t configuredBitrateKbps; + config.audio.flags[audio::config_t::HOST_AUDIO] = session.host_audio; try { config.audio.channels = util::from_view(args.at("x-nv-audio.surround.numChannels"sv)); config.audio.mask = util::from_view(args.at("x-nv-audio.surround.channelMask"sv)); @@ -661,9 +1000,15 @@ namespace rtsp_stream { config.controlProtocolType = util::from_view(args.at("x-nv-general.useReliableUdp"sv)); config.packetsize = util::from_view(args.at("x-nv-video[0].packetSize"sv)); config.minRequiredFecPackets = util::from_view(args.at("x-nv-vqos[0].fec.minRequiredFecPackets"sv)); - config.featureFlags = util::from_view(args.at("x-nv-general.featureFlags"sv)); + config.mlFeatureFlags = util::from_view(args.at("x-ml-general.featureFlags"sv)); config.audioQosType = util::from_view(args.at("x-nv-aqos.qosTrafficType"sv)); config.videoQosType = util::from_view(args.at("x-nv-vqos[0].qosTrafficType"sv)); + config.encryptionFlagsEnabled = util::from_view(args.at("x-ss-general.encryptionEnabled"sv)); + + // Legacy clients use nvFeatureFlags to indicate support for audio encryption + if (util::from_view(args.at("x-nv-general.featureFlags"sv)) & 0x20) { + config.encryptionFlagsEnabled |= SS_ENC_AUDIO; + } config.monitor.height = util::from_view(args.at("x-nv-video[0].clientViewportHt"sv)); config.monitor.width = util::from_view(args.at("x-nv-video[0].clientViewportWd"sv)); @@ -674,9 +1019,11 @@ namespace rtsp_stream { config.monitor.encoderCscMode = util::from_view(args.at("x-nv-video[0].encoderCscMode"sv)); config.monitor.videoFormat = util::from_view(args.at("x-nv-vqos[0].bitStreamFormat"sv)); config.monitor.dynamicRange = util::from_view(args.at("x-nv-video[0].dynamicRangeMode"sv)); + + configuredBitrateKbps = util::from_view(args.at("x-ml-video.configuredBitrateKbps"sv)); } catch (std::out_of_range &) { - respond(sock, &option, 400, "BAD REQUEST", req->sequenceNumber, {}); + respond(sock, session, &option, 400, "BAD REQUEST", req->sequenceNumber, {}); return; } @@ -692,37 +1039,103 @@ namespace rtsp_stream { } } } + else if (session.surround_params.length() > 3) { + // Channels + std::uint8_t c = session.surround_params[0] - '0'; + // Streams + std::uint8_t n = session.surround_params[1] - '0'; + // Coupled streams + std::uint8_t m = session.surround_params[2] - '0'; + auto valid = false; + if ((c == 6 || c == 8) && c == config.audio.channels && n + m == c && session.surround_params.length() == c + 3) { + config.audio.customStreamParams.channelCount = c; + config.audio.customStreamParams.streams = n; + config.audio.customStreamParams.coupledStreams = m; + valid = true; + for (std::uint8_t i = 0; i < c; i++) { + config.audio.customStreamParams.mapping[i] = session.surround_params[i + 3] - '0'; + if (config.audio.customStreamParams.mapping[i] >= c) { + valid = false; + break; + } + } + } + config.audio.flags[audio::config_t::CUSTOM_SURROUND_PARAMS] = valid; + } + + // If the client sent a configured bitrate, we will choose the actual bitrate ourselves + // by using FEC percentage and audio quality settings. If the calculated bitrate ends up + // too low, we'll allow it to exceed the limits rather than reducing the encoding bitrate + // down to nearly nothing. + if (configuredBitrateKbps) { + BOOST_LOG(debug) << "Client configured bitrate is "sv << configuredBitrateKbps << " Kbps"sv; + + // If the FEC percentage isn't too high, adjust the configured bitrate to ensure video + // traffic doesn't exceed the user's selected bitrate when the FEC shards are included. + if (config::stream.fec_percentage <= 80) { + configuredBitrateKbps /= 100.f / (100 - config::stream.fec_percentage); + } + + // Adjust the bitrate to account for audio traffic bandwidth usage (capped at 20% reduction). + // The bitrate per channel is 256 Kbps for high quality mode and 96 Kbps for normal quality. + auto audioBitrateAdjustment = (config.audio.flags[audio::config_t::HIGH_QUALITY] ? 256 : 96) * config.audio.channels; + configuredBitrateKbps -= std::min((std::int64_t) audioBitrateAdjustment, configuredBitrateKbps / 5); + + // Reduce it by another 500Kbps to account for A/V packet overhead and control data + // traffic (capped at 10% reduction). + configuredBitrateKbps -= std::min((std::int64_t) 500, configuredBitrateKbps / 10); + + BOOST_LOG(debug) << "Final adjusted video encoding bitrate is "sv << configuredBitrateKbps << " Kbps"sv; + config.monitor.bitrate = configuredBitrateKbps; + } - if (config.monitor.videoFormat != 0 && video::active_hevc_mode == 1) { + if (config.monitor.videoFormat == 1 && video::active_hevc_mode == 1) { BOOST_LOG(warning) << "HEVC is disabled, yet the client requested HEVC"sv; - respond(sock, &option, 400, "BAD REQUEST", req->sequenceNumber, {}); + respond(sock, session, &option, 400, "BAD REQUEST", req->sequenceNumber, {}); return; } - auto session = stream::session::alloc(config, launch_session->gcm_key, launch_session->iv); + if (config.monitor.videoFormat == 2 && video::active_av1_mode == 1) { + BOOST_LOG(warning) << "AV1 is disabled, yet the client requested AV1"sv; - auto slot = server->accept(session); + respond(sock, session, &option, 400, "BAD REQUEST", req->sequenceNumber, {}); + return; + } + + // Check that any required encryption is enabled + auto encryption_mode = net::encryption_mode_for_address(sock.remote_endpoint().address()); + if (encryption_mode == config::ENCRYPTION_MODE_MANDATORY && + (config.encryptionFlagsEnabled & (SS_ENC_VIDEO | SS_ENC_AUDIO)) != (SS_ENC_VIDEO | SS_ENC_AUDIO)) { + BOOST_LOG(error) << "Rejecting client that cannot comply with mandatory encryption requirement"sv; + + respond(sock, session, &option, 403, "Forbidden", req->sequenceNumber, {}); + return; + } + + auto stream_session = stream::session::alloc(config, session); + + auto slot = server->accept(stream_session); if (!slot) { BOOST_LOG(info) << "Ran out of slots for client from ["sv << ']'; - respond(sock, &option, 503, "Service Unavailable", req->sequenceNumber, {}); + respond(sock, session, &option, 503, "Service Unavailable", req->sequenceNumber, {}); return; } - if (stream::session::start(*session, sock.remote_endpoint().address().to_string())) { + if (stream::session::start(*stream_session, sock.remote_endpoint().address().to_string())) { BOOST_LOG(error) << "Failed to start a streaming session"sv; server->clear(slot); - respond(sock, &option, 500, "Internal Server Error", req->sequenceNumber, {}); + respond(sock, session, &option, 500, "Internal Server Error", req->sequenceNumber, {}); return; } - respond(sock, &option, 200, "OK", req->sequenceNumber, {}); + respond(sock, session, &option, 200, "OK", req->sequenceNumber, {}); } void - cmd_play(rtsp_server_t *server, tcp::socket &sock, msg_t &&req) { + cmd_play(rtsp_server_t *server, tcp::socket &sock, launch_session_t &session, msg_t &&req) { OPTION_ITEM option {}; // I know these string literals will not be modified @@ -731,7 +1144,7 @@ namespace rtsp_stream { auto seqn_str = std::to_string(req->sequenceNumber); option.content = const_cast(seqn_str.c_str()); - respond(sock, &option, 200, "OK", req->sequenceNumber, {}); + respond(sock, session, &option, 200, "OK", req->sequenceNumber, {}); } void @@ -743,12 +1156,11 @@ namespace rtsp_stream { server.map("DESCRIBE"sv, &cmd_describe); server.map("SETUP"sv, &cmd_setup); server.map("ANNOUNCE"sv, &cmd_announce); - server.map("PLAY"sv, &cmd_play); boost::system::error_code ec; - if (server.bind(map_port(rtsp_stream::RTSP_SETUP_PORT), ec)) { - BOOST_LOG(fatal) << "Couldn't bind RTSP server to port ["sv << map_port(rtsp_stream::RTSP_SETUP_PORT) << "], " << ec.message(); + if (server.bind(net::af_from_enum_string(config::sunshine.address_family), net::map_port(rtsp_stream::RTSP_SETUP_PORT), ec)) { + BOOST_LOG(fatal) << "Couldn't bind RTSP server to port ["sv << net::map_port(rtsp_stream::RTSP_SETUP_PORT) << "], " << ec.message(); shutdown_event->raise(true); return; diff --git a/src/rtsp.h b/src/rtsp.h index 2b0355fd947..16dba1e0592 100644 --- a/src/rtsp.h +++ b/src/rtsp.h @@ -13,14 +13,37 @@ namespace rtsp_stream { constexpr auto RTSP_SETUP_PORT = 21; struct launch_session_t { + uint32_t id; + crypto::aes_t gcm_key; crypto::aes_t iv; + std::string av_ping_payload; + uint32_t control_connect_data; + bool host_audio; + std::string unique_id; + int width; + int height; + int fps; + int gcmap; + int appid; + int surround_info; + std::string surround_params; + bool enable_hdr; + bool enable_sops; + + std::optional rtsp_cipher; + std::string rtsp_url_scheme; + uint32_t rtsp_iv_counter; }; void - launch_session_raise(launch_session_t launch_session); + launch_session_raise(std::shared_ptr launch_session); + + void + launch_session_clear(uint32_t launch_session_id); + int session_count(); diff --git a/src/stat_trackers.h b/src/stat_trackers.h index c26c8f455f6..124b906472a 100644 --- a/src/stat_trackers.h +++ b/src/stat_trackers.h @@ -32,11 +32,16 @@ namespace stat_trackers { data.calls += 1; } + void + reset() { + data = {}; + } + private: struct { std::chrono::steady_clock::steady_clock::time_point last_callback_time = std::chrono::steady_clock::now(); T stat_min = std::numeric_limits::max(); - T stat_max = 0; + T stat_max = std::numeric_limits::min(); double stat_total = 0; uint32_t calls = 0; } data; diff --git a/src/stream.cpp b/src/stream.cpp index 18c1473cdad..fe62f03e6ed 100644 --- a/src/stream.cpp +++ b/src/stream.cpp @@ -13,18 +13,19 @@ #include extern "C" { -#include -#include +#include #include } #include "config.h" +#include "globals.h" #include "input.h" -#include "main.h" +#include "logging.h" #include "network.h" #include "stat_trackers.h" #include "stream.h" #include "sync.h" +#include "system_tray.h" #include "thread_safe.h" #include "utility.h" @@ -39,6 +40,9 @@ extern "C" { #define IDX_REQUEST_IDR_FRAME 9 #define IDX_ENCRYPTED 10 #define IDX_HDR_MODE 11 +#define IDX_RUMBLE_TRIGGER_DATA 12 +#define IDX_SET_MOTION_EVENT 13 +#define IDX_SET_RGB_LED 14 static const short packetTypes[] = { 0x0305, // Start A @@ -53,6 +57,9 @@ static const short packetTypes[] = { 0x0302, // IDR frame 0x0001, // fully encrypted 0x010e, // HDR mode + 0x5500, // Rumble triggers (Sunshine protocol extension) + 0x5501, // Set motion event (Sunshine protocol extension) + 0x5502, // Set RGB LED (Sunshine protocol extension) }; namespace asio = boost::asio; @@ -92,7 +99,11 @@ namespace stream { // 5 = P-frame after reference frame invalidation std::uint8_t frameType; - std::uint8_t unknown2[4]; + // Length of the final packet payload for codecs that cannot handle + // zero padding, such as AV1 (Sunshine extension). + boost::endian::little_uint16_at lastPayloadLen; + + std::uint8_t unknown[2]; }; static_assert( @@ -111,6 +122,17 @@ namespace stream { NV_VIDEO_PACKET packet; }; + struct video_packet_enc_prefix_t { + video_packet_raw_t * + payload() { + return (video_packet_raw_t *) (this + 1); + } + + std::uint8_t iv[12]; // 12-byte IV is ideal for AES-GCM + std::uint32_t frameNumber; + std::uint8_t tag[16]; + }; + struct audio_packet_raw_t { uint8_t * payload() { @@ -146,6 +168,31 @@ namespace stream { std::uint16_t highfreq; }; + struct control_rumble_triggers_t { + control_header_v2 header; + + std::uint16_t id; + std::uint16_t left; + std::uint16_t right; + }; + + struct control_set_motion_event_t { + control_header_v2 header; + + std::uint16_t id; + std::uint16_t reportrate; + std::uint8_t type; + }; + + struct control_set_rgb_led_t { + control_header_v2 header; + + std::uint16_t id; + std::uint8_t r; + std::uint8_t g; + std::uint8_t b; + }; + struct control_hdr_mode_t { control_header_v2 header; @@ -193,22 +240,20 @@ namespace stream { using audio_fec_packet_t = util::c_ptr; using audio_aes_t = std::array; - using message_queue_t = std::shared_ptr>>; - using message_queue_queue_t = std::shared_ptr>>; + using av_session_id_t = std::variant; // IP address or SS-Ping-Payload from RTSP handshake + using message_queue_t = std::shared_ptr>>; + using message_queue_queue_t = std::shared_ptr>>; // return bytes written on success // return -1 on error static inline int - encode_audio(int featureSet, const audio::buffer_t &plaintext, audio_packet_t &destination, std::uint32_t avRiKeyIv, crypto::cipher::cbc_t &cbc) { + encode_audio(bool encrypted, const audio::buffer_t &plaintext, audio_packet_t &destination, crypto::aes_t &iv, crypto::cipher::cbc_t &cbc) { // If encryption isn't enabled - if (!(featureSet & 0x20)) { + if (!encrypted) { std::copy(std::begin(plaintext), std::end(plaintext), destination->payload()); return plaintext.size(); } - crypto::aes_t iv {}; - *(std::uint32_t *) iv.data() = util::endian::big(avRiKeyIv + destination->rtp.sequenceNumber); - return cbc.encrypt(std::string_view { (char *) std::begin(plaintext), plaintext.size() }, destination->payload(), &iv); } @@ -222,24 +267,17 @@ namespace stream { class control_server_t { public: int - bind(std::uint16_t port) { - _host = net::host_create(_addr, config::stream.channels, port); + bind(net::af_e address_family, std::uint16_t port) { + _host = net::host_create(address_family, _addr, config::stream.channels, port); return !(bool) _host; } - void - emplace_addr_to_session(const std::string &addr, session_t &session) { - auto lg = _map_addr_session.lock(); - - _map_addr_session->emplace(addr, std::make_pair(0u, &session)); - } - // Get session associated with address. // If none are found, try to find a session not yet claimed. (It will be marked by a port of value 0 // If none of those are found, return nullptr session_t * - get_session(const net::peer_t peer); + get_session(const net::peer_t peer, uint32_t connect_data); // Circular dependency: // iterate refers to session @@ -249,8 +287,15 @@ namespace stream { void iterate(std::chrono::milliseconds timeout); + /** + * @brief Calls the handler for a given control stream message. + * @param type The message type. + * @param session The session the message was received on. + * @param payload The payload of the message. + * @param reinjected `true` if this message is being reprocessed after decryption. + */ void - call(std::uint16_t type, session_t *session, const std::string_view &payload); + call(std::uint16_t type, session_t *session, const std::string_view &payload, bool reinjected); void map(uint16_t type, std::function cb) { @@ -277,8 +322,11 @@ namespace stream { // Callbacks std::unordered_map> _map_type_cb; - // Mapping ip:port to session - sync_util::sync_t>> _map_addr_session; + // All active sessions (including those still waiting for a peer to connect) + sync_util::sync_t> _sessions; + + // ENet peer to session mapping for sessions with a peer connected + sync_util::sync_t> _peer_to_session; ENetAddress _addr; net::host_t _host; @@ -297,12 +345,6 @@ namespace stream { udp::socket video_sock { io }; udp::socket audio_sock { io }; - // This is purely for administrative purposes. - // It's possible two instances of Moonlight are behind a NAT. - // From Sunshine's point of view, the ip addresses are identical - // We need some way to know what ports are already used for different streams - sync_util::sync_t>> audio_video_connections; - control_server_t control_server; }; @@ -320,15 +362,26 @@ namespace stream { safe::shared_t::ptr_t broadcast_ref; + boost::asio::ip::address localAddress; + struct { + std::string ping_payload; + int lowseq; udp::endpoint peer; + + std::optional cipher; + std::uint64_t gcm_iv_counter; + safe::mail_raw_t::event_t idr_events; + safe::mail_raw_t::event_t> invalidate_ref_frames_events; + std::unique_ptr qos; } video; struct { crypto::cipher::cbc_t cipher; + std::string ping_payload; std::uint16_t sequenceNumber; // avRiKeyId == util::endian::big(First (sizeof(avRiKeyId)) bytes of launch_session->iv) @@ -345,15 +398,22 @@ namespace stream { struct { crypto::cipher::gcm_t cipher; - crypto::aes_t iv; + crypto::aes_t legacy_input_enc_iv; // Only used when the client doesn't support full control stream encryption + crypto::aes_t incoming_iv; + crypto::aes_t outgoing_iv; + + std::uint32_t connect_data; // Used for new clients with ML_FF_SESSION_ID_V1 + std::string expected_peer_address; // Only used for legacy clients without ML_FF_SESSION_ID_V1 net::peer_t peer; - std::uint8_t seq; + std::uint32_t seq; - platf::rumble_queue_t rumble_queue; + platf::feedback_queue_t feedback_queue; safe::mail_raw_t::event_t hdr_queue; } control; + std::uint32_t launch_session_id; + safe::mail_raw_t::event_t shutdown_event; safe::signal_t controlEnd; @@ -377,9 +437,29 @@ namespace stream { return plaintext; } - crypto::aes_t iv {}; auto seq = session->control.seq++; - iv[0] = seq; + + auto &iv = session->control.outgoing_iv; + if (session->config.encryptionFlagsEnabled & SS_ENC_CONTROL_V2) { + // We use the deterministic IV construction algorithm specified in NIST SP 800-38D + // Section 8.2.1. The sequence number is our "invocation" field and the 'CH' in the + // high bytes is the "fixed" field. Because each client provides their own unique + // key, our values in the fixed field need only uniquely identify each independent + // use of the client's key with AES-GCM in our code. + // + // The sequence number is 32 bits long which allows for 2^32 control stream messages + // to be sent to each client before the IV repeats. + iv.resize(12); + std::copy_n((uint8_t *) &seq, sizeof(seq), std::begin(iv)); + iv[10] = 'H'; // Host originated + iv[11] = 'C'; // Control stream + } + else { + // Nvidia's old style encryption uses a 16-byte IV + iv.resize(16); + + iv[0] = (std::uint8_t) seq; + } auto packet = (control_encrypted_p) tagged_cipher.data(); @@ -406,38 +486,84 @@ namespace stream { static auto broadcast = safe::make_shared(start_broadcast, end_broadcast); session_t * - control_server_t::get_session(const net::peer_t peer) { - TUPLE_2D(port, addr_string, platf::from_sockaddr_ex((sockaddr *) &peer->address.address)); + control_server_t::get_session(const net::peer_t peer, uint32_t connect_data) { + { + // Fast path - look up existing session by peer + auto lg = _peer_to_session.lock(); + auto it = _peer_to_session->find(peer); + if (it != _peer_to_session->end()) { + return it->second; + } + } - auto lg = _map_addr_session.lock(); - TUPLE_2D(begin, end, _map_addr_session->equal_range(addr_string)); + // Slow path - process new session + TUPLE_2D(peer_port, peer_addr, platf::from_sockaddr_ex((sockaddr *) &peer->address.address)); + auto lg = _sessions.lock(); + for (auto pos = std::begin(*_sessions); pos != std::end(*_sessions); ++pos) { + auto session_p = *pos; - auto it = std::end(_map_addr_session.raw); - for (auto pos = begin; pos != end; ++pos) { - TUPLE_2D_REF(session_port, session_p, pos->second); + // Skip sessions that are already established + if (session_p->control.peer) { + continue; + } - if (port == session_port) { - return session_p; + // Identify the connection by the unique connect data if the client supports it. + // Only fall back to IP address matching for clients without session ID support. + if (session_p->config.mlFeatureFlags & ML_FF_SESSION_ID_V1) { + if (session_p->control.connect_data != connect_data) { + continue; + } + else { + BOOST_LOG(debug) << "Initialized new control stream session by connect data match [v2]"sv; + } } - else if (session_port == 0) { - it = pos; + else { + if (session_p->control.expected_peer_address != peer_addr) { + continue; + } + else { + BOOST_LOG(debug) << "Initialized new control stream session by IP address match [v1]"sv; + } } - } - if (it != std::end(_map_addr_session.raw)) { - TUPLE_2D_REF(session_port, session_p, it->second); + // Once the control stream connection is established, RTSP session state can be torn down + rtsp_stream::launch_session_clear(session_p->launch_session_id); session_p->control.peer = peer; - session_port = port; + // Use the local address from the control connection as the source address + // for other communications to the client. This is necessary to ensure + // proper routing on multi-homed hosts. + auto local_address = platf::from_sockaddr((sockaddr *) &peer->localAddress.address); + session_p->localAddress = boost::asio::ip::make_address(local_address); + + BOOST_LOG(debug) << "Control local address ["sv << local_address << ']'; + BOOST_LOG(debug) << "Control peer address ["sv << peer_addr << ':' << peer_port << ']'; + + // Insert this into the map for O(1) lookups in the future + auto ptslg = _peer_to_session.lock(); + _peer_to_session->emplace(peer, session_p); return session_p; } return nullptr; } + /** + * @brief Calls the handler for a given control stream message. + * @param type The message type. + * @param session The session the message was received on. + * @param payload The payload of the message. + * @param reinjected `true` if this message is being reprocessed after decryption. + */ void - control_server_t::call(std::uint16_t type, session_t *session, const std::string_view &payload) { + control_server_t::call(std::uint16_t type, session_t *session, const std::string_view &payload, bool reinjected) { + // If we are using the encrypted control stream protocol, drop any messages that come off the wire unencrypted + if (session->config.controlProtocolType == 13 && !reinjected && type != packetTypes[IDX_ENCRYPTED]) { + BOOST_LOG(error) << "Dropping unencrypted message on encrypted control stream: "sv << util::hex(type).to_string_view(); + return; + } + auto cb = _map_type_cb.find(type); if (cb == std::end(_map_type_cb)) { BOOST_LOG(debug) @@ -457,7 +583,7 @@ namespace stream { auto res = enet_host_service(_host.get(), &event, timeout.count()); if (res > 0) { - auto session = get_session(event.peer); + auto session = get_session(event.peer, event.data); if (!session) { BOOST_LOG(warning) << "Rejected connection from ["sv << platf::from_sockaddr((sockaddr *) &event.peer->address.address) << "]: it's not properly set up"sv; enet_peer_disconnect_now(event.peer, 0); @@ -474,7 +600,7 @@ namespace stream { auto type = *(std::uint16_t *) packet->data; std::string_view payload { (char *) packet->data + sizeof(type), packet->dataLength - sizeof(type) }; - call(type, session, payload); + call(type, session, payload, false); } break; case ENET_EVENT_TYPE_CONNECT: BOOST_LOG(info) << "CLIENT CONNECTED"sv; @@ -501,16 +627,17 @@ namespace stream { size_t percentage; size_t blocksize; + size_t prefixsize; util::buffer_t shards; char * data(size_t el) { - return &shards[el * blocksize]; + return &shards[(el + 1) * prefixsize + el * blocksize]; } - std::string_view - operator[](size_t el) const { - return { &shards[el * blocksize], blocksize }; + char * + prefix(size_t el) { + return &shards[el * (prefixsize + blocksize)]; } size_t @@ -520,7 +647,7 @@ namespace stream { }; static fec_t - encode(const std::string_view &payload, size_t blocksize, size_t fecpercentage, size_t minparityshards) { + encode(const std::string_view &payload, size_t blocksize, size_t fecpercentage, size_t minparityshards, size_t prefixsize) { auto payload_size = payload.size(); auto pad = payload_size % blocksize != 0; @@ -547,15 +674,21 @@ namespace stream { fecpercentage = 0; } - util::buffer_t shards { nr_shards * blocksize }; + util::buffer_t shards { nr_shards * (blocksize + prefixsize) }; util::buffer_t shards_p { nr_shards }; - // copy payload + padding - auto next = std::copy(std::begin(payload), std::end(payload), std::begin(shards)); - std::fill(next, std::end(shards), 0); // padding with zero - + auto next = std::begin(payload); for (auto x = 0; x < nr_shards; ++x) { - shards_p[x] = (uint8_t *) &shards[x * blocksize]; + shards_p[x] = (uint8_t *) &shards[(x + 1) * prefixsize + x * blocksize]; + + auto copy_len = std::min(blocksize, std::end(payload) - next); + std::copy_n(next, copy_len, shards_p[x]); + if (copy_len < blocksize) { + // Zero any additional space after the end of the payload + std::fill_n(shards_p[x] + copy_len, blocksize - copy_len, 0); + } + + next += copy_len; } if (data_shards + parity_shards <= DATA_SHARDS_MAX) { @@ -570,6 +703,7 @@ namespace stream { nr_shards, fecpercentage, blocksize, + prefixsize, std::move(shards) }; } @@ -621,38 +755,107 @@ namespace stream { return replaced; } + /** + * @brief Pass gamepad feedback data back to the client. + * @param session The session object. + * @param msg The message to pass. + * @return 0 on success. + */ int - send_rumble(session_t *session, std::uint16_t id, std::uint16_t lowfreq, std::uint16_t highfreq) { + send_feedback_msg(session_t *session, platf::gamepad_feedback_msg_t &msg) { if (!session->control.peer) { - BOOST_LOG(warning) << "Couldn't send rumble data, still waiting for PING from Moonlight"sv; + BOOST_LOG(warning) << "Couldn't send gamepad feedback data, still waiting for PING from Moonlight"sv; // Still waiting for PING from Moonlight return -1; } - control_rumble_t plaintext; - plaintext.header.type = packetTypes[IDX_RUMBLE_DATA]; - plaintext.header.payloadLength = sizeof(control_rumble_t) - sizeof(control_header_v2); + std::string payload; + if (msg.type == platf::gamepad_feedback_e::rumble) { + control_rumble_t plaintext; + plaintext.header.type = packetTypes[IDX_RUMBLE_DATA]; + plaintext.header.payloadLength = sizeof(plaintext) - sizeof(control_header_v2); - plaintext.useless = 0xC0FFEE; - plaintext.id = util::endian::little(id); - plaintext.lowfreq = util::endian::little(lowfreq); - plaintext.highfreq = util::endian::little(highfreq); + auto &data = msg.data.rumble; - BOOST_LOG(verbose) << id << " :: "sv << util::hex(lowfreq).to_string_view() << " :: "sv << util::hex(highfreq).to_string_view(); - std::array - encrypted_payload; + plaintext.useless = 0xC0FFEE; + plaintext.id = util::endian::little(msg.id); + plaintext.lowfreq = util::endian::little(data.lowfreq); + plaintext.highfreq = util::endian::little(data.highfreq); + + BOOST_LOG(verbose) << "Rumble: "sv << msg.id << " :: "sv << util::hex(data.lowfreq).to_string_view() << " :: "sv << util::hex(data.highfreq).to_string_view(); + std::array + encrypted_payload; + + payload = encode_control(session, util::view(plaintext), encrypted_payload); + } + else if (msg.type == platf::gamepad_feedback_e::rumble_triggers) { + control_rumble_triggers_t plaintext; + plaintext.header.type = packetTypes[IDX_RUMBLE_TRIGGER_DATA]; + plaintext.header.payloadLength = sizeof(plaintext) - sizeof(control_header_v2); + + auto &data = msg.data.rumble_triggers; + + plaintext.id = util::endian::little(msg.id); + plaintext.left = util::endian::little(data.left_trigger); + plaintext.right = util::endian::little(data.right_trigger); + + BOOST_LOG(verbose) << "Rumble triggers: "sv << msg.id << " :: "sv << util::hex(data.left_trigger).to_string_view() << " :: "sv << util::hex(data.right_trigger).to_string_view(); + std::array + encrypted_payload; + + payload = encode_control(session, util::view(plaintext), encrypted_payload); + } + else if (msg.type == platf::gamepad_feedback_e::set_motion_event_state) { + control_set_motion_event_t plaintext; + plaintext.header.type = packetTypes[IDX_SET_MOTION_EVENT]; + plaintext.header.payloadLength = sizeof(plaintext) - sizeof(control_header_v2); + + auto &data = msg.data.motion_event_state; + + plaintext.id = util::endian::little(msg.id); + plaintext.reportrate = util::endian::little(data.report_rate); + plaintext.type = data.motion_type; + + BOOST_LOG(verbose) << "Motion event state: "sv << msg.id << " :: "sv << util::hex(data.report_rate).to_string_view() << " :: "sv << util::hex(data.motion_type).to_string_view(); + std::array + encrypted_payload; + + payload = encode_control(session, util::view(plaintext), encrypted_payload); + } + else if (msg.type == platf::gamepad_feedback_e::set_rgb_led) { + control_set_rgb_led_t plaintext; + plaintext.header.type = packetTypes[IDX_SET_RGB_LED]; + plaintext.header.payloadLength = sizeof(plaintext) - sizeof(control_header_v2); + + auto &data = msg.data.rgb_led; + + plaintext.id = util::endian::little(msg.id); + plaintext.r = data.r; + plaintext.g = data.g; + plaintext.b = data.b; + + BOOST_LOG(verbose) << "RGB: "sv << msg.id << " :: "sv << util::hex(data.r).to_string_view() << util::hex(data.g).to_string_view() << util::hex(data.b).to_string_view(); + std::array + encrypted_payload; + + payload = encode_control(session, util::view(plaintext), encrypted_payload); + } + else { + BOOST_LOG(error) << "Unknown gamepad feedback message type"sv; + return -1; + } - auto payload = encode_control(session, util::view(plaintext), encrypted_payload); if (session->broadcast_ref->control_server.send(payload, session->control.peer)) { TUPLE_2D(port, addr, platf::from_sockaddr_ex((sockaddr *) &session->control.peer->address.address)); - BOOST_LOG(warning) << "Couldn't send termination code to ["sv << addr << ':' << port << ']'; + BOOST_LOG(warning) << "Couldn't send gamepad feedback to ["sv << addr << ':' << port << ']'; return -1; } - BOOST_LOG(debug) << "Send gamepadnr ["sv << id << "] with lowfreq ["sv << lowfreq << "] and highfreq ["sv << highfreq << ']'; - return 0; } @@ -690,7 +893,7 @@ namespace stream { void controlBroadcastThread(control_server_t *server) { server->map(packetTypes[IDX_PERIODIC_PING], [](session_t *session, const std::string_view &payload) { - BOOST_LOG(verbose) << "type [IDX_START_A]"sv; + BOOST_LOG(verbose) << "type [IDX_PERIODIC_PING]"sv; }); server->map(packetTypes[IDX_START_A], [&](session_t *session, const std::string_view &payload) { @@ -733,7 +936,7 @@ namespace stream { << "firstFrame [" << firstFrame << ']' << std::endl << "lastFrame [" << lastFrame << ']'; - session->video.idr_events->raise(true); + session->video.invalidate_ref_frames_events->raise(std::make_pair(firstFrame, lastFrame)); }); server->map(packetTypes[IDX_INPUT_DATA], [&](session_t *session, const std::string_view &payload) { @@ -745,7 +948,7 @@ namespace stream { std::vector plaintext; auto &cipher = session->control.cipher; - auto &iv = session->control.iv; + auto &iv = session->control.legacy_input_enc_iv; if (cipher.decrypt(tagged_cipher, plaintext, &iv)) { // something went wrong :( @@ -755,11 +958,10 @@ namespace stream { return; } - if (tagged_cipher_length >= 16 + sizeof(crypto::aes_t)) { + if (tagged_cipher_length >= 16 + iv.size()) { std::copy(payload.end() - 16, payload.end(), std::begin(iv)); } - input::print(plaintext.data()); input::passthrough(session->input, std::move(plaintext)); }); @@ -780,11 +982,27 @@ namespace stream { std::string_view tagged_cipher { (char *) header->payload(), (size_t) tagged_cipher_length }; auto &cipher = session->control.cipher; - crypto::aes_t iv {}; - iv[0] = (std::uint8_t) seq; + auto &iv = session->control.incoming_iv; + if (session->config.encryptionFlagsEnabled & SS_ENC_CONTROL_V2) { + // We use the deterministic IV construction algorithm specified in NIST SP 800-38D + // Section 8.2.1. The sequence number is our "invocation" field and the 'CC' in the + // high bytes is the "fixed" field. Because each client provides their own unique + // key, our values in the fixed field need only uniquely identify each independent + // use of the client's key with AES-GCM in our code. + // + // The sequence number is 32 bits long which allows for 2^32 control stream messages + // to be received from each client before the IV repeats. + iv.resize(12); + std::copy_n((uint8_t *) &seq, sizeof(seq), std::begin(iv)); + iv[10] = 'C'; // Client originated + iv[11] = 'C'; // Control stream + } + else { + // Nvidia's old style encryption uses a 16-byte IV + iv.resize(16); - // update control sequence - ++session->control.seq; + iv[0] = (std::uint8_t) seq; + } std::vector plaintext; if (cipher.decrypt(tagged_cipher, plaintext, &iv)) { @@ -796,57 +1014,64 @@ namespace stream { return; } - // Ensure compatibility with old packet type - std::string_view next_payload { (char *) plaintext.data(), plaintext.size() }; - auto type = *(std::uint16_t *) next_payload.data(); + auto type = *(std::uint16_t *) plaintext.data(); + std::string_view next_payload { (char *) plaintext.data() + 4, plaintext.size() - 4 }; if (type == packetTypes[IDX_ENCRYPTED]) { BOOST_LOG(error) << "Bad packet type [IDX_ENCRYPTED] found"sv; - session::stop(*session); return; } - // IDX_INPUT_DATA will attempt to decrypt unencrypted data, therefore we need to skip it. - if (type != packetTypes[IDX_INPUT_DATA]) { - server->call(type, session, next_payload); - - return; + // IDX_INPUT_DATA callback will attempt to decrypt unencrypted data, therefore we need pass it directly + if (type == packetTypes[IDX_INPUT_DATA]) { + plaintext.erase(std::begin(plaintext), std::begin(plaintext) + 4); + input::passthrough(session->input, std::move(plaintext)); + } + else { + server->call(type, session, next_payload, true); } - - // Ensure compatibility with IDX_INPUT_DATA - constexpr auto skip = sizeof(std::uint16_t) * 2; - plaintext.erase(std::begin(plaintext), std::begin(plaintext) + skip); - - input::print(plaintext.data()); - input::passthrough(session->input, std::move(plaintext)); }); // This thread handles latency-sensitive control messages platf::adjust_thread_priority(platf::thread_priority_e::critical); - auto shutdown_event = mail::man->event(mail::broadcast_shutdown); - while (!shutdown_event->peek()) { + // Check for both the full shutdown event and the shutdown event for this + // broadcast to ensure we can inform connected clients of our graceful + // termination when we shut down. + auto shutdown_event = mail::man->event(mail::shutdown); + auto broadcast_shutdown_event = mail::man->event(mail::broadcast_shutdown); + while (!shutdown_event->peek() && !broadcast_shutdown_event->peek()) { bool has_session_awaiting_peer = false; { - auto lg = server->_map_addr_session.lock(); + auto lg = server->_sessions.lock(); auto now = std::chrono::steady_clock::now(); - KITTY_WHILE_LOOP(auto pos = std::begin(*server->_map_addr_session), pos != std::end(*server->_map_addr_session), { - TUPLE_2D_REF(addr, port_session, *pos); - auto session = port_session.second; + KITTY_WHILE_LOOP(auto pos = std::begin(*server->_sessions), pos != std::end(*server->_sessions), { + // Don't perform additional session processing if we're shutting down + if (shutdown_event->peek() || broadcast_shutdown_event->peek()) { + break; + } + + auto session = *pos; if (now > session->pingTimeout) { - BOOST_LOG(info) << addr << ": Ping Timeout"sv; + auto address = session->control.peer ? platf::from_sockaddr((sockaddr *) &session->control.peer->address.address) : session->control.expected_peer_address; + BOOST_LOG(info) << address << ": Ping Timeout"sv; session::stop(*session); } if (session->state.load(std::memory_order_acquire) == session::state_e::STOPPING) { - pos = server->_map_addr_session->erase(pos); + pos = server->_sessions->erase(pos); if (session->control.peer) { + { + auto ptslg = server->_peer_to_session.lock(); + server->_peer_to_session->erase(session->control.peer); + } + enet_peer_disconnect_now(session->control.peer, 0); } @@ -860,22 +1085,20 @@ namespace stream { if (!session->control.peer) { has_session_awaiting_peer = true; } + else { + auto &feedback_queue = session->control.feedback_queue; + while (feedback_queue->peek()) { + auto feedback_msg = feedback_queue->pop(); - auto &rumble_queue = session->control.rumble_queue; - while (rumble_queue->peek()) { - auto rumble = rumble_queue->pop(); - - send_rumble(session, rumble->id, rumble->lowfreq, rumble->highfreq); - } + send_feedback_msg(session, *feedback_msg); + } - // Unlike rumble which we send as best-effort, HDR state messages are critical - // for proper functioning of some clients. We must wait to pop entries from - // the queue until we're sure we have a peer to send them to. - auto &hdr_queue = session->control.hdr_queue; - while (session->control.peer && hdr_queue->peek()) { - auto hdr_info = hdr_queue->pop(); + auto &hdr_queue = session->control.hdr_queue; + while (session->control.peer && hdr_queue->peek()) { + auto hdr_info = hdr_queue->pop(); - send_hdr_mode(session, std::move(hdr_info)); + send_hdr_mode(session, std::move(hdr_info)); + } } ++pos; @@ -904,9 +1127,9 @@ namespace stream { sizeof(control_encrypted_t) + crypto::cipher::round_to_pkcs7_padded(sizeof(plaintext)) + crypto::cipher::tag_size> encrypted_payload; - auto lg = server->_map_addr_session.lock(); - for (auto pos = std::begin(*server->_map_addr_session); pos != std::end(*server->_map_addr_session); ++pos) { - auto session = pos->second.second; + auto lg = server->_sessions.lock(); + for (auto pos = std::begin(*server->_sessions); pos != std::end(*server->_sessions); ++pos) { + auto session = *pos; // We may not have gotten far enough to have an ENet connection yet if (session->control.peer) { @@ -927,8 +1150,8 @@ namespace stream { void recvThread(broadcast_ctx_t &ctx) { - std::map peer_to_video_session; - std::map peer_to_audio_session; + std::map peer_to_video_session; + std::map peer_to_audio_session; auto &video_sock = ctx.video_sock; auto &audio_sock = ctx.audio_sock; @@ -946,30 +1169,30 @@ namespace stream { auto populate_peer_to_session = [&]() { while (message_queue_queue->peek()) { auto message_queue_opt = message_queue_queue->pop(); - TUPLE_3D_REF(socket_type, addr, message_queue, *message_queue_opt); + TUPLE_3D_REF(socket_type, session_id, message_queue, *message_queue_opt); switch (socket_type) { case socket_e::video: if (message_queue) { - peer_to_video_session.emplace(addr, message_queue); + peer_to_video_session.emplace(session_id, message_queue); } else { - peer_to_video_session.erase(addr); + peer_to_video_session.erase(session_id); } break; case socket_e::audio: if (message_queue) { - peer_to_audio_session.emplace(addr, message_queue); + peer_to_audio_session.emplace(session_id, message_queue); } else { - peer_to_audio_session.erase(addr); + peer_to_audio_session.erase(session_id); } break; } } }; - auto recv_func_init = [&](udp::socket &sock, int buf_elem, std::map &peer_to_session) { + auto recv_func_init = [&](udp::socket &sock, int buf_elem, std::map &peer_to_session) { recv_func[buf_elem] = [&, buf_elem](const boost::system::error_code &ec, size_t bytes) { auto fg = util::fail_guard([&]() { sock.async_receive_from(asio::buffer(buf[buf_elem]), peer, 0, recv_func[buf_elem]); @@ -990,10 +1213,23 @@ namespace stream { return; } - auto it = peer_to_session.find(peer.address()); - if (it != std::end(peer_to_session)) { - BOOST_LOG(debug) << "RAISE: "sv << peer.address().to_string() << ':' << peer.port() << " :: " << type_str; - it->second->raise(peer.port(), std::string { buf[buf_elem].data(), bytes }); + if (bytes == 4) { + // For legacy PING packets, find the matching session by address. + auto it = peer_to_session.find(peer.address()); + if (it != std::end(peer_to_session)) { + BOOST_LOG(debug) << "RAISE: "sv << peer.address().to_string() << ':' << peer.port() << " :: " << type_str; + it->second->raise(peer, std::string { buf[buf_elem].data(), bytes }); + } + } + else if (bytes >= sizeof(SS_PING)) { + auto ping = (PSS_PING) buf[buf_elem].data(); + + // For new PING packets that include a client identifier, search by payload. + auto it = peer_to_session.find(std::string { ping->payload, sizeof(ping->payload) }); + if (it != std::end(peer_to_session)) { + BOOST_LOG(debug) << "RAISE: "sv << peer.address().to_string() << ':' << peer.port() << " :: " << type_str; + it->second->raise(peer, std::string { buf[buf_elem].data(), bytes }); + } } }; }; @@ -1019,6 +1255,7 @@ namespace stream { platf::adjust_thread_priority(platf::thread_priority_e::high); stat_trackers::min_max_avg_tracker frame_processing_latency_tracker; + crypto::aes_t iv(12); while (auto packet = packets->pop()) { if (shutdown_event->peek()) { @@ -1028,13 +1265,32 @@ namespace stream { auto session = (session_t *) packet->channel_data; auto lowseq = session->video.lowseq; - auto av_packet = packet->av_packet; - std::string_view payload { (char *) av_packet->data, (size_t) av_packet->size }; - std::vector payload_new; + std::string_view payload { (char *) packet->data(), packet->data_size() }; + std::vector payload_with_replacements; + + // Apply replacements on the packet payload before performing any other operations. + // We need to know the final frame size to calculate the last packet size, and we + // must avoid matching replacements against the frame header or any other non-video + // part of the payload. + if (packet->is_idr() && packet->replacements) { + for (auto &replacement : *packet->replacements) { + auto frame_old = replacement.old; + auto frame_new = replacement._new; + + payload_with_replacements = replace(payload, frame_old, frame_new); + payload = { (char *) payload_with_replacements.data(), payload_with_replacements.size() }; + } + } video_short_frame_header_t frame_header = {}; frame_header.headerType = 0x01; // Short header type - frame_header.frameType = (av_packet->flags & AV_PKT_FLAG_KEY) ? 2 : 1; + frame_header.frameType = packet->is_idr() ? 2 : + packet->after_ref_frame_invalidation ? 5 : + 1; + frame_header.lastPayloadLen = (payload.size() + sizeof(frame_header)) % (session->config.packetsize - sizeof(NV_VIDEO_PACKET)); + if (frame_header.lastPayloadLen == 0) { + frame_header.lastPayloadLen = session->config.packetsize - sizeof(NV_VIDEO_PACKET); + } if (packet->frame_timestamp) { auto duration_to_latency = [](const std::chrono::steady_clock::duration &duration) { @@ -1059,21 +1315,12 @@ namespace stream { frame_header.frame_processing_latency = 0; } + std::vector payload_new; std::copy_n((uint8_t *) &frame_header, sizeof(frame_header), std::back_inserter(payload_new)); std::copy(std::begin(payload), std::end(payload), std::back_inserter(payload_new)); payload = { (char *) payload_new.data(), payload_new.size() }; - if (av_packet->flags & AV_PKT_FLAG_KEY) { - for (auto &replacement : *packet->replacements) { - auto frame_old = replacement.old; - auto frame_new = replacement._new; - - payload_new = replace(payload, frame_old, frame_new); - payload = { (char *) payload_new.data(), payload_new.size() }; - } - } - // insert packet headers auto blocksize = session->config.packetsize + MAX_RTP_HEADER_SIZE; auto payload_blocksize = blocksize - sizeof(video_packet_raw_t); @@ -1130,9 +1377,8 @@ namespace stream { for (int x = 0; x < packets; ++x) { auto *inspect = (video_packet_raw_t *) ¤t_payload[x * blocksize]; - auto av_packet = packet->av_packet; - inspect->packet.frameIndex = av_packet->pts; + inspect->packet.frameIndex = packet->frame_index(); inspect->packet.streamPacketIndex = ((uint32_t) lowseq + x) << 8; // Match multiFecFlags with Moonlight @@ -1148,7 +1394,9 @@ namespace stream { } } - auto shards = fec::encode(current_payload, blocksize, fecPercentage, session->config.minRequiredFecPackets); + // If video encryption is enabled, we allocate space for the encryption header before each shard + auto shards = fec::encode(current_payload, blocksize, fecPercentage, session->config.minRequiredFecPackets, + session->video.cipher ? sizeof(video_packet_enc_prefix_t) : 0); // set FEC info now that we know for sure what our percentage will be for this frame for (auto x = 0; x < shards.size(); ++x) { @@ -1168,17 +1416,39 @@ namespace stream { inspect->rtp.timestamp = util::endian::big(timestamp); inspect->packet.multiFecBlocks = (blockIndex << 4) | lastBlockIndex; - inspect->packet.frameIndex = av_packet->pts; + inspect->packet.frameIndex = packet->frame_index(); + + // Encrypt this shard if video encryption is enabled + if (session->video.cipher) { + // We use the deterministic IV construction algorithm specified in NIST SP 800-38D + // Section 8.2.1. The sequence number is our "invocation" field and the 'V' in the + // high bytes is the "fixed" field. Because each client provides their own unique + // key, our values in the fixed field need only uniquely identify each independent + // use of the client's key with AES-GCM in our code. + // + // The IV counter is 64 bits long which allows for 2^64 encrypted video packets + // to be sent to each client before the IV repeats. + std::copy_n((uint8_t *) &session->video.gcm_iv_counter, sizeof(session->video.gcm_iv_counter), std::begin(iv)); + iv[11] = 'V'; // Video stream + session->video.gcm_iv_counter++; + + // Encrypt the target buffer in place + auto *prefix = (video_packet_enc_prefix_t *) shards.prefix(x); + prefix->frameNumber = packet->frame_index(); + std::copy(std::begin(iv), std::end(iv), prefix->iv); + session->video.cipher->encrypt(std::string_view { (char *) inspect, (size_t) blocksize }, prefix->tag, &iv); + } } auto peer_address = session->video.peer.address(); auto batch_info = platf::batched_send_info_t { shards.shards.begin(), - shards.blocksize, + shards.prefixsize + shards.blocksize, shards.nr_shards, (uintptr_t) sock.native_handle(), peer_address, session->video.peer.port(), + session->localAddress, }; // Use a batched send if it's supported on this platform @@ -1186,15 +1456,24 @@ namespace stream { // Batched send is not available, so send each packet individually BOOST_LOG(verbose) << "Falling back to unbatched send"sv; for (auto x = 0; x < shards.size(); ++x) { - sock.send_to(asio::buffer(shards[x]), session->video.peer); + auto send_info = platf::send_info_t { + shards.prefix(x), + shards.prefixsize + shards.blocksize, + (uintptr_t) sock.native_handle(), + peer_address, + session->video.peer.port(), + session->localAddress, + }; + + platf::send(send_info); } } - if (av_packet->flags & AV_PKT_FLAG_KEY) { - BOOST_LOG(verbose) << "Key Frame ["sv << av_packet->pts << "] :: send ["sv << shards.size() << "] shards..."sv; + if (packet->is_idr()) { + BOOST_LOG(verbose) << "Key Frame ["sv << packet->frame_index() << "] :: send ["sv << shards.size() << "] shards..."sv; } else { - BOOST_LOG(verbose) << "Frame ["sv << av_packet->pts << "] :: send ["sv << shards.size() << "] shards..."sv << std::endl; + BOOST_LOG(verbose) << "Frame ["sv << packet->frame_index() << "] :: send ["sv << shards.size() << "] shards..."sv << std::endl; } ++blockIndex; @@ -1221,6 +1500,7 @@ namespace stream { audio_packet_t audio_packet { (audio_packet_raw_t *) malloc(sizeof(audio_packet_raw_t) + max_block_size) }; fec::rs_t rs { reed_solomon_new(RTPA_DATA_SHARDS, RTPA_FEC_SHARDS) }; + crypto::aes_t iv(16); // For unknown reasons, the RS parity matrix computed by our RS implementation // doesn't match the one Nvidia uses for audio data. I'm not exactly sure why, @@ -1248,11 +1528,9 @@ namespace stream { auto sequenceNumber = session->audio.sequenceNumber; auto timestamp = session->audio.timestamp; - // This will be mapped to big-endianness later - // For now, encode_audio needs it to be the proper sequenceNumber - audio_packet->rtp.sequenceNumber = sequenceNumber; + *(std::uint32_t *) iv.data() = util::endian::big(session->audio.avRiKeyId + sequenceNumber); - auto bytes = encode_audio(session->config.featureFlags, packet_data, audio_packet, session->audio.avRiKeyId, session->audio.cipher); + auto bytes = encode_audio(session->config.encryptionFlagsEnabled & SS_ENC_AUDIO, packet_data, audio_packet, iv, session->audio.cipher); if (bytes < 0) { BOOST_LOG(error) << "Couldn't encode audio packet"sv; break; @@ -1267,9 +1545,17 @@ namespace stream { auto &shards_p = session->audio.shards_p; std::copy_n(audio_packet->payload(), bytes, shards_p[sequenceNumber % RTPA_DATA_SHARDS]); + auto peer_address = session->audio.peer.address(); try { - sock.send_to(asio::buffer((char *) audio_packet.get(), sizeof(audio_packet_raw_t) + bytes), session->audio.peer); - + auto send_info = platf::send_info_t { + (const char *) audio_packet.get(), + sizeof(audio_packet_raw_t) + bytes, + (uintptr_t) sock.native_handle(), + peer_address, + session->audio.peer.port(), + session->localAddress, + }; + platf::send(send_info); BOOST_LOG(verbose) << "Audio ["sv << sequenceNumber << "] :: send..."sv; auto &fec_packet = session->audio.fec_packet; @@ -1287,7 +1573,16 @@ namespace stream { fec_packet->rtp.sequenceNumber = util::endian::big(sequenceNumber + x + 1); fec_packet->fecHeader.fecShardIndex = x; memcpy(fec_packet->payload(), shards_p[RTPA_DATA_SHARDS + x], bytes); - sock.send_to(asio::buffer((char *) fec_packet.get(), sizeof(audio_fec_packet_raw_t) + bytes), session->audio.peer); + + auto send_info = platf::send_info_t { + (const char *) fec_packet.get(), + sizeof(audio_fec_packet_raw_t) + bytes, + (uintptr_t) sock.native_handle(), + peer_address, + session->audio.peer.port(), + session->localAddress, + }; + platf::send(send_info); BOOST_LOG(verbose) << "Audio FEC ["sv << (sequenceNumber & ~(RTPA_DATA_SHARDS - 1)) << ' ' << x << "] :: send..."sv; } } @@ -1303,39 +1598,41 @@ namespace stream { int start_broadcast(broadcast_ctx_t &ctx) { - auto control_port = map_port(CONTROL_PORT); - auto video_port = map_port(VIDEO_STREAM_PORT); - auto audio_port = map_port(AUDIO_STREAM_PORT); + auto address_family = net::af_from_enum_string(config::sunshine.address_family); + auto protocol = address_family == net::IPV4 ? udp::v4() : udp::v6(); + auto control_port = net::map_port(CONTROL_PORT); + auto video_port = net::map_port(VIDEO_STREAM_PORT); + auto audio_port = net::map_port(AUDIO_STREAM_PORT); - if (ctx.control_server.bind(control_port)) { + if (ctx.control_server.bind(address_family, control_port)) { BOOST_LOG(error) << "Couldn't bind Control server to port ["sv << control_port << "], likely another process already bound to the port"sv; return -1; } boost::system::error_code ec; - ctx.video_sock.open(udp::v4(), ec); + ctx.video_sock.open(protocol, ec); if (ec) { BOOST_LOG(fatal) << "Couldn't open socket for Video server: "sv << ec.message(); return -1; } - ctx.video_sock.bind(udp::endpoint(udp::v4(), video_port), ec); + ctx.video_sock.bind(udp::endpoint(protocol, video_port), ec); if (ec) { BOOST_LOG(fatal) << "Couldn't bind Video server to port ["sv << video_port << "]: "sv << ec.message(); return -1; } - ctx.audio_sock.open(udp::v4(), ec); + ctx.audio_sock.open(protocol, ec); if (ec) { BOOST_LOG(fatal) << "Couldn't open socket for Audio server: "sv << ec.message(); return -1; } - ctx.audio_sock.bind(udp::endpoint(udp::v4(), audio_port), ec); + ctx.audio_sock.bind(udp::endpoint(protocol, audio_port), ec); if (ec) { BOOST_LOG(fatal) << "Couldn't bind Audio server to port ["sv << audio_port << "]: "sv << ec.message(); @@ -1389,17 +1686,24 @@ namespace stream { } int - recv_ping(decltype(broadcast)::ptr_t ref, socket_e type, udp::endpoint &peer, std::chrono::milliseconds timeout) { - auto constexpr ping = "PING"sv; - + recv_ping(session_t *session, decltype(broadcast)::ptr_t ref, socket_e type, std::string_view expected_payload, udp::endpoint &peer, std::chrono::milliseconds timeout) { auto messages = std::make_shared(30); - ref->message_queue_queue->raise(type, peer.address(), messages); + av_session_id_t session_id = std::string { expected_payload }; + + // Only allow matches on the peer address for legacy clients + if (!(session->config.mlFeatureFlags & ML_FF_SESSION_ID_V1)) { + ref->message_queue_queue->raise(type, peer.address(), messages); + } + ref->message_queue_queue->raise(type, session_id, messages); auto fg = util::fail_guard([&]() { messages->stop(); // remove message queue from session - ref->message_queue_queue->raise(type, peer.address(), nullptr); + if (!(session->config.mlFeatureFlags & ML_FF_SESSION_ID_V1)) { + ref->message_queue_queue->raise(type, peer.address(), nullptr); + } + ref->message_queue_queue->raise(type, session_id, nullptr); }); auto start_time = std::chrono::steady_clock::now(); @@ -1413,45 +1717,24 @@ namespace stream { break; } - TUPLE_2D_REF(port, msg, *msg_opt); - if (msg == ping) { - BOOST_LOG(debug) << "Received ping from "sv << peer.address() << ':' << port << " ["sv << util::hex_vec(msg) << ']'; - - // Update connection details. - { - auto addr_str = peer.address().to_string(); - - auto &connections = ref->audio_video_connections; - - auto lg = connections.lock(); - - std::remove_reference_t::iterator pos = std::end(*connections); - - for (auto it = std::begin(*connections); it != std::end(*connections); ++it) { - TUPLE_2D_REF(addr, port_ref, *it); - - if (!port_ref && addr_str == addr) { - pos = it; - } - else if (port_ref == port) { - break; - } - } - - if (pos == std::end(*connections)) { - continue; - } - - pos->second = port; - peer.port(port); - } - - return port; + TUPLE_2D_REF(recv_peer, msg, *msg_opt); + if (msg.find(expected_payload) != std::string::npos) { + // Match the new PING payload format + BOOST_LOG(debug) << "Received ping [v2] from "sv << recv_peer.address() << ':' << recv_peer.port() << " ["sv << util::hex_vec(msg) << ']'; + } + else if (!(session->config.mlFeatureFlags & ML_FF_SESSION_ID_V1) && msg == "PING"sv) { + // Match the legacy fixed PING payload only if the new type is not supported + BOOST_LOG(debug) << "Received ping [v1] from "sv << recv_peer.address() << ':' << recv_peer.port() << " ["sv << util::hex_vec(msg) << ']'; + } + else { + BOOST_LOG(debug) << "Received non-ping from "sv << recv_peer.address() << ':' << recv_peer.port() << " ["sv << util::hex_vec(msg) << ']'; + current_time = std::chrono::steady_clock::now(); + continue; } - BOOST_LOG(debug) << "Received non-ping from "sv << peer.address() << ':' << port << " ["sv << util::hex_vec(msg) << ']'; - - current_time = std::chrono::steady_clock::now(); + // Update connection details. + peer = recv_peer; + return 0; } BOOST_LOG(error) << "Initial Ping Timeout"sv; @@ -1467,17 +1750,15 @@ namespace stream { while_starting_do_nothing(session->state); auto ref = broadcast.ref(); - auto port = recv_ping(ref, socket_e::video, session->video.peer, config::stream.ping_timeout); - if (port < 0) { + auto error = recv_ping(session, ref, socket_e::video, session->video.ping_payload, session->video.peer, config::stream.ping_timeout); + if (error < 0) { return; } - // Enable QoS tagging on video traffic if requested by the client - if (session->config.videoQosType) { - auto address = session->video.peer.address(); - session->video.qos = platf::enable_socket_qos(ref->video_sock.native_handle(), address, - session->video.peer.port(), platf::qos_data_type_e::video); - } + // Enable local prioritization and QoS tagging on video traffic if requested by the client + auto address = session->video.peer.address(); + session->video.qos = platf::enable_socket_qos(ref->video_sock.native_handle(), address, + session->video.peer.port(), platf::qos_data_type_e::video, session->config.videoQosType != 0); BOOST_LOG(debug) << "Start capturing Video"sv; video::capture(session->mail, session->config.monitor, session); @@ -1492,17 +1773,15 @@ namespace stream { while_starting_do_nothing(session->state); auto ref = broadcast.ref(); - auto port = recv_ping(ref, socket_e::audio, session->audio.peer, config::stream.ping_timeout); - if (port < 0) { + auto error = recv_ping(session, ref, socket_e::audio, session->audio.ping_payload, session->audio.peer, config::stream.ping_timeout); + if (error < 0) { return; } - // Enable QoS tagging on audio traffic if requested by the client - if (session->config.audioQosType) { - auto address = session->audio.peer.address(); - session->audio.qos = platf::enable_socket_qos(ref->audio_sock.native_handle(), address, - session->audio.peer.port(), platf::qos_data_type_e::audio); - } + // Enable local prioritization and QoS tagging on audio traffic if requested by the client + auto address = session->audio.peer.address(); + session->audio.qos = platf::enable_socket_qos(ref->audio_sock.native_handle(), address, + session->audio.peer.port(), platf::qos_data_type_e::audio, session->config.audioQosType != 0); BOOST_LOG(debug) << "Start capturing Audio"sv; audio::capture(session->mail, session->config.audio, session); @@ -1535,8 +1814,8 @@ namespace stream { // The alternative is that Sunshine can never start another session until it's manually restarted. auto task = []() { BOOST_LOG(fatal) << "Hang detected! Session failed to terminate in 10 seconds."sv; - log_flush(); - std::abort(); + logging::log_flush(); + lifetime::debug_trap(); }; auto force_kill = task_pool.pushDelayed(task, 10s).task_id; auto fg = util::fail_guard([&force_kill]() { @@ -1554,43 +1833,13 @@ namespace stream { BOOST_LOG(debug) << "Resetting Input..."sv; input::reset(session.input); - BOOST_LOG(debug) << "Removing references to any connections..."sv; - { - auto video_addr = session.video.peer.address().to_string(); - auto audio_addr = session.audio.peer.address().to_string(); - - auto video_port = session.video.peer.port(); - auto audio_port = session.audio.peer.port(); - - auto &connections = session.broadcast_ref->audio_video_connections; - - auto lg = connections.lock(); - - auto validate_size = connections->size(); - for (auto it = std::begin(*connections); it != std::end(*connections);) { - TUPLE_2D_REF(addr, port, *it); - - if ((video_port == port && video_addr == addr) || - (audio_port == port && audio_addr == addr)) { - it = connections->erase(it); - } - else { - ++it; - } - } - - auto new_size = connections->size(); - if (validate_size != new_size + 2) { - BOOST_LOG(warning) << "Couldn't remove reference to session connections: ending all broadcasts"sv; - - // A reference to the event object is still stored somewhere else. So no need to keep - // a reference to it. - mail::man->event(mail::broadcast_shutdown)->raise(true); - } - } - // If this is the last session, invoke the platform callbacks if (--running_sessions == 0) { +#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 + if (proc::proc.running()) { + system_tray::update_tray_pausing(proc::proc.get_last_run_app_name()); + } +#endif platf::streaming_will_stop(); } @@ -1606,7 +1855,14 @@ namespace stream { return -1; } - session.broadcast_ref->control_server.emplace_addr_to_session(addr_string, session); + session.control.expected_peer_address = addr_string; + BOOST_LOG(debug) << "Expecting incoming session connections from "sv << addr_string; + + // Insert this session into the session list + { + auto lg = session.broadcast_ref->control_server._sessions.lock(); + session.broadcast_ref->control_server._sessions->push_back(&session); + } auto addr = boost::asio::ip::make_address(addr_string); session.video.peer.address(addr); @@ -1615,16 +1871,6 @@ namespace stream { session.audio.peer.address(addr); session.audio.peer.port(0); - { - auto &connections = session.broadcast_ref->audio_video_connections; - - auto lg = connections.lock(); - - // allocate a location for connections - connections->emplace_back(addr_string, 0); - connections->emplace_back(addr_string, 0); - } - session.pingTimeout = std::chrono::steady_clock::now() + config::stream.ping_timeout; session.audioThread = std::thread { audioThread, &session }; @@ -1635,30 +1881,44 @@ namespace stream { // If this is the first session, invoke the platform callbacks if (++running_sessions == 1) { platf::streaming_will_start(); +#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 + system_tray::update_tray_playing(proc::proc.get_last_run_app_name()); +#endif } return 0; } std::shared_ptr - alloc(config_t &config, crypto::aes_t &gcm_key, crypto::aes_t &iv) { + alloc(config_t &config, rtsp_stream::launch_session_t &launch_session) { auto session = std::make_shared(); auto mail = std::make_shared(); session->shutdown_event = mail->event(mail::shutdown); + session->launch_session_id = launch_session.id; session->config = config; - session->control.rumble_queue = mail->queue(mail::rumble); + session->control.connect_data = launch_session.control_connect_data; + session->control.feedback_queue = mail->queue(mail::gamepad_feedback); session->control.hdr_queue = mail->event(mail::hdr); - session->control.iv = iv; + session->control.legacy_input_enc_iv = launch_session.iv; session->control.cipher = crypto::cipher::gcm_t { - gcm_key, false + launch_session.gcm_key, false }; session->video.idr_events = mail->event(mail::idr); + session->video.invalidate_ref_frames_events = mail->event>(mail::invalidate_ref_frames); session->video.lowseq = 0; + session->video.ping_payload = launch_session.av_ping_payload; + if (config.encryptionFlagsEnabled & SS_ENC_VIDEO) { + BOOST_LOG(info) << "Video encryption enabled"sv; + session->video.cipher = crypto::cipher::gcm_t { + launch_session.gcm_key, false + }; + session->video.gcm_iv_counter = 0; + } constexpr auto max_block_size = crypto::cipher::round_to_pkcs7_padded(2048); @@ -1685,10 +1945,11 @@ namespace stream { session->audio.fec_packet->fecHeader.ssrc = 0; session->audio.cipher = crypto::cipher::cbc_t { - gcm_key, true + launch_session.gcm_key, true }; - session->audio.avRiKeyId = util::endian::big(*(std::uint32_t *) iv.data()); + session->audio.ping_payload = launch_session.av_ping_payload; + session->audio.avRiKeyId = util::endian::big(*(std::uint32_t *) launch_session.iv.data()); session->audio.sequenceNumber = 0; session->audio.timestamp = 0; diff --git a/src/stream.h b/src/stream.h index 302b9c0e64e..565ae4ed56e 100644 --- a/src/stream.h +++ b/src/stream.h @@ -3,6 +3,7 @@ * @brief todo */ #pragma once +#include #include @@ -22,11 +23,13 @@ namespace stream { int packetsize; int minRequiredFecPackets; - int featureFlags; + int mlFeatureFlags; int controlProtocolType; int audioQosType; int videoQosType; + uint32_t encryptionFlagsEnabled; + std::optional gcmap; }; @@ -39,7 +42,7 @@ namespace stream { }; std::shared_ptr - alloc(config_t &config, crypto::aes_t &gcm_key, crypto::aes_t &iv); + alloc(config_t &config, rtsp_stream::launch_session_t &launch_session); int start(session_t &session, const std::string &addr_string); void diff --git a/src/system_tray.cpp b/src/system_tray.cpp index ed66357a6ac..06a147f2b3b 100644 --- a/src/system_tray.cpp +++ b/src/system_tray.cpp @@ -9,11 +9,20 @@ #define WIN32_LEAN_AND_MEAN #include #include - #define TRAY_ICON WEB_DIR "images/favicon.ico" + #define TRAY_ICON WEB_DIR "images/sunshine.ico" + #define TRAY_ICON_PLAYING WEB_DIR "images/sunshine-playing.ico" + #define TRAY_ICON_PAUSING WEB_DIR "images/sunshine-pausing.ico" + #define TRAY_ICON_LOCKED WEB_DIR "images/sunshine-locked.ico" #elif defined(__linux__) || defined(linux) || defined(__linux) - #define TRAY_ICON "sunshine" + #define TRAY_ICON "sunshine-tray" + #define TRAY_ICON_PLAYING "sunshine-playing" + #define TRAY_ICON_PAUSING "sunshine-pausing" + #define TRAY_ICON_LOCKED "sunshine-locked" #elif defined(__APPLE__) || defined(__MACH__) #define TRAY_ICON WEB_DIR "images/logo-sunshine-16.png" + #define TRAY_ICON_PLAYING WEB_DIR "images/sunshine-playing-16.png" + #define TRAY_ICON_PAUSING WEB_DIR "images/sunshine-pausing-16.png" + #define TRAY_ICON_LOCKED WEB_DIR "images/sunshine-locked-16.png" #include #endif @@ -22,20 +31,23 @@ #include // lib includes - #include "tray/tray.h" + #include "tray/src/tray.h" #include #include // local includes #include "confighttp.h" - #include "main.h" + #include "logging.h" #include "platform/common.h" #include "process.h" + #include "src/entry_handler.h" + #include "version.h" using namespace std::literals; // system_tray namespace namespace system_tray { + static std::atomic tray_initialized = false; /** * @brief Callback for opening the UI from the system tray. @@ -100,25 +112,24 @@ namespace system_tray { */ void tray_quit_cb(struct tray_menu *item) { - BOOST_LOG(info) << "Quiting from system tray"sv; + BOOST_LOG(info) << "Quitting from system tray"sv; #ifdef _WIN32 // If we're running in a service, return a special status to // tell it to terminate too, otherwise it will just respawn us. if (GetConsoleWindow() == NULL) { - lifetime::exit_sunshine(ERROR_SHUTDOWN_IN_PROGRESS, false); + lifetime::exit_sunshine(ERROR_SHUTDOWN_IN_PROGRESS, true); + return; } #endif - lifetime::exit_sunshine(0, false); + lifetime::exit_sunshine(0, true); } // Tray menu static struct tray tray = { .icon = TRAY_ICON, - #if defined(_WIN32) - .tooltip = const_cast("Sunshine"), // cast the string literal to a non-const char* pointer - #endif + .tooltip = PROJECT_NAME, .menu = (struct tray_menu[]) { // todo - use boost/locale to translate menu strings @@ -136,6 +147,8 @@ namespace system_tray { { .text = "Restart", .cb = tray_restart_cb }, { .text = "Quit", .cb = tray_quit_cb }, { .text = nullptr } }, + .iconPathCount = 4, + .allIconPaths = { TRAY_ICON, TRAY_ICON_LOCKED, TRAY_ICON_PLAYING, TRAY_ICON_PAUSING }, }; /** @@ -228,6 +241,7 @@ namespace system_tray { BOOST_LOG(info) << "System tray created"sv; } + tray_initialized = true; while (tray_loop(1) == 0) { BOOST_LOG(debug) << "System tray loop"sv; } @@ -264,9 +278,114 @@ namespace system_tray { */ int end_tray() { + tray_initialized = false; tray_exit(); return 0; } + /** + * @brief Sets the tray icon in playing mode and spawns the appropriate notification + * @param app_name The started application name + */ + void + update_tray_playing(std::string app_name) { + if (!tray_initialized) { + return; + } + + tray.notification_title = NULL; + tray.notification_text = NULL; + tray.notification_cb = NULL; + tray.notification_icon = NULL; + tray.icon = TRAY_ICON_PLAYING; + tray_update(&tray); + tray.icon = TRAY_ICON_PLAYING; + tray.notification_title = "Stream Started"; + char msg[256]; + snprintf(msg, std::size(msg), "Streaming started for %s", app_name.c_str()); + tray.notification_text = msg; + tray.tooltip = msg; + tray.notification_icon = TRAY_ICON_PLAYING; + tray_update(&tray); + } + + /** + * @brief Sets the tray icon in pausing mode (stream stopped but app running) and spawns the appropriate notification + * @param app_name The paused application name + */ + void + update_tray_pausing(std::string app_name) { + if (!tray_initialized) { + return; + } + + tray.notification_title = NULL; + tray.notification_text = NULL; + tray.notification_cb = NULL; + tray.notification_icon = NULL; + tray.icon = TRAY_ICON_PAUSING; + tray_update(&tray); + char msg[256]; + snprintf(msg, std::size(msg), "Streaming paused for %s", app_name.c_str()); + tray.icon = TRAY_ICON_PAUSING; + tray.notification_title = "Stream Paused"; + tray.notification_text = msg; + tray.tooltip = msg; + tray.notification_icon = TRAY_ICON_PAUSING; + tray_update(&tray); + } + + /** + * @brief Sets the tray icon in stopped mode (app and stream stopped) and spawns the appropriate notification + * @param app_name The started application name + */ + void + update_tray_stopped(std::string app_name) { + if (!tray_initialized) { + return; + } + + tray.notification_title = NULL; + tray.notification_text = NULL; + tray.notification_cb = NULL; + tray.notification_icon = NULL; + tray.icon = TRAY_ICON; + tray_update(&tray); + char msg[256]; + snprintf(msg, std::size(msg), "Application %s successfully stopped", app_name.c_str()); + tray.icon = TRAY_ICON; + tray.notification_icon = TRAY_ICON; + tray.notification_title = "Application Stopped"; + tray.notification_text = msg; + tray.tooltip = PROJECT_NAME; + tray_update(&tray); + } + + /** + * @brief Spawns a notification for PIN Pairing. Clicking it opens the PIN Web UI Page + */ + void + update_tray_require_pin() { + if (!tray_initialized) { + return; + } + + tray.notification_title = NULL; + tray.notification_text = NULL; + tray.notification_cb = NULL; + tray.notification_icon = NULL; + tray.icon = TRAY_ICON; + tray_update(&tray); + tray.icon = TRAY_ICON; + tray.notification_title = "Incoming Pairing Request"; + tray.notification_text = "Click here to complete the pairing process"; + tray.notification_icon = TRAY_ICON_LOCKED; + tray.tooltip = PROJECT_NAME; + tray.notification_cb = []() { + launch_ui_with_path("/pin"); + }; + tray_update(&tray); + } + } // namespace system_tray #endif diff --git a/src/system_tray.h b/src/system_tray.h index 18e3445f6bc..f824c8a13be 100644 --- a/src/system_tray.h +++ b/src/system_tray.h @@ -27,5 +27,13 @@ namespace system_tray { run_tray(); int end_tray(); + void + update_tray_playing(std::string app_name); + void + update_tray_pausing(std::string app_name); + void + update_tray_stopped(std::string app_name); + void + update_tray_require_pin(); } // namespace system_tray diff --git a/src/thread_safe.h b/src/thread_safe.h index d745bf0886f..1135c441d74 100644 --- a/src/thread_safe.h +++ b/src/thread_safe.h @@ -38,7 +38,7 @@ namespace safe { _cv.notify_all(); } - // pop and view shoud not be used interchangeably + // pop and view should not be used interchangeably status_t pop() { std::unique_lock ul { _lock }; @@ -60,7 +60,7 @@ namespace safe { return val; } - // pop and view shoud not be used interchangeably + // pop and view should not be used interchangeably template status_t pop(std::chrono::duration delay) { @@ -81,8 +81,8 @@ namespace safe { return val; } - // pop and view shoud not be used interchangeably - const status_t & + // pop and view should not be used interchangeably + status_t view() { std::unique_lock ul { _lock }; @@ -101,7 +101,7 @@ namespace safe { return _status; } - // pop and view shoud not be used interchangeably + // pop and view should not be used interchangeably template status_t view(std::chrono::duration delay) { diff --git a/src/upnp.cpp b/src/upnp.cpp index 6fc5a1309a7..f65bcb87cc4 100644 --- a/src/upnp.cpp +++ b/src/upnp.cpp @@ -2,12 +2,13 @@ * @file src/upnp.cpp * @brief todo */ -#include -#include +#include +#include #include "config.h" #include "confighttp.h" -#include "main.h" +#include "globals.h" +#include "logging.h" #include "network.h" #include "nvhttp.h" #include "rtsp.h" @@ -61,13 +62,13 @@ namespace upnp { class deinit_t: public platf::deinit_t { public: deinit_t() { - auto rtsp = std::to_string(::map_port(rtsp_stream::RTSP_SETUP_PORT)); - auto video = std::to_string(::map_port(stream::VIDEO_STREAM_PORT)); - auto audio = std::to_string(::map_port(stream::AUDIO_STREAM_PORT)); - auto control = std::to_string(::map_port(stream::CONTROL_PORT)); - auto gs_http = std::to_string(::map_port(nvhttp::PORT_HTTP)); - auto gs_https = std::to_string(::map_port(nvhttp::PORT_HTTPS)); - auto wm_http = std::to_string(::map_port(confighttp::PORT_HTTPS)); + auto rtsp = std::to_string(net::map_port(rtsp_stream::RTSP_SETUP_PORT)); + auto video = std::to_string(net::map_port(stream::VIDEO_STREAM_PORT)); + auto audio = std::to_string(net::map_port(stream::AUDIO_STREAM_PORT)); + auto control = std::to_string(net::map_port(stream::CONTROL_PORT)); + auto gs_http = std::to_string(net::map_port(nvhttp::PORT_HTTP)); + auto gs_https = std::to_string(net::map_port(nvhttp::PORT_HTTPS)); + auto wm_http = std::to_string(net::map_port(confighttp::PORT_HTTPS)); mappings.assign({ { { rtsp, rtsp, "TCP"s }, "Sunshine - RTSP"s }, @@ -91,6 +92,84 @@ namespace upnp { upnp_thread.join(); } + /** + * @brief Opens pinholes for IPv6 traffic if the IGD is capable. + * @details Not many IGDs support this feature, so we perform error logging with debug level. + * @return true if the pinholes were opened successfully. + */ + bool + create_ipv6_pinholes() { + int err; + device_t device { upnpDiscover(2000, nullptr, nullptr, 0, IPv6, 2, &err) }; + if (!device || err) { + BOOST_LOG(debug) << "Couldn't discover any IPv6 UPNP devices"sv; + return false; + } + + IGDdatas data; + urls_t urls; + std::array lan_addr; + auto status = UPNP_GetValidIGD(device.get(), &urls.el, &data, lan_addr.data(), lan_addr.size()); + if (status != 1 && status != 2) { + BOOST_LOG(debug) << "No valid IPv6 IGD: "sv << status_string(status); + return false; + } + + if (data.IPv6FC.controlurl[0] != 0) { + int firewallEnabled, pinholeAllowed; + + // Check if this firewall supports IPv6 pinholes + err = UPNP_GetFirewallStatus(urls->controlURL_6FC, data.IPv6FC.servicetype, &firewallEnabled, &pinholeAllowed); + if (err == UPNPCOMMAND_SUCCESS) { + BOOST_LOG(debug) << "UPnP IPv6 firewall control available. Firewall is "sv + << (firewallEnabled ? "enabled"sv : "disabled"sv) + << ", pinhole is "sv + << (pinholeAllowed ? "allowed"sv : "disallowed"sv); + + if (pinholeAllowed) { + // Create pinholes for each port + auto mapping_period = std::to_string(PORT_MAPPING_LIFETIME.count()); + auto shutdown_event = mail::man->event(mail::shutdown); + + for (auto it = std::begin(mappings); it != std::end(mappings) && !shutdown_event->peek(); ++it) { + auto mapping = *it; + char uniqueId[8]; + + // Open a pinhole for the LAN port, since there will be no WAN->LAN port mapping on IPv6 + err = UPNP_AddPinhole(urls->controlURL_6FC, + data.IPv6FC.servicetype, + "", "0", + lan_addr.data(), + mapping.port.lan.c_str(), + mapping.port.proto.c_str(), + mapping_period.c_str(), + uniqueId); + if (err == UPNPCOMMAND_SUCCESS) { + BOOST_LOG(debug) << "Successfully created pinhole for "sv << mapping.port.proto << ' ' << mapping.port.lan; + } + else { + BOOST_LOG(debug) << "Failed to create pinhole for "sv << mapping.port.proto << ' ' << mapping.port.lan << ": "sv << err; + } + } + + return err == 0; + } + else { + BOOST_LOG(debug) << "IPv6 pinholes are not allowed by the IGD"sv; + return false; + } + } + else { + BOOST_LOG(debug) << "Failed to get IPv6 firewall status: "sv << err; + return false; + } + } + else { + BOOST_LOG(debug) << "IPv6 Firewall Control is not supported by the IGD"sv; + return false; + } + } + /** * @brief Maps a port via UPnP. * @param data IGDdatas from UPNP_GetValidIGD() @@ -100,7 +179,7 @@ namespace upnp { * @return `true` on success. */ bool - map_port(const IGDdatas &data, const urls_t &urls, const std::string &lan_addr, const mapping_t &mapping) { + map_upnp_port(const IGDdatas &data, const urls_t &urls, const std::string &lan_addr, const mapping_t &mapping) { char intClient[16]; char intPort[6]; char desc[80]; @@ -205,7 +284,7 @@ namespace upnp { * @param data urls_t from UPNP_GetValidIGD() */ void - unmap_all_ports(const urls_t &urls, const IGDdatas &data) { + unmap_all_upnp_ports(const urls_t &urls, const IGDdatas &data) { for (auto it = std::begin(mappings); it != std::end(mappings); ++it) { auto status = UPNP_DeletePortMapping( urls->controlURL, @@ -232,6 +311,7 @@ namespace upnp { bool mapped = false; IGDdatas data; urls_t mapped_urls; + auto address_family = net::af_from_enum_string(config::sunshine.address_family); // Refresh UPnP rules every few minutes. They can be lost if the router reboots, // WAN IP address changes, or various other conditions. @@ -239,7 +319,7 @@ namespace upnp { int err = 0; device_t device { upnpDiscover(2000, nullptr, nullptr, 0, IPv4, 2, &err) }; if (!device || err) { - BOOST_LOG(warning) << "Couldn't discover any UPNP devices"sv; + BOOST_LOG(warning) << "Couldn't discover any IPv4 UPNP devices"sv; mapped = false; continue; } @@ -263,13 +343,21 @@ namespace upnp { BOOST_LOG(debug) << "Found valid IGD device: "sv << urls->rootdescURL; for (auto it = std::begin(mappings); it != std::end(mappings) && !shutdown_event->peek(); ++it) { - map_port(data, urls, lan_addr_str, *it); + map_upnp_port(data, urls, lan_addr_str, *it); } if (!mapped) { BOOST_LOG(info) << "Completed UPnP port mappings to "sv << lan_addr_str << " via "sv << urls->rootdescURL; } + // If we are listening on IPv6 and the IGD has an IPv6 firewall enabled, try to create IPv6 firewall pinholes + if (address_family == net::af_e::BOTH) { + if (create_ipv6_pinholes() && !mapped) { + // Only log the first time through + BOOST_LOG(info) << "Successfully opened IPv6 pinholes on the IGD"sv; + } + } + mapped = true; mapped_urls = std::move(urls); } while (!shutdown_event->view(REFRESH_INTERVAL)); @@ -277,7 +365,7 @@ namespace upnp { if (mapped) { // Unmap ports upon termination BOOST_LOG(info) << "Unmapping UPNP ports..."sv; - unmap_all_ports(mapped_urls, data); + unmap_all_upnp_ports(mapped_urls, data); } } diff --git a/src/video.cpp b/src/video.cpp index 545e2fbdfea..394e7d46fdf 100644 --- a/src/video.cpp +++ b/src/video.cpp @@ -7,15 +7,21 @@ #include #include +#include + extern "C" { +#include #include -#include +#include +#include } #include "cbs.h" #include "config.h" +#include "globals.h" #include "input.h" -#include "main.h" +#include "logging.h" +#include "nvenc/nvenc_base.h" #include "platform/common.h" #include "sync.h" #include "video.h" @@ -44,12 +50,6 @@ namespace video { av_buffer_unref(&ref); } - using ctx_t = util::safe_ptr; - using frame_t = util::safe_ptr; - using buffer_t = util::safe_ptr; - using sws_t = util::safe_ptr; - using img_event_t = std::shared_ptr>>; - namespace nv { enum class profile_h264_e : int { @@ -80,49 +80,50 @@ namespace video { }; } // namespace qsv - platf::mem_type_e - map_base_dev_type(AVHWDeviceType type); - platf::pix_fmt_e - map_pix_fmt(AVPixelFormat fmt); - - util::Either - dxgi_make_hwdevice_ctx(platf::hwdevice_t *hwdevice_ctx); - util::Either - vaapi_make_hwdevice_ctx(platf::hwdevice_t *hwdevice_ctx); - util::Either - cuda_make_hwdevice_ctx(platf::hwdevice_t *hwdevice_ctx); + util::Either + dxgi_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *); + util::Either + vaapi_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *); + util::Either + cuda_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *); + util::Either + vt_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *); - int - hwframe_ctx(ctx_t &ctx, platf::hwdevice_t *hwdevice, buffer_t &hwdevice_ctx, AVPixelFormat format); - - class swdevice_t: public platf::hwdevice_t { + class avcodec_software_encode_device_t: public platf::avcodec_encode_device_t { public: int convert(platf::img_t &img) override { - av_frame_make_writable(sw_frame.get()); - - const int linesizes[2] { - img.row_pitch, 0 - }; - - std::uint8_t *data[4]; - - data[0] = sw_frame->data[0] + offsetY; - if (sw_frame->format == AV_PIX_FMT_NV12) { - data[1] = sw_frame->data[1] + offsetUV * 2; - data[2] = nullptr; - } - else { - data[1] = sw_frame->data[1] + offsetUV; - data[2] = sw_frame->data[2] + offsetUV; - data[3] = nullptr; + // If we need to add aspect ratio padding, we need to scale into an intermediate output buffer + bool requires_padding = (sw_frame->width != sws_output_frame->width || sw_frame->height != sws_output_frame->height); + + // Setup the input frame using the caller's img_t + sws_input_frame->data[0] = img.data; + sws_input_frame->linesize[0] = img.row_pitch; + + // Perform color conversion and scaling to the final size + auto status = sws_scale_frame(sws.get(), requires_padding ? sws_output_frame.get() : sw_frame.get(), sws_input_frame.get()); + if (status < 0) { + char string[AV_ERROR_MAX_STRING_SIZE]; + BOOST_LOG(error) << "Couldn't scale frame: "sv << av_make_error_string(string, AV_ERROR_MAX_STRING_SIZE, status); + return -1; } - int ret = sws_scale(sws.get(), (std::uint8_t *const *) &img.data, linesizes, 0, img.height, data, sw_frame->linesize); - if (ret <= 0) { - BOOST_LOG(error) << "Couldn't convert image to required format and/or size"sv; - - return -1; + // If we require aspect ratio padding, copy the output frame into the final padded frame + if (requires_padding) { + auto fmt_desc = av_pix_fmt_desc_get((AVPixelFormat) sws_output_frame->format); + auto planes = av_pix_fmt_count_planes((AVPixelFormat) sws_output_frame->format); + for (int plane = 0; plane < planes; plane++) { + auto shift_h = plane == 0 ? 0 : fmt_desc->log2_chroma_h; + auto shift_w = plane == 0 ? 0 : fmt_desc->log2_chroma_w; + auto offset = ((offsetW >> shift_w) * fmt_desc->comp[plane].step) + (offsetH >> shift_h) * sw_frame->linesize[plane]; + + // Copy line-by-line to preserve leading padding for each row + for (int line = 0; line < sws_output_frame->height >> shift_h; line++) { + memcpy(sw_frame->data[plane] + offset + (line * sw_frame->linesize[plane]), + sws_output_frame->data[plane] + (line * sws_output_frame->linesize[plane]), + (size_t) (sws_output_frame->width >> shift_w) * fmt_desc->comp[plane].step); + } + } } // If frame is not a software frame, it means we still need to transfer from main memory @@ -157,53 +158,24 @@ namespace video { } void - set_colorspace(std::uint32_t colorspace, std::uint32_t color_range) override { + apply_colorspace() override { + auto avcodec_colorspace = avcodec_colorspace_from_sunshine_colorspace(colorspace); sws_setColorspaceDetails(sws.get(), sws_getCoefficients(SWS_CS_DEFAULT), 0, - sws_getCoefficients(colorspace), color_range - 1, + sws_getCoefficients(avcodec_colorspace.software_format), avcodec_colorspace.range - 1, 0, 1 << 16, 1 << 16); } /** * When preserving aspect ratio, ensure that padding is black */ - int + void prefill() { auto frame = sw_frame ? sw_frame.get() : this->frame; - auto width = frame->width; - auto height = frame->height; - av_frame_get_buffer(frame, 0); - sws_t sws { - sws_getContext( - width, height, AV_PIX_FMT_BGR0, - width, height, (AVPixelFormat) frame->format, - SWS_LANCZOS | SWS_ACCURATE_RND, - nullptr, nullptr, nullptr) - }; - - if (!sws) { - return -1; - } - - util::buffer_t img { (std::size_t)(width * height) }; - std::fill(std::begin(img), std::end(img), 0); - - const int linesizes[2] { - width, 0 - }; - av_frame_make_writable(frame); - - auto data = img.begin(); - int ret = sws_scale(sws.get(), (std::uint8_t *const *) &data, linesizes, 0, height, frame->data, frame->linesize); - if (ret <= 0) { - BOOST_LOG(error) << "Couldn't convert image to required format and/or size"sv; - - return -1; - } - - return 0; + ptrdiff_t linesize[4] = { frame->linesize[0], frame->linesize[1], frame->linesize[2], frame->linesize[3] }; + av_image_fill_black(frame->data, linesize, (AVPixelFormat) frame->format, frame->color_range, frame->width, frame->height); } int @@ -220,9 +192,8 @@ namespace video { this->frame = frame; } - if (prefill()) { - return -1; - } + // Fill aspect ratio padding in the destination frame + prefill(); auto out_width = frame->width; auto out_height = frame->height; @@ -232,133 +203,97 @@ namespace video { out_width = in_width * scalar; out_height = in_height * scalar; - // result is always positive - auto offsetW = (frame->width - out_width) / 2; - auto offsetH = (frame->height - out_height) / 2; - offsetUV = (offsetW + offsetH * frame->width / 2) / 2; - offsetY = offsetW + offsetH * frame->width; + sws_input_frame.reset(av_frame_alloc()); + sws_input_frame->width = in_width; + sws_input_frame->height = in_height; + sws_input_frame->format = AV_PIX_FMT_BGR0; - sws.reset(sws_getContext( - in_width, in_height, AV_PIX_FMT_BGR0, - out_width, out_height, format, - SWS_LANCZOS | SWS_ACCURATE_RND, - nullptr, nullptr, nullptr)); + sws_output_frame.reset(av_frame_alloc()); + sws_output_frame->width = out_width; + sws_output_frame->height = out_height; + sws_output_frame->format = format; - return sws ? 0 : -1; - } - - ~swdevice_t() override {} - - // Store ownership when frame is hw_frame - frame_t hw_frame; - - frame_t sw_frame; - sws_t sws; - - // offset of input image to output frame in pixels - int offsetUV; - int offsetY; - }; + // Result is always positive + offsetW = (frame->width - out_width) / 2; + offsetH = (frame->height - out_height) / 2; - enum flag_e { - DEFAULT = 0x00, - PARALLEL_ENCODING = 0x01, - H264_ONLY = 0x02, // When HEVC is too heavy - LIMITED_GOP_SIZE = 0x04, // Some encoders don't like it when you have an infinite GOP_SIZE. *cough* VAAPI *cough* - SINGLE_SLICE_ONLY = 0x08, // Never use multiple slices <-- Older intel iGPU's ruin it for everyone else :P - CBR_WITH_VBR = 0x10, // Use a VBR rate control mode to simulate CBR - RELAXED_COMPLIANCE = 0x20, // Use FF_COMPLIANCE_UNOFFICIAL compliance mode - NO_RC_BUF_LIMIT = 0x40, // Don't set rc_buffer_size - }; + sws.reset(sws_alloc_context()); + if (!sws) { + return -1; + } - struct encoder_t { - std::string_view name; - enum flag_e { - PASSED, // Is supported - REF_FRAMES_RESTRICT, // Set maximum reference frames - CBR, // Some encoders don't support CBR, if not supported --> attempt constant quantatication parameter instead - DYNAMIC_RANGE, // hdr - VUI_PARAMETERS, // AMD encoder with VAAPI doesn't add VUI parameters to SPS - MAX_FLAGS - }; + AVDictionary *options { nullptr }; + av_dict_set_int(&options, "srcw", sws_input_frame->width, 0); + av_dict_set_int(&options, "srch", sws_input_frame->height, 0); + av_dict_set_int(&options, "src_format", sws_input_frame->format, 0); + av_dict_set_int(&options, "dstw", sws_output_frame->width, 0); + av_dict_set_int(&options, "dsth", sws_output_frame->height, 0); + av_dict_set_int(&options, "dst_format", sws_output_frame->format, 0); + av_dict_set_int(&options, "sws_flags", SWS_LANCZOS | SWS_ACCURATE_RND, 0); + av_dict_set_int(&options, "threads", config::video.min_threads, 0); + + auto status = av_opt_set_dict(sws.get(), &options); + av_dict_free(&options); + if (status < 0) { + char string[AV_ERROR_MAX_STRING_SIZE]; + BOOST_LOG(error) << "Failed to set SWS options: "sv << av_make_error_string(string, AV_ERROR_MAX_STRING_SIZE, status); + return -1; + } - static std::string_view - from_flag(flag_e flag) { -#define _CONVERT(x) \ - case flag_e::x: \ - return #x##sv - switch (flag) { - _CONVERT(PASSED); - _CONVERT(REF_FRAMES_RESTRICT); - _CONVERT(CBR); - _CONVERT(DYNAMIC_RANGE); - _CONVERT(VUI_PARAMETERS); - _CONVERT(MAX_FLAGS); + status = sws_init_context(sws.get(), nullptr, nullptr); + if (status < 0) { + char string[AV_ERROR_MAX_STRING_SIZE]; + BOOST_LOG(error) << "Failed to initialize SWS: "sv << av_make_error_string(string, AV_ERROR_MAX_STRING_SIZE, status); + return -1; } -#undef _CONVERT - return "unknown"sv; + return 0; } - struct option_t { - KITTY_DEFAULT_CONSTR_MOVE(option_t) - option_t(const option_t &) = default; - - std::string name; - std::variant *, std::string, std::string *> value; - - option_t(std::string &&name, decltype(value) &&value): - name { std::move(name) }, value { std::move(value) } {} - }; - - AVHWDeviceType base_dev_type, derived_dev_type; - AVPixelFormat dev_pix_fmt; - - AVPixelFormat static_pix_fmt, dynamic_pix_fmt; - - struct { - std::vector common_options; - std::vector sdr_options; - std::vector hdr_options; - std::optional qp; - - std::string name; - std::bitset capabilities; - - bool - operator[](flag_e flag) const { - return capabilities[(std::size_t) flag]; - } + // Store ownership when frame is hw_frame + avcodec_frame_t hw_frame; - std::bitset::reference - operator[](flag_e flag) { - return capabilities[(std::size_t) flag]; - } - } hevc, h264; + avcodec_frame_t sw_frame; + avcodec_frame_t sws_input_frame; + avcodec_frame_t sws_output_frame; + sws_t sws; - int flags; + // Offset of input image to output frame in pixels + int offsetW; + int offsetH; + }; - std::function(platf::hwdevice_t *hwdevice)> make_hwdevice_ctx; + enum flag_e : uint32_t { + DEFAULT = 0, + PARALLEL_ENCODING = 1 << 1, // Capture and encoding can run concurrently on separate threads + H264_ONLY = 1 << 2, // When HEVC is too heavy + LIMITED_GOP_SIZE = 1 << 3, // Some encoders don't like it when you have an infinite GOP_SIZE. *cough* VAAPI *cough* + SINGLE_SLICE_ONLY = 1 << 4, // Never use multiple slices <-- Older intel iGPU's ruin it for everyone else :P + CBR_WITH_VBR = 1 << 5, // Use a VBR rate control mode to simulate CBR + RELAXED_COMPLIANCE = 1 << 6, // Use FF_COMPLIANCE_UNOFFICIAL compliance mode + NO_RC_BUF_LIMIT = 1 << 7, // Don't set rc_buffer_size + REF_FRAMES_INVALIDATION = 1 << 8, // Support reference frames invalidation + ALWAYS_REPROBE = 1 << 9, // This is an encoder of last resort and we want to aggressively probe for a better one }; - class session_t { + class avcodec_encode_session_t: public encode_session_t { public: - session_t() = default; - session_t(ctx_t &&ctx, std::shared_ptr &&device, int inject): - ctx { std::move(ctx) }, device { std::move(device) }, inject { inject } {} + avcodec_encode_session_t() = default; + avcodec_encode_session_t(avcodec_ctx_t &&avcodec_ctx, std::unique_ptr encode_device, int inject): + avcodec_ctx { std::move(avcodec_ctx) }, device { std::move(encode_device) }, inject { inject } {} - session_t(session_t &&other) noexcept = default; - ~session_t() { + avcodec_encode_session_t(avcodec_encode_session_t &&other) noexcept = default; + ~avcodec_encode_session_t() { // Order matters here because the context relies on the hwdevice still being valid - ctx.reset(); + avcodec_ctx.reset(); device.reset(); } // Ensure objects are destroyed in the correct order - session_t & - operator=(session_t &&other) { + avcodec_encode_session_t & + operator=(avcodec_encode_session_t &&other) { device = std::move(other.device); - ctx = std::move(other.ctx); + avcodec_ctx = std::move(other.avcodec_ctx); replacements = std::move(other.replacements); sps = std::move(other.sps); vps = std::move(other.vps); @@ -368,8 +303,38 @@ namespace video { return *this; } - ctx_t ctx; - std::shared_ptr device; + int + convert(platf::img_t &img) override { + if (!device) return -1; + return device->convert(img); + } + + void + request_idr_frame() override { + if (device && device->frame) { + auto &frame = device->frame; + frame->pict_type = AV_PICTURE_TYPE_I; + frame->flags |= AV_FRAME_FLAG_KEY; + } + } + + void + request_normal_frame() override { + if (device && device->frame) { + auto &frame = device->frame; + frame->pict_type = AV_PICTURE_TYPE_NONE; + frame->flags &= ~AV_FRAME_FLAG_KEY; + } + } + + void + invalidate_ref_frames(int64_t first_frame, int64_t last_frame) override { + BOOST_LOG(error) << "Encoder doesn't support reference frame invalidation"; + request_idr_frame(); + } + + avcodec_ctx_t avcodec_ctx; + std::unique_ptr device; std::vector replacements; @@ -380,6 +345,51 @@ namespace video { int inject; }; + class nvenc_encode_session_t: public encode_session_t { + public: + nvenc_encode_session_t(std::unique_ptr encode_device): + device(std::move(encode_device)) { + } + + int + convert(platf::img_t &img) override { + if (!device) return -1; + return device->convert(img); + } + + void + request_idr_frame() override { + force_idr = true; + } + + void + request_normal_frame() override { + force_idr = false; + } + + void + invalidate_ref_frames(int64_t first_frame, int64_t last_frame) override { + if (!device || !device->nvenc) return; + + if (!device->nvenc->invalidate_ref_frames(first_frame, last_frame)) { + force_idr = true; + } + } + + nvenc::nvenc_encoded_frame + encode_frame(uint64_t frame_index) { + if (!device || !device->nvenc) return {}; + + auto result = device->nvenc->encode_frame(frame_index, force_idr); + force_idr = false; + return result; + } + + private: + std::unique_ptr device; + bool force_idr = false; + }; + struct sync_session_ctx_t { safe::signal_t *join_event; safe::mail_raw_t::event_t shutdown_event; @@ -395,8 +405,7 @@ namespace video { struct sync_session_t { sync_session_ctx_t *ctx; - - session_t session; + std::unique_ptr session; }; using encode_session_ctx_queue_t = safe::queue_t; @@ -433,25 +442,100 @@ namespace video { auto capture_thread_async = safe::make_shared(start_capture_async, end_capture_async); auto capture_thread_sync = safe::make_shared(start_capture_sync, end_capture_sync); - static encoder_t nvenc { - "nvenc"sv, #ifdef _WIN32 - AV_HWDEVICE_TYPE_D3D11VA, AV_HWDEVICE_TYPE_NONE, - AV_PIX_FMT_D3D11, -#else - AV_HWDEVICE_TYPE_CUDA, AV_HWDEVICE_TYPE_NONE, - AV_PIX_FMT_CUDA, -#endif - AV_PIX_FMT_NV12, AV_PIX_FMT_P010, + encoder_t nvenc { + "nvenc"sv, + std::make_unique( + platf::mem_type_e::dxgi, + platf::pix_fmt_e::nv12, platf::pix_fmt_e::p010), + { + // Common options + {}, + // SDR-specific options + {}, + // HDR-specific options + {}, + // Fallback options + {}, + std::nullopt, // QP rate control fallback + "av1_nvenc"s, + }, + { + // Common options + {}, + // SDR-specific options + {}, + // HDR-specific options + {}, + // Fallback options + {}, + std::nullopt, // QP rate control fallback + "hevc_nvenc"s, + }, + { + // Common options + {}, + // SDR-specific options + {}, + // HDR-specific options + {}, + // Fallback options + {}, + std::nullopt, // QP rate control fallback + "h264_nvenc"s, + }, + PARALLEL_ENCODING | REF_FRAMES_INVALIDATION // flags + }; +#elif !defined(__APPLE__) + encoder_t nvenc { + "nvenc"sv, + std::make_unique( + #ifdef _WIN32 + AV_HWDEVICE_TYPE_D3D11VA, AV_HWDEVICE_TYPE_NONE, + AV_PIX_FMT_D3D11, + #else + AV_HWDEVICE_TYPE_CUDA, AV_HWDEVICE_TYPE_NONE, + AV_PIX_FMT_CUDA, + #endif + AV_PIX_FMT_NV12, AV_PIX_FMT_P010, + #ifdef _WIN32 + dxgi_init_avcodec_hardware_input_buffer + #else + cuda_init_avcodec_hardware_input_buffer + #endif + ), { // Common options { { "delay"s, 0 }, { "forced-idr"s, 1 }, { "zerolatency"s, 1 }, - { "preset"s, &config::video.nv.nv_preset }, - { "tune"s, &config::video.nv.nv_tune }, - { "rc"s, &config::video.nv.nv_rc }, + { "preset"s, &config::video.nv_legacy.preset }, + { "tune"s, NV_ENC_TUNING_INFO_ULTRA_LOW_LATENCY }, + { "rc"s, NV_ENC_PARAMS_RC_CBR }, + { "multipass"s, &config::video.nv_legacy.multipass }, + { "aq"s, &config::video.nv_legacy.aq }, + }, + // SDR-specific options + {}, + // HDR-specific options + {}, + // Fallback options + {}, + std::nullopt, // QP rate control fallback + "av1_nvenc"s, + }, + { + // Common options + { + { "delay"s, 0 }, + { "forced-idr"s, 1 }, + { "zerolatency"s, 1 }, + { "preset"s, &config::video.nv_legacy.preset }, + { "tune"s, NV_ENC_TUNING_INFO_ULTRA_LOW_LATENCY }, + { "rc"s, NV_ENC_PARAMS_RC_CBR }, + { "multipass"s, &config::video.nv_legacy.multipass }, + { "aq"s, &config::video.nv_legacy.aq }, }, // SDR-specific options { @@ -461,7 +545,8 @@ namespace video { { { "profile"s, (int) nv::profile_hevc_e::main_10 }, }, - std::nullopt, + {}, // Fallback options + std::nullopt, // QP rate control fallback "hevc_nvenc"s, }, { @@ -469,35 +554,52 @@ namespace video { { "delay"s, 0 }, { "forced-idr"s, 1 }, { "zerolatency"s, 1 }, - { "preset"s, &config::video.nv.nv_preset }, - { "tune"s, &config::video.nv.nv_tune }, - { "rc"s, &config::video.nv.nv_rc }, - { "coder"s, &config::video.nv.nv_coder }, + { "preset"s, &config::video.nv_legacy.preset }, + { "tune"s, NV_ENC_TUNING_INFO_ULTRA_LOW_LATENCY }, + { "rc"s, NV_ENC_PARAMS_RC_CBR }, + { "coder"s, &config::video.nv_legacy.h264_coder }, + { "multipass"s, &config::video.nv_legacy.multipass }, + { "aq"s, &config::video.nv_legacy.aq }, }, // SDR-specific options { { "profile"s, (int) nv::profile_h264_e::high }, }, {}, // HDR-specific options - std::make_optional({ "qp"s, &config::video.qp }), + {}, // Fallback options + std::nullopt, // QP rate control fallback "h264_nvenc"s, }, - PARALLEL_ENCODING, -#ifdef _WIN32 - dxgi_make_hwdevice_ctx -#else - cuda_make_hwdevice_ctx -#endif + PARALLEL_ENCODING }; +#endif #ifdef _WIN32 - static encoder_t quicksync { + encoder_t quicksync { "quicksync"sv, - AV_HWDEVICE_TYPE_D3D11VA, - AV_HWDEVICE_TYPE_QSV, - AV_PIX_FMT_QSV, - AV_PIX_FMT_NV12, - AV_PIX_FMT_P010, + std::make_unique( + AV_HWDEVICE_TYPE_D3D11VA, AV_HWDEVICE_TYPE_QSV, + AV_PIX_FMT_QSV, + AV_PIX_FMT_NV12, AV_PIX_FMT_P010, + dxgi_init_avcodec_hardware_input_buffer), + { + // Common options + { + { "preset"s, &config::video.qsv.qsv_preset }, + { "forced_idr"s, 1 }, + { "async_depth"s, 1 }, + { "low_delay_brc"s, 1 }, + { "low_power"s, 1 }, + }, + // SDR-specific options + {}, + // HDR-specific options + {}, + // Fallback options + {}, + std::nullopt, // QP rate control fallback + "av1_qsv"s, + }, { // Common options { @@ -517,7 +619,11 @@ namespace video { { { "profile"s, (int) qsv::profile_hevc_e::main_10 }, }, - std::make_optional({ "qp"s, &config::video.qp }), + // Fallback options + { + { "low_power"s, []() { return config::video.qsv.qsv_slow_hevc ? 0 : 1; } }, + }, + std::nullopt, // QP rate control fallback "hevc_qsv"s, }, { @@ -538,66 +644,125 @@ namespace video { { { "profile"s, (int) qsv::profile_h264_e::high }, }, - {}, // HDR-specific options - std::make_optional({ "qp"s, &config::video.qp }), + // HDR-specific options + {}, + // Fallback options + { + { "low_power"s, 0 }, // Some old/low-end Intel GPUs don't support low power encoding + }, + std::nullopt, // QP rate control fallback "h264_qsv"s, }, - PARALLEL_ENCODING | CBR_WITH_VBR | RELAXED_COMPLIANCE | NO_RC_BUF_LIMIT, - dxgi_make_hwdevice_ctx, + PARALLEL_ENCODING | CBR_WITH_VBR | RELAXED_COMPLIANCE | NO_RC_BUF_LIMIT }; - static encoder_t amdvce { + encoder_t amdvce { "amdvce"sv, - AV_HWDEVICE_TYPE_D3D11VA, AV_HWDEVICE_TYPE_NONE, - AV_PIX_FMT_D3D11, - AV_PIX_FMT_NV12, AV_PIX_FMT_P010, + std::make_unique( + AV_HWDEVICE_TYPE_D3D11VA, AV_HWDEVICE_TYPE_NONE, + AV_PIX_FMT_D3D11, + AV_PIX_FMT_NV12, AV_PIX_FMT_P010, + dxgi_init_avcodec_hardware_input_buffer), { // Common options { - { "filler_data"s, true }, + { "filler_data"s, false }, + { "log_to_dbg"s, []() { return config::sunshine.min_log_level < 2 ? 1 : 0; } }, + { "preencode"s, &config::video.amd.amd_preanalysis }, + { "quality"s, &config::video.amd.amd_quality_av1 }, + { "rc"s, &config::video.amd.amd_rc_av1 }, + { "usage"s, &config::video.amd.amd_usage_av1 }, + { "enforce_hrd"s, &config::video.amd.amd_enforce_hrd }, + }, + {}, // SDR-specific options + {}, // HDR-specific options + {}, // Fallback options + std::nullopt, // QP rate control fallback + "av1_amf"s, + }, + { + // Common options + { + { "filler_data"s, false }, + { "log_to_dbg"s, []() { return config::sunshine.min_log_level < 2 ? 1 : 0; } }, { "gops_per_idr"s, 1 }, { "header_insertion_mode"s, "idr"s }, - { "preanalysis"s, &config::video.amd.amd_preanalysis }, + { "preencode"s, &config::video.amd.amd_preanalysis }, { "qmax"s, 51 }, { "qmin"s, 0 }, { "quality"s, &config::video.amd.amd_quality_hevc }, { "rc"s, &config::video.amd.amd_rc_hevc }, { "usage"s, &config::video.amd.amd_usage_hevc }, { "vbaq"s, &config::video.amd.amd_vbaq }, + { "enforce_hrd"s, &config::video.amd.amd_enforce_hrd }, }, {}, // SDR-specific options {}, // HDR-specific options - std::make_optional({ "qp_p"s, &config::video.qp }), + {}, // Fallback options + std::nullopt, // QP rate control fallback "hevc_amf"s, }, { // Common options { - { "filler_data"s, true }, - { "log_to_dbg"s, "1"s }, - { "preanalysis"s, &config::video.amd.amd_preanalysis }, + { "filler_data"s, false }, + { "log_to_dbg"s, []() { return config::sunshine.min_log_level < 2 ? 1 : 0; } }, + { "preencode"s, &config::video.amd.amd_preanalysis }, { "qmax"s, 51 }, { "qmin"s, 0 }, { "quality"s, &config::video.amd.amd_quality_h264 }, { "rc"s, &config::video.amd.amd_rc_h264 }, { "usage"s, &config::video.amd.amd_usage_h264 }, { "vbaq"s, &config::video.amd.amd_vbaq }, + { "enforce_hrd"s, &config::video.amd.amd_enforce_hrd }, }, - {}, // SDR-specific options - {}, // HDR-specific options - std::make_optional({ "qp_p"s, &config::video.qp }), + // SDR-specific options + {}, + // HDR-specific options + {}, + // Fallback options + { + { "usage"s, 2 /* AMF_VIDEO_ENCODER_USAGE_LOW_LATENCY */ }, // Workaround for https://github.com/GPUOpen-LibrariesAndSDKs/AMF/issues/410 + }, + std::nullopt, // QP rate control fallback "h264_amf"s, }, - PARALLEL_ENCODING, - dxgi_make_hwdevice_ctx + PARALLEL_ENCODING }; #endif - static encoder_t software { + encoder_t software { "software"sv, - AV_HWDEVICE_TYPE_NONE, AV_HWDEVICE_TYPE_NONE, - AV_PIX_FMT_NONE, - AV_PIX_FMT_YUV420P, AV_PIX_FMT_YUV420P10, + std::make_unique( + AV_HWDEVICE_TYPE_NONE, AV_HWDEVICE_TYPE_NONE, + AV_PIX_FMT_NONE, + AV_PIX_FMT_YUV420P, AV_PIX_FMT_YUV420P10, + nullptr), + { + // libsvtav1 takes different presets than libx264/libx265. + // We set an infinite GOP length, use a low delay prediction structure, + // force I frames to be key frames, and set max bitrate to default to work + // around a FFmpeg bug with CBR mode. + { + { "svtav1-params"s, "keyint=-1:pred-struct=1:force-key-frames=1:mbr=0"s }, + { "preset"s, &config::video.sw.svtav1_preset }, + }, + {}, // SDR-specific options + {}, // HDR-specific options + {}, // Fallback options + + // QP rate control fallback + std::nullopt, + +#ifdef ENABLE_BROKEN_AV1_ENCODER + // Due to bugs preventing on-demand IDR frames from working and very poor + // real-time encoding performance, we do not enable libsvtav1 by default. + // It is only suitable for testing AV1 until the IDR frame issue is fixed. + "libsvtav1"s, +#else + {}, +#endif + }, { // x265's Info SEI is so long that it causes the IDR picture data to be // kicked to the 2nd packet in the frame, breaking Moonlight's parsing logic. @@ -611,7 +776,8 @@ namespace video { }, {}, // SDR-specific options {}, // HDR-specific options - std::make_optional("qp"s, &config::video.qp), + {}, // Fallback options + std::nullopt, // QP rate control fallback "libx265"s, }, { @@ -622,65 +788,114 @@ namespace video { }, {}, // SDR-specific options {}, // HDR-specific options - std::make_optional("qp"s, &config::video.qp), + {}, // Fallback options + std::nullopt, // QP rate control fallback "libx264"s, }, - H264_ONLY | PARALLEL_ENCODING, - - nullptr + H264_ONLY | PARALLEL_ENCODING | ALWAYS_REPROBE }; #ifdef __linux__ - static encoder_t vaapi { + encoder_t vaapi { "vaapi"sv, - AV_HWDEVICE_TYPE_VAAPI, AV_HWDEVICE_TYPE_NONE, - AV_PIX_FMT_VAAPI, - AV_PIX_FMT_NV12, AV_PIX_FMT_YUV420P10, + std::make_unique( + AV_HWDEVICE_TYPE_VAAPI, AV_HWDEVICE_TYPE_NONE, + AV_PIX_FMT_VAAPI, + AV_PIX_FMT_NV12, AV_PIX_FMT_P010, + vaapi_init_avcodec_hardware_input_buffer), { // Common options { + { "low_power"s, 1 }, + { "async_depth"s, 1 }, + { "idr_interval"s, std::numeric_limits::max() }, + }, + // SDR-specific options + {}, + // HDR-specific options + {}, + // Fallback options + { + { "low_power"s, 0 }, // Not all VAAPI drivers expose LP entrypoints + }, + std::make_optional("qp"s, &config::video.qp), + "av1_vaapi"s, + }, + { + // Common options + { + { "low_power"s, 1 }, { "async_depth"s, 1 }, { "sei"s, 0 }, { "idr_interval"s, std::numeric_limits::max() }, }, - {}, // SDR-specific options - {}, // HDR-specific options + // SDR-specific options + {}, + // HDR-specific options + {}, + // Fallback options + { + { "low_power"s, 0 }, // Not all VAAPI drivers expose LP entrypoints + }, std::make_optional("qp"s, &config::video.qp), "hevc_vaapi"s, }, { // Common options { + { "low_power"s, 1 }, { "async_depth"s, 1 }, { "sei"s, 0 }, { "idr_interval"s, std::numeric_limits::max() }, }, - {}, // SDR-specific options - {}, // HDR-specific options + // SDR-specific options + {}, + // HDR-specific options + {}, + // Fallback options + { + { "low_power"s, 0 }, // Not all VAAPI drivers expose LP entrypoints + }, std::make_optional("qp"s, &config::video.qp), "h264_vaapi"s, }, - LIMITED_GOP_SIZE | PARALLEL_ENCODING | SINGLE_SLICE_ONLY | NO_RC_BUF_LIMIT, - - vaapi_make_hwdevice_ctx + LIMITED_GOP_SIZE | PARALLEL_ENCODING | SINGLE_SLICE_ONLY | NO_RC_BUF_LIMIT }; #endif #ifdef __APPLE__ - static encoder_t videotoolbox { + encoder_t videotoolbox { "videotoolbox"sv, - AV_HWDEVICE_TYPE_NONE, AV_HWDEVICE_TYPE_NONE, - AV_PIX_FMT_VIDEOTOOLBOX, - AV_PIX_FMT_NV12, AV_PIX_FMT_NV12, + std::make_unique( + AV_HWDEVICE_TYPE_VIDEOTOOLBOX, AV_HWDEVICE_TYPE_NONE, + AV_PIX_FMT_VIDEOTOOLBOX, + AV_PIX_FMT_NV12, AV_PIX_FMT_P010, + vt_init_avcodec_hardware_input_buffer), + { + // Common options + { + { "allow_sw"s, &config::video.vt.vt_allow_sw }, + { "require_sw"s, &config::video.vt.vt_require_sw }, + { "realtime"s, &config::video.vt.vt_realtime }, + { "prio_speed"s, 1 }, + }, + {}, // SDR-specific options + {}, // HDR-specific options + {}, // Fallback options + std::nullopt, + "av1_videotoolbox"s, + }, { // Common options { { "allow_sw"s, &config::video.vt.vt_allow_sw }, { "require_sw"s, &config::video.vt.vt_require_sw }, { "realtime"s, &config::video.vt.vt_realtime }, + { "prio_speed"s, 1 }, }, {}, // SDR-specific options {}, // HDR-specific options + {}, // Fallback options std::nullopt, "hevc_videotoolbox"s, }, @@ -690,15 +905,17 @@ namespace video { { "allow_sw"s, &config::video.vt.vt_allow_sw }, { "require_sw"s, &config::video.vt.vt_require_sw }, { "realtime"s, &config::video.vt.vt_realtime }, + { "prio_speed"s, 1 }, }, {}, // SDR-specific options {}, // HDR-specific options + { + { "flags"s, "-low_delay" }, + }, // Fallback options std::nullopt, "h264_videotoolbox"s, }, - DEFAULT, - - nullptr + DEFAULT }; #endif @@ -721,13 +938,15 @@ namespace video { static encoder_t *chosen_encoder; int active_hevc_mode; + int active_av1_mode; + bool last_encoder_probe_supported_ref_frames_invalidation = false; void - reset_display(std::shared_ptr &disp, AVHWDeviceType type, const std::string &display_name, const config_t &config) { + reset_display(std::shared_ptr &disp, const platf::mem_type_e &type, const std::string &display_name, const config_t &config) { // We try this twice, in case we still get an error on reinitialization for (int x = 0; x < 2; ++x) { disp.reset(); - disp = platf::display(map_base_dev_type(type), display_name, config); + disp = platf::display(type, display_name, config); if (disp) { break; } @@ -737,6 +956,61 @@ namespace video { } } + /** + * @brief Updates the list of display names before or during a stream. + * @details This will attempt to keep `current_display_index` pointing at the same display. + * @param dev_type The encoder device type used for display lookup. + * @param display_names The list of display names to repopulate. + * @param current_display_index The current display index or -1 if not yet known. + */ + void + refresh_displays(platf::mem_type_e dev_type, std::vector &display_names, int ¤t_display_index) { + std::string current_display_name; + + // If we have a current display index, let's start with that + if (current_display_index >= 0 && current_display_index < display_names.size()) { + current_display_name = display_names.at(current_display_index); + } + + // Refresh the display names + auto old_display_names = std::move(display_names); + display_names = platf::display_names(dev_type); + + // If we now have no displays, let's put the old display array back and fail + if (display_names.empty() && !old_display_names.empty()) { + BOOST_LOG(error) << "No displays were found after reenumeration!"sv; + display_names = std::move(old_display_names); + return; + } + else if (display_names.empty()) { + display_names.emplace_back(config::video.output_name); + } + + // We now have a new display name list, so reset the index back to 0 + current_display_index = 0; + + // If we had a name previously, let's try to find it in the new list + if (!current_display_name.empty()) { + for (int x = 0; x < display_names.size(); ++x) { + if (display_names[x] == current_display_name) { + current_display_index = x; + return; + } + } + + // The old display was removed, so we'll start back at the first display again + BOOST_LOG(warning) << "Previous active display ["sv << current_display_name << "] is no longer present"sv; + } + else { + for (int x = 0; x < display_names.size(); ++x) { + if (display_names[x] == config::video.output_name) { + current_display_index = x; + return; + } + } + } + } + void captureThread( std::shared_ptr> capture_ctx_queue, @@ -759,23 +1033,6 @@ namespace video { auto switch_display_event = mail::man->event(mail::switch_display); - // Get all the monitor names now, rather than at boot, to - // get the most up-to-date list available monitors - auto display_names = platf::display_names(map_base_dev_type(encoder.base_dev_type)); - int display_p = 0; - - if (display_names.empty()) { - display_names.emplace_back(config::video.output_name); - } - - for (int x = 0; x < display_names.size(); ++x) { - if (display_names[x] == config::video.output_name) { - display_p = x; - - break; - } - } - // Wait for the initial capture context or a request to stop the queue auto initial_capture_ctx = capture_ctx_queue->pop(); if (!initial_capture_ctx) { @@ -783,7 +1040,12 @@ namespace video { } capture_ctxs.emplace_back(std::move(*initial_capture_ctx)); - auto disp = platf::display(map_base_dev_type(encoder.base_dev_type), display_names[display_p], capture_ctxs.front().config); + // Get all the monitor names now, rather than at boot, to + // get the most up-to-date list available monitors + std::vector display_names; + int display_p = -1; + refresh_displays(encoder.platform_formats->dev_type, display_names, display_p); + auto disp = platf::display(encoder.platform_formats->dev_type, display_names[display_p], capture_ctxs.front().config); if (!disp) { return; } @@ -916,8 +1178,6 @@ namespace video { if (switch_display_event->peek()) { artificial_reinit = true; - - display_p = std::clamp(*switch_display_event->pop(), 0, (int) display_names.size() - 1); return false; } @@ -966,8 +1226,20 @@ namespace video { } while (capture_ctx_queue->running()) { + // Release the display before reenumerating displays, since some capture backends + // only support a single display session per device/application. + disp.reset(); + + // Refresh display names since a display removal might have caused the reinitialization + refresh_displays(encoder.platform_formats->dev_type, display_names, display_p); + + // Process any pending display switch with the new list of displays + if (switch_display_event->peek()) { + display_p = std::clamp(*switch_display_event->pop(), 0, (int) display_names.size() - 1); + } + // reset_display() will sleep between retries - reset_display(disp, encoder.base_dev_type, display_names[display_p], capture_ctxs.front().config); + reset_display(disp, encoder.platform_formats->dev_type, display_names[display_p], capture_ctxs.front().config); if (disp) { break; } @@ -994,10 +1266,11 @@ namespace video { } int - encode(int64_t frame_nr, session_t &session, frame_t::pointer frame, safe::mail_raw_t::queue_t &packets, void *channel_data, const std::optional &frame_timestamp) { + encode_avcodec(int64_t frame_nr, avcodec_encode_session_t &session, safe::mail_raw_t::queue_t &packets, void *channel_data, std::optional frame_timestamp) { + auto &frame = session.device->frame; frame->pts = frame_nr; - auto &ctx = session.ctx; + auto &ctx = session.avcodec_ctx; auto &sps = session.sps; auto &vps = session.vps; @@ -1012,7 +1285,7 @@ namespace video { } while (ret >= 0) { - auto packet = std::make_unique(nullptr); + auto packet = std::make_unique(); auto av_packet = packet.get()->av_packet; ret = avcodec_receive_packet(ctx.get(), av_packet); @@ -1023,6 +1296,14 @@ namespace video { return ret; } + if (av_packet->flags & AV_PKT_FLAG_KEY) { + BOOST_LOG(debug) << "Frame "sv << frame_nr << ": IDR Keyframe (AV_FRAME_FLAG_KEY)"sv; + } + + if ((frame->flags & AV_FRAME_FLAG_KEY) && !(av_packet->flags & AV_PKT_FLAG_KEY)) { + BOOST_LOG(error) << "Encoder did not produce IDR frame when requested!"sv; + } + if (session.inject) { if (session.inject == 1) { auto h264 = cbs::make_sps_h264(ctx.get(), av_packet); @@ -1051,260 +1332,320 @@ namespace video { packet->frame_timestamp = frame_timestamp; } - packet->replacements = &session.replacements; - packet->channel_data = channel_data; - packets->raise(std::move(packet)); + packet->replacements = &session.replacements; + packet->channel_data = channel_data; + packets->raise(std::move(packet)); + } + + return 0; + } + + int + encode_nvenc(int64_t frame_nr, nvenc_encode_session_t &session, safe::mail_raw_t::queue_t &packets, void *channel_data, std::optional frame_timestamp) { + auto encoded_frame = session.encode_frame(frame_nr); + if (encoded_frame.data.empty()) { + BOOST_LOG(error) << "NvENC returned empty packet"; + return -1; + } + + if (frame_nr != encoded_frame.frame_index) { + BOOST_LOG(error) << "NvENC frame index mismatch " << frame_nr << " " << encoded_frame.frame_index; + } + + auto packet = std::make_unique(std::move(encoded_frame.data), encoded_frame.frame_index, encoded_frame.idr); + packet->channel_data = channel_data; + packet->after_ref_frame_invalidation = encoded_frame.after_ref_frame_invalidation; + packet->frame_timestamp = frame_timestamp; + packets->raise(std::move(packet)); + + return 0; + } + + int + encode(int64_t frame_nr, encode_session_t &session, safe::mail_raw_t::queue_t &packets, void *channel_data, std::optional frame_timestamp) { + if (auto avcodec_session = dynamic_cast(&session)) { + return encode_avcodec(frame_nr, *avcodec_session, packets, channel_data, frame_timestamp); + } + else if (auto nvenc_session = dynamic_cast(&session)) { + return encode_nvenc(frame_nr, *nvenc_session, packets, channel_data, frame_timestamp); } - return 0; + return -1; } - std::optional - make_session(platf::display_t *disp, const encoder_t &encoder, const config_t &config, int width, int height, std::shared_ptr &&hwdevice) { - bool hardware = encoder.base_dev_type != AV_HWDEVICE_TYPE_NONE; + std::unique_ptr + make_avcodec_encode_session(platf::display_t *disp, const encoder_t &encoder, const config_t &config, int width, int height, std::unique_ptr encode_device) { + auto platform_formats = dynamic_cast(encoder.platform_formats.get()); + if (!platform_formats) { + return nullptr; + } + + bool hardware = platform_formats->avcodec_base_dev_type != AV_HWDEVICE_TYPE_NONE; - auto &video_format = config.videoFormat == 0 ? encoder.h264 : encoder.hevc; - if (!video_format[encoder_t::PASSED]) { + auto &video_format = config.videoFormat == 0 ? encoder.h264 : + config.videoFormat == 1 ? encoder.hevc : + encoder.av1; + if (!video_format[encoder_t::PASSED] || !disp->is_codec_supported(video_format.name, config)) { BOOST_LOG(error) << encoder.name << ": "sv << video_format.name << " mode not supported"sv; - return std::nullopt; + return nullptr; } if (config.dynamicRange && !video_format[encoder_t::DYNAMIC_RANGE]) { BOOST_LOG(error) << video_format.name << ": dynamic range not supported"sv; - return std::nullopt; + return nullptr; } auto codec = avcodec_find_encoder_by_name(video_format.name.c_str()); if (!codec) { BOOST_LOG(error) << "Couldn't open ["sv << video_format.name << ']'; - return std::nullopt; - } - - ctx_t ctx { avcodec_alloc_context3(codec) }; - ctx->width = config.width; - ctx->height = config.height; - ctx->time_base = AVRational { 1, config.framerate }; - ctx->framerate = AVRational { config.framerate, 1 }; - - if (config.videoFormat == 0) { - ctx->profile = FF_PROFILE_H264_HIGH; - } - else if (config.dynamicRange == 0) { - ctx->profile = FF_PROFILE_HEVC_MAIN; - } - else { - ctx->profile = FF_PROFILE_HEVC_MAIN_10; - } - - // B-frames delay decoder output, so never use them - ctx->max_b_frames = 0; - - // Use an infinite GOP length since I-frames are generated on demand - ctx->gop_size = encoder.flags & LIMITED_GOP_SIZE ? - std::numeric_limits::max() : - std::numeric_limits::max(); - - ctx->keyint_min = std::numeric_limits::max(); - - // Some client decoders have limits on the number of reference frames - if (config.numRefFrames) { - if (video_format[encoder_t::REF_FRAMES_RESTRICT]) { - ctx->refs = config.numRefFrames; - } - else { - BOOST_LOG(warning) << "Client requested reference frame limit, but encoder doesn't support it!"sv; - } + return nullptr; } - ctx->flags |= (AV_CODEC_FLAG_CLOSED_GOP | AV_CODEC_FLAG_LOW_DELAY); - ctx->flags2 |= AV_CODEC_FLAG2_FAST; + auto colorspace = encode_device->colorspace; + auto sw_fmt = (colorspace.bit_depth == 10) ? platform_formats->avcodec_pix_fmt_10bit : platform_formats->avcodec_pix_fmt_8bit; - ctx->color_range = (config.encoderCscMode & 0x1) ? AVCOL_RANGE_JPEG : AVCOL_RANGE_MPEG; + // Allow up to 1 retry to apply the set of fallback options. + // + // Note: If we later end up needing multiple sets of + // fallback options, we may need to allow more retries + // to try applying each set. + avcodec_ctx_t ctx; + for (int retries = 0; retries < 2; retries++) { + ctx.reset(avcodec_alloc_context3(codec)); + ctx->width = config.width; + ctx->height = config.height; + ctx->time_base = AVRational { 1, config.framerate }; + ctx->framerate = AVRational { config.framerate, 1 }; - int sws_color_space; - if (config.dynamicRange && disp->is_hdr()) { - // When HDR is active, that overrides the colorspace the client requested - BOOST_LOG(info) << "HDR color coding [Rec. 2020 + SMPTE 2084 PQ]"sv; - ctx->color_primaries = AVCOL_PRI_BT2020; - ctx->color_trc = AVCOL_TRC_SMPTE2084; - ctx->colorspace = AVCOL_SPC_BT2020_NCL; - sws_color_space = SWS_CS_BT2020; - } - else { - switch (config.encoderCscMode >> 1) { + switch (config.videoFormat) { case 0: - default: - // Rec. 601 - BOOST_LOG(info) << "SDR color coding [Rec. 601]"sv; - ctx->color_primaries = AVCOL_PRI_SMPTE170M; - ctx->color_trc = AVCOL_TRC_SMPTE170M; - ctx->colorspace = AVCOL_SPC_SMPTE170M; - sws_color_space = SWS_CS_SMPTE170M; + ctx->profile = FF_PROFILE_H264_HIGH; break; case 1: - // Rec. 709 - BOOST_LOG(info) << "SDR color coding [Rec. 709]"sv; - ctx->color_primaries = AVCOL_PRI_BT709; - ctx->color_trc = AVCOL_TRC_BT709; - ctx->colorspace = AVCOL_SPC_BT709; - sws_color_space = SWS_CS_ITU709; + ctx->profile = config.dynamicRange ? FF_PROFILE_HEVC_MAIN_10 : FF_PROFILE_HEVC_MAIN; break; case 2: - // Rec. 2020 - BOOST_LOG(info) << "SDR color coding [Rec. 2020]"sv; - ctx->color_primaries = AVCOL_PRI_BT2020; - ctx->color_trc = AVCOL_TRC_BT2020_10; - ctx->colorspace = AVCOL_SPC_BT2020_NCL; - sws_color_space = SWS_CS_BT2020; + // AV1 supports both 8 and 10 bit encoding with the same Main profile + ctx->profile = FF_PROFILE_AV1_MAIN; break; } - } - BOOST_LOG(info) << "Color range: ["sv << ((config.encoderCscMode & 0x1) ? "JPEG"sv : "MPEG"sv) << ']'; + // B-frames delay decoder output, so never use them + ctx->max_b_frames = 0; - AVPixelFormat sw_fmt; - if (config.dynamicRange == 0) { - sw_fmt = encoder.static_pix_fmt; - } - else { - sw_fmt = encoder.dynamic_pix_fmt; - } + // Use an infinite GOP length since I-frames are generated on demand + ctx->gop_size = encoder.flags & LIMITED_GOP_SIZE ? + std::numeric_limits::max() : + std::numeric_limits::max(); - // Used by cbs::make_sps_hevc - ctx->sw_pix_fmt = sw_fmt; + ctx->keyint_min = std::numeric_limits::max(); - if (hardware) { - buffer_t hwdevice_ctx; + // Some client decoders have limits on the number of reference frames + if (config.numRefFrames) { + if (video_format[encoder_t::REF_FRAMES_RESTRICT]) { + ctx->refs = config.numRefFrames; + } + else { + BOOST_LOG(warning) << "Client requested reference frame limit, but encoder doesn't support it!"sv; + } + } - ctx->pix_fmt = encoder.dev_pix_fmt; + // We forcefully reset the flags to avoid clash on reuse of AVCodecContext + ctx->flags = 0; + ctx->flags |= AV_CODEC_FLAG_CLOSED_GOP | AV_CODEC_FLAG_LOW_DELAY; - // Create the base hwdevice context - auto buf_or_error = encoder.make_hwdevice_ctx(hwdevice.get()); - if (buf_or_error.has_right()) { - return std::nullopt; - } - hwdevice_ctx = std::move(buf_or_error.left()); + ctx->flags2 |= AV_CODEC_FLAG2_FAST; + + auto avcodec_colorspace = avcodec_colorspace_from_sunshine_colorspace(colorspace); + + ctx->color_range = avcodec_colorspace.range; + ctx->color_primaries = avcodec_colorspace.primaries; + ctx->color_trc = avcodec_colorspace.transfer_function; + ctx->colorspace = avcodec_colorspace.matrix; + + // Used by cbs::make_sps_hevc + ctx->sw_pix_fmt = sw_fmt; + + if (hardware) { + avcodec_buffer_t encoding_stream_context; + + ctx->pix_fmt = platform_formats->avcodec_dev_pix_fmt; + + // Create the base hwdevice context + auto buf_or_error = platform_formats->init_avcodec_hardware_input_buffer(encode_device.get()); + if (buf_or_error.has_right()) { + return nullptr; + } + encoding_stream_context = std::move(buf_or_error.left()); - // If this encoder requires derivation from the base, derive the desired type - if (encoder.derived_dev_type != AV_HWDEVICE_TYPE_NONE) { - buffer_t derived_hwdevice_ctx; + // If this encoder requires derivation from the base, derive the desired type + if (platform_formats->avcodec_derived_dev_type != AV_HWDEVICE_TYPE_NONE) { + avcodec_buffer_t derived_context; - // Allow the hwdevice to prepare for this type of context to be derived - if (hwdevice->prepare_to_derive_context(encoder.derived_dev_type)) { - return std::nullopt; + // Allow the hwdevice to prepare for this type of context to be derived + if (encode_device->prepare_to_derive_context(platform_formats->avcodec_derived_dev_type)) { + return nullptr; + } + + auto err = av_hwdevice_ctx_create_derived(&derived_context, platform_formats->avcodec_derived_dev_type, encoding_stream_context.get(), 0); + if (err) { + char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 }; + BOOST_LOG(error) << "Failed to derive device context: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err); + + return nullptr; + } + + encoding_stream_context = std::move(derived_context); } - auto err = av_hwdevice_ctx_create_derived(&derived_hwdevice_ctx, encoder.derived_dev_type, hwdevice_ctx.get(), 0); - if (err) { - char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 }; - BOOST_LOG(error) << "Failed to derive device context: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err); + // Initialize avcodec hardware frames + { + avcodec_buffer_t frame_ref { av_hwframe_ctx_alloc(encoding_stream_context.get()) }; + + auto frame_ctx = (AVHWFramesContext *) frame_ref->data; + frame_ctx->format = ctx->pix_fmt; + frame_ctx->sw_format = sw_fmt; + frame_ctx->height = ctx->height; + frame_ctx->width = ctx->width; + frame_ctx->initial_pool_size = 0; + + // Allow the hwdevice to modify hwframe context parameters + encode_device->init_hwframes(frame_ctx); + + if (auto err = av_hwframe_ctx_init(frame_ref.get()); err < 0) { + return nullptr; + } - return std::nullopt; + ctx->hw_frames_ctx = av_buffer_ref(frame_ref.get()); } - hwdevice_ctx = std::move(derived_hwdevice_ctx); + ctx->slices = config.slicesPerFrame; } + else /* software */ { + ctx->pix_fmt = sw_fmt; - if (hwframe_ctx(ctx, hwdevice.get(), hwdevice_ctx, sw_fmt)) { - return std::nullopt; + // Clients will request for the fewest slices per frame to get the + // most efficient encode, but we may want to provide more slices than + // requested to ensure we have enough parallelism for good performance. + ctx->slices = std::max(config.slicesPerFrame, config::video.min_threads); } - ctx->slices = config.slicesPerFrame; - } - else /* software */ { - ctx->pix_fmt = sw_fmt; + if (encoder.flags & SINGLE_SLICE_ONLY) { + ctx->slices = 1; + } - // Clients will request for the fewest slices per frame to get the - // most efficient encode, but we may want to provide more slices than - // requested to ensure we have enough parallelism for good performance. - ctx->slices = std::max(config.slicesPerFrame, config::video.min_threads); - } + ctx->thread_type = FF_THREAD_SLICE; + ctx->thread_count = ctx->slices; - if (encoder.flags & SINGLE_SLICE_ONLY) { - ctx->slices = 1; - } + AVDictionary *options { nullptr }; + auto handle_option = [&options](const encoder_t::option_t &option) { + std::visit( + util::overloaded { + [&](int v) { av_dict_set_int(&options, option.name.c_str(), v, 0); }, + [&](int *v) { av_dict_set_int(&options, option.name.c_str(), *v, 0); }, + [&](std::optional *v) { if(*v) av_dict_set_int(&options, option.name.c_str(), **v, 0); }, + [&](std::function v) { av_dict_set_int(&options, option.name.c_str(), v(), 0); }, + [&](const std::string &v) { av_dict_set(&options, option.name.c_str(), v.c_str(), 0); }, + [&](std::string *v) { if(!v->empty()) av_dict_set(&options, option.name.c_str(), v->c_str(), 0); } }, + option.value); + }; + + // Apply common options, then format-specific overrides + for (auto &option : video_format.common_options) { + handle_option(option); + } + for (auto &option : (config.dynamicRange ? video_format.hdr_options : video_format.sdr_options)) { + handle_option(option); + } + if (retries > 0) { + for (auto &option : video_format.fallback_options) { + handle_option(option); + } + } - ctx->thread_type = FF_THREAD_SLICE; - ctx->thread_count = ctx->slices; + if (video_format[encoder_t::CBR]) { + auto bitrate = config.bitrate * 1000; + ctx->rc_max_rate = bitrate; + ctx->bit_rate = bitrate; - AVDictionary *options { nullptr }; - auto handle_option = [&options](const encoder_t::option_t &option) { - std::visit( - util::overloaded { - [&](int v) { av_dict_set_int(&options, option.name.c_str(), v, 0); }, - [&](int *v) { av_dict_set_int(&options, option.name.c_str(), *v, 0); }, - [&](std::optional *v) { if(*v) av_dict_set_int(&options, option.name.c_str(), **v, 0); }, - [&](const std::string &v) { av_dict_set(&options, option.name.c_str(), v.c_str(), 0); }, - [&](std::string *v) { if(!v->empty()) av_dict_set(&options, option.name.c_str(), v->c_str(), 0); } }, - option.value); - }; + if (encoder.flags & CBR_WITH_VBR) { + // Ensure rc_max_bitrate != bit_rate to force VBR mode + ctx->bit_rate--; + } + else { + ctx->rc_min_rate = bitrate; + } - // Apply common options, then format-specific overrides - for (auto &option : video_format.common_options) { - handle_option(option); - } - for (auto &option : (config.dynamicRange ? video_format.hdr_options : video_format.sdr_options)) { - handle_option(option); - } + if (encoder.flags & RELAXED_COMPLIANCE) { + ctx->strict_std_compliance = FF_COMPLIANCE_UNOFFICIAL; + } - if (video_format[encoder_t::CBR]) { - auto bitrate = config.bitrate * 1000; - ctx->rc_max_rate = bitrate; - ctx->bit_rate = bitrate; + if (!(encoder.flags & NO_RC_BUF_LIMIT)) { + if (!hardware && (ctx->slices > 1 || config.videoFormat == 1)) { + // Use a larger rc_buffer_size for software encoding when slices are enabled, + // because libx264 can severely degrade quality if the buffer is too small. + // libx265 encounters this issue more frequently, so always scale the + // buffer by 1.5x for software HEVC encoding. + ctx->rc_buffer_size = bitrate / ((config.framerate * 10) / 15); + } + else { + ctx->rc_buffer_size = bitrate / config.framerate; - if (encoder.flags & CBR_WITH_VBR) { - // Ensure rc_max_bitrate != bit_rate to force VBR mode - ctx->bit_rate--; +#ifndef __APPLE__ + if (encoder.name == "nvenc" && config::video.nv_legacy.vbv_percentage_increase > 0) { + ctx->rc_buffer_size += ctx->rc_buffer_size * config::video.nv_legacy.vbv_percentage_increase / 100; + } +#endif + } + } + } + else if (video_format.qp) { + handle_option(*video_format.qp); } else { - ctx->rc_min_rate = bitrate; + BOOST_LOG(error) << "Couldn't set video quality: encoder "sv << encoder.name << " doesn't support qp"sv; + return nullptr; } - if (encoder.flags & RELAXED_COMPLIANCE) { - ctx->strict_std_compliance = FF_COMPLIANCE_UNOFFICIAL; - } + if (auto status = avcodec_open2(ctx.get(), codec, &options)) { + char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 }; - if (!(encoder.flags & NO_RC_BUF_LIMIT)) { - if (!hardware && (ctx->slices > 1 || config.videoFormat != 0)) { - // Use a larger rc_buffer_size for software encoding when slices are enabled, - // because libx264 can severely degrade quality if the buffer is too small. - // libx265 encounters this issue more frequently, so always scale the - // buffer by 1.5x for software HEVC encoding. - ctx->rc_buffer_size = bitrate / ((config.framerate * 10) / 15); + if (!video_format.fallback_options.empty() && retries == 0) { + BOOST_LOG(info) + << "Retrying with fallback configuration options for ["sv << video_format.name << "] after error: "sv + << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, status); + + continue; } else { - ctx->rc_buffer_size = bitrate / config.framerate; + BOOST_LOG(error) + << "Could not open codec ["sv + << video_format.name << "]: "sv + << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, status); + + return nullptr; } } - } - else if (video_format.qp) { - handle_option(*video_format.qp); - } - else { - BOOST_LOG(error) << "Couldn't set video quality: encoder "sv << encoder.name << " doesn't support qp"sv; - return std::nullopt; - } - - if (auto status = avcodec_open2(ctx.get(), codec, &options)) { - char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 }; - BOOST_LOG(error) - << "Could not open codec ["sv - << video_format.name << "]: "sv - << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, status); - return std::nullopt; + // Successfully opened the codec + break; } - frame_t frame { av_frame_alloc() }; + avcodec_frame_t frame { av_frame_alloc() }; frame->format = ctx->pix_fmt; frame->width = ctx->width; frame->height = ctx->height; + frame->color_range = ctx->color_range; + frame->color_primaries = ctx->color_primaries; + frame->color_trc = ctx->color_trc; + frame->colorspace = ctx->colorspace; + frame->chroma_location = ctx->chroma_sample_location; // Attach HDR metadata to the AVFrame - if (config.dynamicRange && disp->is_hdr()) { + if (colorspace_is_hdr(colorspace)) { SS_HDR_METADATA hdr_metadata; if (disp->get_hdr_metadata(hdr_metadata)) { auto mdm = av_mastering_display_metadata_create_side_data(frame.get()); @@ -1332,38 +1673,86 @@ namespace video { clm->MaxFALL = hdr_metadata.maxFrameAverageLightLevel; } } + else { + BOOST_LOG(error) << "Couldn't get display hdr metadata when colorspace selection indicates it should have one"; + } } - std::shared_ptr device; + std::unique_ptr encode_device_final; - if (!hwdevice->data) { - auto device_tmp = std::make_unique(); + if (!encode_device->data) { + auto software_encode_device = std::make_unique(); - if (device_tmp->init(width, height, frame.get(), sw_fmt, hardware)) { - return std::nullopt; + if (software_encode_device->init(width, height, frame.get(), sw_fmt, hardware)) { + return nullptr; } + software_encode_device->colorspace = colorspace; - device = std::move(device_tmp); + encode_device_final = std::move(software_encode_device); } else { - device = std::move(hwdevice); + encode_device_final = std::move(encode_device); } - if (device->set_frame(frame.release(), ctx->hw_frames_ctx)) { - return std::nullopt; + if (encode_device_final->set_frame(frame.release(), ctx->hw_frames_ctx)) { + return nullptr; } - device->set_colorspace(sws_color_space, ctx->color_range); + encode_device_final->apply_colorspace(); - session_t session { + auto session = std::make_unique( std::move(ctx), - std::move(device), + std::move(encode_device_final), // 0 ==> don't inject, 1 ==> inject for h264, 2 ==> inject for hevc - (1 - (int) video_format[encoder_t::VUI_PARAMETERS]) * (1 + config.videoFormat), - }; + config.videoFormat <= 1 ? (1 - (int) video_format[encoder_t::VUI_PARAMETERS]) * (1 + config.videoFormat) : 0); + + return session; + } + + std::unique_ptr + make_nvenc_encode_session(const config_t &client_config, std::unique_ptr encode_device) { + if (!encode_device->init_encoder(client_config, encode_device->colorspace)) { + return nullptr; + } + + return std::make_unique(std::move(encode_device)); + } + + std::unique_ptr + make_encode_session(platf::display_t *disp, const encoder_t &encoder, const config_t &config, int width, int height, std::unique_ptr encode_device) { + if (encode_device) { + switch (encode_device->colorspace.colorspace) { + case colorspace_e::bt2020: + BOOST_LOG(info) << "HDR color coding [Rec. 2020 + SMPTE 2084 PQ]"sv; + break; + + case colorspace_e::rec601: + BOOST_LOG(info) << "SDR color coding [Rec. 601]"sv; + break; + + case colorspace_e::rec709: + BOOST_LOG(info) << "SDR color coding [Rec. 709]"sv; + break; + + case colorspace_e::bt2020sdr: + BOOST_LOG(info) << "SDR color coding [Rec. 2020]"sv; + break; + } + BOOST_LOG(info) << "Color depth: " << encode_device->colorspace.bit_depth << "-bit"; + BOOST_LOG(info) << "Color range: ["sv << (encode_device->colorspace.full_range ? "JPEG"sv : "MPEG"sv) << ']'; + } - return std::make_optional(std::move(session)); + if (dynamic_cast(encode_device.get())) { + auto avcodec_encode_device = boost::dynamic_pointer_cast(std::move(encode_device)); + return make_avcodec_encode_session(disp, encoder, config, width, height, std::move(avcodec_encode_device)); + } + else if (dynamic_cast(encode_device.get())) { + auto nvenc_encode_device = boost::dynamic_pointer_cast(std::move(encode_device)); + return make_nvenc_encode_session(config, std::move(nvenc_encode_device)); + } + + return nullptr; } void @@ -1373,20 +1762,19 @@ namespace video { img_event_t images, config_t config, std::shared_ptr disp, - std::shared_ptr &&hwdevice, + std::unique_ptr encode_device, safe::signal_t &reinit_event, const encoder_t &encoder, void *channel_data) { - auto session = make_session(disp.get(), encoder, config, disp->width, disp->height, std::move(hwdevice)); + auto session = make_encode_session(disp.get(), encoder, config, disp->width, disp->height, std::move(encode_device)); if (!session) { return; } - auto frame = session->device->frame; - auto shutdown_event = mail->event(mail::shutdown); auto packets = mail::man->queue(mail::video_packets); auto idr_events = mail->event(mail::idr); + auto invalidate_ref_frames_events = mail->event>(mail::invalidate_ref_frames); { // Load a dummy image into the AVFrame to ensure we have something to encode @@ -1394,7 +1782,7 @@ namespace video { // allocation which can be freed immediately after convert(), so we do this // in a separate scope. auto dummy_img = disp->alloc_img(); - if (!dummy_img || disp->dummy_img(dummy_img.get()) || session->device->convert(*dummy_img)) { + if (!dummy_img || disp->dummy_img(dummy_img.get()) || session->convert(*dummy_img)) { return; } } @@ -1404,20 +1792,30 @@ namespace video { break; } - if (idr_events->peek()) { - frame->pict_type = AV_PICTURE_TYPE_I; - frame->key_frame = 1; + bool requested_idr_frame = false; + + while (invalidate_ref_frames_events->peek()) { + if (auto frames = invalidate_ref_frames_events->pop(0ms)) { + session->invalidate_ref_frames(frames->first, frames->second); + } + } + if (idr_events->peek()) { + requested_idr_frame = true; idr_events->pop(); } + if (requested_idr_frame) { + session->request_idr_frame(); + } + std::optional frame_timestamp; // Encode at a minimum of 10 FPS to avoid image quality issues with static content - if (!frame->key_frame || images->peek()) { + if (!requested_idr_frame || images->peek()) { if (auto img = images->pop(100ms)) { frame_timestamp = img->frame_timestamp; - if (session->device->convert(*img)) { + if (session->convert(*img)) { BOOST_LOG(error) << "Could not convert image"sv; return; } @@ -1427,13 +1825,12 @@ namespace video { } } - if (encode(frame_nr++, *session, frame, packets, channel_data, frame_timestamp)) { + if (encode(frame_nr++, *session, packets, channel_data, frame_timestamp)) { BOOST_LOG(error) << "Could not encode video packet"sv; return; } - frame->pict_type = AV_PICTURE_TYPE_NONE; - frame->key_frame = 0; + session->request_normal_frame(); } } @@ -1468,15 +1865,35 @@ namespace video { }; } + std::unique_ptr + make_encode_device(platf::display_t &disp, const encoder_t &encoder, const config_t &config) { + std::unique_ptr result; + + auto colorspace = colorspace_from_client_config(config, disp.is_hdr()); + auto pix_fmt = (colorspace.bit_depth == 10) ? encoder.platform_formats->pix_fmt_10bit : encoder.platform_formats->pix_fmt_8bit; + + if (dynamic_cast(encoder.platform_formats.get())) { + result = disp.make_avcodec_encode_device(pix_fmt); + } + else if (dynamic_cast(encoder.platform_formats.get())) { + result = disp.make_nvenc_encode_device(pix_fmt); + } + + if (result) { + result->colorspace = colorspace; + } + + return result; + } + std::optional make_synced_session(platf::display_t *disp, const encoder_t &encoder, platf::img_t &img, sync_session_ctx_t &ctx) { sync_session_t encode_session; encode_session.ctx = &ctx; - auto pix_fmt = ctx.config.dynamicRange == 0 ? map_pix_fmt(encoder.static_pix_fmt) : map_pix_fmt(encoder.dynamic_pix_fmt); - auto hwdevice = disp->make_hwdevice(pix_fmt); - if (!hwdevice) { + auto encode_device = make_encode_device(*disp, encoder, ctx.config); + if (!encode_device) { return std::nullopt; } @@ -1485,18 +1902,28 @@ namespace video { // Update client with our current HDR display state hdr_info_t hdr_info = std::make_unique(false); - if (ctx.config.dynamicRange && disp->is_hdr()) { - disp->get_hdr_metadata(hdr_info->metadata); - hdr_info->enabled = true; + if (colorspace_is_hdr(encode_device->colorspace)) { + if (disp->get_hdr_metadata(hdr_info->metadata)) { + hdr_info->enabled = true; + } + else { + BOOST_LOG(error) << "Couldn't get display hdr metadata when colorspace selection indicates it should have one"; + } } ctx.hdr_events->raise(std::move(hdr_info)); - auto session = make_session(disp, encoder, ctx.config, img.width, img.height, std::move(hwdevice)); + auto session = make_encode_session(disp, encoder, ctx.config, img.width, img.height, std::move(encode_device)); if (!session) { return std::nullopt; } - encode_session.session = std::move(*session); + // Load the initial image to prepare for encoding + if (session->convert(img)) { + BOOST_LOG(error) << "Could not convert initial image"sv; + return std::nullopt; + } + + encode_session.session = std::move(session); return encode_session; } @@ -1504,22 +1931,10 @@ namespace video { encode_e encode_run_sync( std::vector> &synced_session_ctxs, - encode_session_ctx_queue_t &encode_session_ctx_queue) { + encode_session_ctx_queue_t &encode_session_ctx_queue, + std::vector &display_names, + int &display_p) { const auto &encoder = *chosen_encoder; - auto display_names = platf::display_names(map_base_dev_type(encoder.base_dev_type)); - int display_p = 0; - - if (display_names.empty()) { - display_names.emplace_back(config::video.output_name); - } - - for (int x = 0; x < display_names.size(); ++x) { - if (display_names[x] == config::video.output_name) { - display_p = x; - - break; - } - } std::shared_ptr disp; @@ -1535,8 +1950,16 @@ namespace video { } while (encode_session_ctx_queue.running()) { + // Refresh display names since a display removal might have caused the reinitialization + refresh_displays(encoder.platform_formats->dev_type, display_names, display_p); + + // Process any pending display switch with the new list of displays + if (switch_display_event->peek()) { + display_p = std::clamp(*switch_display_event->pop(), 0, (int) display_names.size() - 1); + } + // reset_display() will sleep between retries - reset_display(disp, encoder.base_dev_type, display_names[display_p], synced_session_ctxs.front()->config); + reset_display(disp, encoder.platform_formats->dev_type, display_names[display_p], synced_session_ctxs.front()->config); if (disp) { break; } @@ -1582,7 +2005,6 @@ namespace video { } KITTY_WHILE_LOOP(auto pos = std::begin(synced_sessions), pos != std::end(synced_sessions), { - auto frame = pos->session.device->frame; auto ctx = pos->ctx; if (ctx->shutdown_event->peek()) { // Let waiting thread know it can delete shutdown_event @@ -1601,13 +2023,11 @@ namespace video { } if (ctx->idr_events->peek()) { - frame->pict_type = AV_PICTURE_TYPE_I; - frame->key_frame = 1; - + pos->session->request_idr_frame(); ctx->idr_events->pop(); } - if (frame_captured && pos->session.device->convert(*img)) { + if (frame_captured && pos->session->convert(*img)) { BOOST_LOG(error) << "Could not convert image"sv; ctx->shutdown_event->raise(true); @@ -1619,23 +2039,20 @@ namespace video { frame_timestamp = img->frame_timestamp; } - if (encode(ctx->frame_nr++, pos->session, frame, ctx->packets, ctx->channel_data, frame_timestamp)) { + if (encode(ctx->frame_nr++, *pos->session, ctx->packets, ctx->channel_data, frame_timestamp)) { BOOST_LOG(error) << "Could not encode video packet"sv; ctx->shutdown_event->raise(true); continue; } - frame->pict_type = AV_PICTURE_TYPE_NONE; - frame->key_frame = 0; + pos->session->request_normal_frame(); ++pos; }) if (switch_display_event->peek()) { ec = platf::capture_e::reinit; - - display_p = std::clamp(*switch_display_event->pop(), 0, (int) display_names.size() - 1); return false; } @@ -1686,7 +2103,9 @@ namespace video { // Encoding and capture takes place on this thread platf::adjust_thread_priority(platf::thread_priority_e::high); - while (encode_run_sync(synced_session_ctxs, ctx) == encode_e::reinit) {} + std::vector display_names; + int display_p = -1; + while (encode_run_sync(synced_session_ctxs, ctx, display_names, display_p) == encode_e::reinit) {} } void @@ -1739,9 +2158,9 @@ namespace video { } auto &encoder = *chosen_encoder; - auto pix_fmt = config.dynamicRange == 0 ? map_pix_fmt(encoder.static_pix_fmt) : map_pix_fmt(encoder.dynamic_pix_fmt); - auto hwdevice = display->make_hwdevice(pix_fmt); - if (!hwdevice) { + + auto encode_device = make_encode_device(*display, encoder, config); + if (!encode_device) { return; } @@ -1750,9 +2169,13 @@ namespace video { // Update client with our current HDR display state hdr_info_t hdr_info = std::make_unique(false); - if (config.dynamicRange && display->is_hdr()) { - display->get_hdr_metadata(hdr_info->metadata); - hdr_info->enabled = true; + if (colorspace_is_hdr(encode_device->colorspace)) { + if (display->get_hdr_metadata(hdr_info->metadata)) { + hdr_info->enabled = true; + } + else { + BOOST_LOG(error) << "Couldn't get display hdr metadata when colorspace selection indicates it should have one"; + } } hdr_event->raise(std::move(hdr_info)); @@ -1760,7 +2183,7 @@ namespace video { frame_nr, mail, images, config, display, - std::move(hwdevice), + std::move(encode_device), ref->reinit_event, *ref->encoder_p, channel_data); } @@ -1802,19 +2225,13 @@ namespace video { }; int - validate_config(std::shared_ptr &disp, const encoder_t &encoder, const config_t &config) { - reset_display(disp, encoder.base_dev_type, config::video.output_name, config); - if (!disp) { - return -1; - } - - auto pix_fmt = config.dynamicRange == 0 ? map_pix_fmt(encoder.static_pix_fmt) : map_pix_fmt(encoder.dynamic_pix_fmt); - auto hwdevice = disp->make_hwdevice(pix_fmt); - if (!hwdevice) { + validate_config(std::shared_ptr disp, const encoder_t &encoder, const config_t &config) { + auto encode_device = make_encode_device(*disp, encoder, config); + if (!encode_device) { return -1; } - auto session = make_session(disp.get(), encoder, config, disp->width, disp->height, std::move(hwdevice)); + auto session = make_encode_session(disp.get(), encoder, config, disp->width, disp->height, std::move(encode_device)); if (!session) { return -1; } @@ -1822,33 +2239,40 @@ namespace video { { // Image buffers are large, so we use a separate scope to free it immediately after convert() auto img = disp->alloc_img(); - if (!img || disp->dummy_img(img.get()) || session->device->convert(*img)) { + if (!img || disp->dummy_img(img.get()) || session->convert(*img)) { return -1; } } - auto frame = session->device->frame; - - frame->pict_type = AV_PICTURE_TYPE_I; + session->request_idr_frame(); auto packets = mail::man->queue(mail::video_packets); while (!packets->peek()) { - if (encode(1, *session, frame, packets, nullptr, {})) { + if (encode(1, *session, packets, nullptr, {})) { return -1; } } auto packet = packets->pop(); - auto av_packet = packet->av_packet; - if (!(av_packet->flags & AV_PKT_FLAG_KEY)) { + if (!packet->is_idr()) { BOOST_LOG(error) << "First packet type is not an IDR frame"sv; return -1; } int flag = 0; - if (cbs::validate_sps(&*av_packet, config.videoFormat ? AV_CODEC_ID_H265 : AV_CODEC_ID_H264)) { - flag |= VUI_PARAMS; + + // This check only applies for H.264 and HEVC + if (config.videoFormat <= 1) { + if (auto packet_avcodec = dynamic_cast(packet.get())) { + if (cbs::validate_sps(packet_avcodec->av_packet, config.videoFormat ? AV_CODEC_ID_H265 : AV_CODEC_ID_H264)) { + flag |= VUI_PARAMS; + } + } + else { + // Don't check it for non-avcodec encoders. + flag |= VUI_PARAMS; + } } return flag; @@ -1863,16 +2287,28 @@ namespace video { BOOST_LOG(info) << "Encoder ["sv << encoder.name << "] failed"sv; }); - auto force_hevc = active_hevc_mode >= 2; - auto test_hevc = force_hevc || (active_hevc_mode == 0 && !(encoder.flags & H264_ONLY)); + auto test_hevc = active_hevc_mode >= 2 || (active_hevc_mode == 0 && !(encoder.flags & H264_ONLY)); + auto test_av1 = active_av1_mode >= 2 || (active_av1_mode == 0 && !(encoder.flags & H264_ONLY)); encoder.h264.capabilities.set(); encoder.hevc.capabilities.set(); + encoder.av1.capabilities.set(); // First, test encoder viability config_t config_max_ref_frames { 1920, 1080, 60, 1000, 1, 1, 1, 0, 0 }; config_t config_autoselect { 1920, 1080, 60, 1000, 1, 0, 1, 0, 0 }; + // If the encoder isn't supported at all (not even H.264), bail early + reset_display(disp, encoder.platform_formats->dev_type, config::video.output_name, config_autoselect); + if (!disp) { + return false; + } + if (!disp->is_codec_supported(encoder.h264.name, config_autoselect)) { + fg.disable(); + BOOST_LOG(info) << "Encoder ["sv << encoder.name << "] is not supported on this GPU"sv; + return false; + } + retry: // If we're expecting failure, use the autoselect ref config first since that will always succeed // if the encoder is available. @@ -1907,35 +2343,78 @@ namespace video { config_max_ref_frames.videoFormat = 1; config_autoselect.videoFormat = 1; - retry_hevc: - auto max_ref_frames_hevc = validate_config(disp, encoder, config_max_ref_frames); - auto autoselect_hevc = max_ref_frames_hevc >= 0 ? max_ref_frames_hevc : validate_config(disp, encoder, config_autoselect); - if (autoselect_hevc < 0) { - if (encoder.hevc.qp && encoder.hevc[encoder_t::CBR]) { + if (disp->is_codec_supported(encoder.hevc.name, config_autoselect)) { + retry_hevc: + auto max_ref_frames_hevc = validate_config(disp, encoder, config_max_ref_frames); + + // If H.264 succeeded with max ref frames specified, assume that we can count on + // HEVC to also succeed with max ref frames specified if HEVC is supported. + auto autoselect_hevc = (max_ref_frames_hevc >= 0 || max_ref_frames_h264 >= 0) ? + max_ref_frames_hevc : + validate_config(disp, encoder, config_autoselect); + + if (autoselect_hevc < 0 && encoder.hevc.qp && encoder.hevc[encoder_t::CBR]) { // It's possible the encoder isn't accepting Constant Bit Rate. Turn off CBR and make another attempt encoder.hevc.capabilities.set(); encoder.hevc[encoder_t::CBR] = false; goto retry_hevc; } - // If HEVC must be supported, but it is not supported - if (force_hevc) { - return false; + for (auto [validate_flag, encoder_flag] : packet_deficiencies) { + encoder.hevc[encoder_flag] = (max_ref_frames_hevc & validate_flag && autoselect_hevc & validate_flag); } - } - for (auto [validate_flag, encoder_flag] : packet_deficiencies) { - encoder.hevc[encoder_flag] = (max_ref_frames_hevc & validate_flag && autoselect_hevc & validate_flag); + encoder.hevc[encoder_t::REF_FRAMES_RESTRICT] = max_ref_frames_hevc >= 0; + encoder.hevc[encoder_t::PASSED] = max_ref_frames_hevc >= 0 || autoselect_hevc >= 0; + } + else { + BOOST_LOG(info) << "Encoder ["sv << encoder.hevc.name << "] is not supported on this GPU"sv; + encoder.hevc.capabilities.reset(); } - - encoder.hevc[encoder_t::REF_FRAMES_RESTRICT] = max_ref_frames_hevc >= 0; - encoder.hevc[encoder_t::PASSED] = max_ref_frames_hevc >= 0 || autoselect_hevc >= 0; } else { // Clear all cap bits for HEVC if we didn't probe it encoder.hevc.capabilities.reset(); } + if (test_av1) { + config_max_ref_frames.videoFormat = 2; + config_autoselect.videoFormat = 2; + + if (disp->is_codec_supported(encoder.av1.name, config_autoselect)) { + retry_av1: + auto max_ref_frames_av1 = validate_config(disp, encoder, config_max_ref_frames); + + // If H.264 succeeded with max ref frames specified, assume that we can count on + // AV1 to also succeed with max ref frames specified if AV1 is supported. + auto autoselect_av1 = (max_ref_frames_av1 >= 0 || max_ref_frames_h264 >= 0) ? + max_ref_frames_av1 : + validate_config(disp, encoder, config_autoselect); + + if (autoselect_av1 < 0 && encoder.av1.qp && encoder.av1[encoder_t::CBR]) { + // It's possible the encoder isn't accepting Constant Bit Rate. Turn off CBR and make another attempt + encoder.av1.capabilities.set(); + encoder.av1[encoder_t::CBR] = false; + goto retry_av1; + } + + for (auto [validate_flag, encoder_flag] : packet_deficiencies) { + encoder.av1[encoder_flag] = (max_ref_frames_av1 & validate_flag && autoselect_av1 & validate_flag); + } + + encoder.av1[encoder_t::REF_FRAMES_RESTRICT] = max_ref_frames_av1 >= 0; + encoder.av1[encoder_t::PASSED] = max_ref_frames_av1 >= 0 || autoselect_av1 >= 0; + } + else { + BOOST_LOG(info) << "Encoder ["sv << encoder.av1.name << "] is not supported on this GPU"sv; + encoder.av1.capabilities.reset(); + } + } + else { + // Clear all cap bits for AV1 if we didn't probe it + encoder.av1.capabilities.reset(); + } + std::vector> configs { { encoder_t::DYNAMIC_RANGE, { 1920, 1080, 60, 1000, 1, 0, 3, 1, 1 } }, }; @@ -1943,9 +2422,17 @@ namespace video { for (auto &[flag, config] : configs) { auto h264 = config; auto hevc = config; + auto av1 = config; h264.videoFormat = 0; hevc.videoFormat = 1; + av1.videoFormat = 2; + + // Reset the display since we're switching from SDR to HDR + reset_display(disp, encoder.platform_formats->dev_type, config::video.output_name, config); + if (!disp) { + return false; + } // HDR is not supported with H.264. Don't bother even trying it. encoder.h264[flag] = flag != encoder_t::DYNAMIC_RANGE && validate_config(disp, encoder, h264) >= 0; @@ -1953,6 +2440,10 @@ namespace video { if (encoder.hevc[encoder_t::PASSED]) { encoder.hevc[flag] = validate_config(disp, encoder, hevc) >= 0; } + + if (encoder.av1[encoder_t::PASSED]) { + encoder.av1[flag] = validate_config(disp, encoder, av1) >= 0; + } } encoder.h264[encoder_t::VUI_PARAMETERS] = encoder.h264[encoder_t::VUI_PARAMETERS] && !config::sunshine.flags[config::flag::FORCE_VIDEO_HEADER_REPLACE]; @@ -1971,7 +2462,7 @@ namespace video { /** * This is called once at startup and each time a stream is launched to - * ensure the best encoder is selected. Encoder availablility can change + * ensure the best encoder is selected. Encoder availability can change * at runtime due to all sorts of things from driver updates to eGPUs. * * This is only safe to call when there is no client actively streaming. @@ -1980,10 +2471,38 @@ namespace video { probe_encoders() { auto encoder_list = encoders; + // If we already have a good encoder, check to see if another probe is required + if (chosen_encoder && !(chosen_encoder->flags & ALWAYS_REPROBE) && !platf::needs_encoder_reenumeration()) { + return 0; + } + // Restart encoder selection auto previous_encoder = chosen_encoder; chosen_encoder = nullptr; active_hevc_mode = config::video.hevc_mode; + active_av1_mode = config::video.av1_mode; + last_encoder_probe_supported_ref_frames_invalidation = false; + + auto adjust_encoder_constraints = [&](encoder_t *encoder) { + // If we can't satisfy both the encoder and codec requirement, prefer the encoder over codec support + if (active_hevc_mode == 3 && !encoder->hevc[encoder_t::DYNAMIC_RANGE]) { + BOOST_LOG(warning) << "Encoder ["sv << encoder->name << "] does not support HEVC Main10 on this system"sv; + active_hevc_mode = 0; + } + else if (active_hevc_mode == 2 && !encoder->hevc[encoder_t::PASSED]) { + BOOST_LOG(warning) << "Encoder ["sv << encoder->name << "] does not support HEVC on this system"sv; + active_hevc_mode = 0; + } + + if (active_av1_mode == 3 && !encoder->av1[encoder_t::DYNAMIC_RANGE]) { + BOOST_LOG(warning) << "Encoder ["sv << encoder->name << "] does not support AV1 Main10 on this system"sv; + active_av1_mode = 0; + } + else if (active_av1_mode == 2 && !encoder->av1[encoder_t::PASSED]) { + BOOST_LOG(warning) << "Encoder ["sv << encoder->name << "] does not support AV1 on this system"sv; + active_av1_mode = 0; + } + }; if (!config::video.encoder.empty()) { // If there is a specific encoder specified, use it if it passes validation @@ -1997,11 +2516,8 @@ namespace video { break; } - // If we can't satisfy both the encoder and HDR requirement, prefer the encoder over HDR support - if (active_hevc_mode == 3 && !encoder->hevc[encoder_t::DYNAMIC_RANGE]) { - BOOST_LOG(warning) << "Encoder ["sv << config::video.encoder << "] does not support HDR on this system"sv; - active_hevc_mode = 0; - } + // We will return an encoder here even if it fails one of the codec requirements specified by the user + adjust_encoder_constraints(encoder); chosen_encoder = encoder; break; @@ -2017,8 +2533,8 @@ namespace video { BOOST_LOG(info) << "// Testing for available encoders, this may generate errors. You can safely ignore those errors. //"sv; - // If we haven't found an encoder yet, but we want one with HDR support, search for that now. - if (chosen_encoder == nullptr && active_hevc_mode == 3) { + // If we haven't found an encoder yet, but we want one with specific codec support, search for that now. + if (chosen_encoder == nullptr && (active_hevc_mode >= 2 || active_av1_mode >= 2)) { KITTY_WHILE_LOOP(auto pos = std::begin(encoder_list), pos != std::end(encoder_list), { auto encoder = *pos; @@ -2028,8 +2544,16 @@ namespace video { continue; } - // Skip it if it doesn't support HDR - if (!encoder->hevc[encoder_t::DYNAMIC_RANGE]) { + // Skip it if it doesn't support the specified codec at all + if ((active_hevc_mode >= 2 && !encoder->hevc[encoder_t::PASSED]) || + (active_av1_mode >= 2 && !encoder->av1[encoder_t::PASSED])) { + pos++; + continue; + } + + // Skip it if it doesn't support HDR on the specified codec + if ((active_hevc_mode == 3 && !encoder->hevc[encoder_t::DYNAMIC_RANGE]) || + (active_av1_mode == 3 && !encoder->av1[encoder_t::DYNAMIC_RANGE])) { pos++; continue; } @@ -2039,7 +2563,7 @@ namespace video { }); if (chosen_encoder == nullptr) { - BOOST_LOG(error) << "Couldn't find any working HDR-capable encoder"sv; + BOOST_LOG(error) << "Couldn't find any working encoder that meets HEVC/AV1 requirements"sv; } } @@ -2057,13 +2581,22 @@ namespace video { continue; } + // We will return an encoder here even if it fails one of the codec requirements specified by the user + adjust_encoder_constraints(encoder); + chosen_encoder = encoder; break; }); } if (chosen_encoder == nullptr) { - BOOST_LOG(fatal) << "Couldn't find any working encoder"sv; + BOOST_LOG(fatal) << "Unable to find display or encoder during startup."sv; + if (!config::video.adapter_name.empty() || !config::video.output_name.empty()) { + BOOST_LOG(fatal) << "Please ensure your manually chosen GPU and monitor are connected and powered on."sv; + } + else { + BOOST_LOG(fatal) << "Please check that a display is connected and powered on."sv; + } return -1; } @@ -2073,12 +2606,15 @@ namespace video { auto &encoder = *chosen_encoder; + last_encoder_probe_supported_ref_frames_invalidation = (encoder.flags & REF_FRAMES_INVALIDATION); + BOOST_LOG(debug) << "------ h264 ------"sv; for (int x = 0; x < encoder_t::MAX_FLAGS; ++x) { auto flag = (encoder_t::flag_e) x; BOOST_LOG(debug) << encoder_t::from_flag(flag) << (encoder.h264[flag] ? ": supported"sv : ": unsupported"sv); } BOOST_LOG(debug) << "-------------------"sv; + BOOST_LOG(info) << "Found H.264 encoder: "sv << encoder.h264.name << " ["sv << encoder.name << ']'; if (encoder.hevc[encoder_t::PASSED]) { BOOST_LOG(debug) << "------ hevc ------"sv; @@ -2088,52 +2624,41 @@ namespace video { } BOOST_LOG(debug) << "-------------------"sv; - BOOST_LOG(info) << "Found encoder "sv << encoder.name << ": ["sv << encoder.h264.name << ", "sv << encoder.hevc.name << ']'; + BOOST_LOG(info) << "Found HEVC encoder: "sv << encoder.hevc.name << " ["sv << encoder.name << ']'; } - else { - BOOST_LOG(info) << "Found encoder "sv << encoder.name << ": ["sv << encoder.h264.name << ']'; + + if (encoder.av1[encoder_t::PASSED]) { + BOOST_LOG(debug) << "------ av1 ------"sv; + for (int x = 0; x < encoder_t::MAX_FLAGS; ++x) { + auto flag = (encoder_t::flag_e) x; + BOOST_LOG(debug) << encoder_t::from_flag(flag) << (encoder.av1[flag] ? ": supported"sv : ": unsupported"sv); + } + BOOST_LOG(debug) << "-------------------"sv; + + BOOST_LOG(info) << "Found AV1 encoder: "sv << encoder.av1.name << " ["sv << encoder.name << ']'; } if (active_hevc_mode == 0) { active_hevc_mode = encoder.hevc[encoder_t::PASSED] ? (encoder.hevc[encoder_t::DYNAMIC_RANGE] ? 3 : 2) : 1; } - return 0; - } - - int - hwframe_ctx(ctx_t &ctx, platf::hwdevice_t *hwdevice, buffer_t &hwdevice_ctx, AVPixelFormat format) { - buffer_t frame_ref { av_hwframe_ctx_alloc(hwdevice_ctx.get()) }; - - auto frame_ctx = (AVHWFramesContext *) frame_ref->data; - frame_ctx->format = ctx->pix_fmt; - frame_ctx->sw_format = format; - frame_ctx->height = ctx->height; - frame_ctx->width = ctx->width; - frame_ctx->initial_pool_size = 0; - - // Allow the hwdevice to modify hwframe context parameters - hwdevice->init_hwframes(frame_ctx); - - if (auto err = av_hwframe_ctx_init(frame_ref.get()); err < 0) { - return err; + if (active_av1_mode == 0) { + active_av1_mode = encoder.av1[encoder_t::PASSED] ? (encoder.av1[encoder_t::DYNAMIC_RANGE] ? 3 : 2) : 1; } - ctx->hw_frames_ctx = av_buffer_ref(frame_ref.get()); - return 0; } // Linux only declaration - typedef int (*vaapi_make_hwdevice_ctx_fn)(platf::hwdevice_t *base, AVBufferRef **hw_device_buf); + typedef int (*vaapi_init_avcodec_hardware_input_buffer_fn)(platf::avcodec_encode_device_t *encode_device, AVBufferRef **hw_device_buf); - util::Either - vaapi_make_hwdevice_ctx(platf::hwdevice_t *base) { - buffer_t hw_device_buf; + util::Either + vaapi_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *encode_device) { + avcodec_buffer_t hw_device_buf; // If an egl hwdevice - if (base->data) { - if (((vaapi_make_hwdevice_ctx_fn) base->data)(base, &hw_device_buf)) { + if (encode_device->data) { + if (((vaapi_init_avcodec_hardware_input_buffer_fn) encode_device->data)(encode_device, &hw_device_buf)) { return -1; } @@ -2152,9 +2677,9 @@ namespace video { return hw_device_buf; } - util::Either - cuda_make_hwdevice_ctx(platf::hwdevice_t *base) { - buffer_t hw_device_buf; + util::Either + cuda_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *encode_device) { + avcodec_buffer_t hw_device_buf; auto status = av_hwdevice_ctx_create(&hw_device_buf, AV_HWDEVICE_TYPE_CUDA, nullptr, nullptr, 1 /* AV_CUDA_USE_PRIMARY_CONTEXT */); if (status < 0) { @@ -2166,6 +2691,20 @@ namespace video { return hw_device_buf; } + util::Either + vt_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *encode_device) { + avcodec_buffer_t hw_device_buf; + + auto status = av_hwdevice_ctx_create(&hw_device_buf, AV_HWDEVICE_TYPE_VIDEOTOOLBOX, nullptr, nullptr, 0); + if (status < 0) { + char string[AV_ERROR_MAX_STRING_SIZE]; + BOOST_LOG(error) << "Failed to create a VideoToolbox device: "sv << av_make_error_string(string, AV_ERROR_MAX_STRING_SIZE, status); + return -1; + } + + return hw_device_buf; + } + #ifdef _WIN32 } @@ -2173,14 +2712,14 @@ void do_nothing(void *) {} namespace video { - util::Either - dxgi_make_hwdevice_ctx(platf::hwdevice_t *hwdevice_ctx) { - buffer_t ctx_buf { av_hwdevice_ctx_alloc(AV_HWDEVICE_TYPE_D3D11VA) }; + util::Either + dxgi_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *encode_device) { + avcodec_buffer_t ctx_buf { av_hwdevice_ctx_alloc(AV_HWDEVICE_TYPE_D3D11VA) }; auto ctx = (AVD3D11VADeviceContext *) ((AVHWDeviceContext *) ctx_buf->data)->hwctx; std::fill_n((std::uint8_t *) ctx, sizeof(AVD3D11VADeviceContext), 0); - auto device = (ID3D11Device *) hwdevice_ctx->data; + auto device = (ID3D11Device *) encode_device->data; device->AddRef(); ctx->device = device; @@ -2244,6 +2783,8 @@ namespace video { return platf::mem_type_e::cuda; case AV_HWDEVICE_TYPE_NONE: return platf::mem_type_e::system; + case AV_HWDEVICE_TYPE_VIDEOTOOLBOX: + return platf::mem_type_e::videotoolbox; default: return platf::mem_type_e::unknown; } @@ -2269,33 +2810,4 @@ namespace video { return platf::pix_fmt_e::unknown; } - color_t - make_color_matrix(float Cr, float Cb, const float2 &range_Y, const float2 &range_UV) { - float Cg = 1.0f - Cr - Cb; - - float Cr_i = 1.0f - Cr; - float Cb_i = 1.0f - Cb; - - float shift_y = range_Y[0] / 255.0f; - float shift_uv = range_UV[0] / 255.0f; - - float scale_y = (range_Y[1] - range_Y[0]) / 255.0f; - float scale_uv = (range_UV[1] - range_UV[0]) / 255.0f; - return { - { Cr, Cg, Cb, 0.0f }, - { -(Cr * 0.5f / Cb_i), -(Cg * 0.5f / Cb_i), 0.5f, 0.5f }, - { 0.5f, -(Cg * 0.5f / Cr_i), -(Cb * 0.5f / Cr_i), 0.5f }, - { scale_y, shift_y }, - { scale_uv, shift_uv }, - }; - } - - color_t colors[] { - make_color_matrix(0.299f, 0.114f, { 16.0f, 235.0f }, { 16.0f, 240.0f }), // BT601 MPEG - make_color_matrix(0.299f, 0.114f, { 0.0f, 255.0f }, { 0.0f, 255.0f }), // BT601 JPEG - make_color_matrix(0.2126f, 0.0722f, { 16.0f, 235.0f }, { 16.0f, 240.0f }), // BT709 MPEG - make_color_matrix(0.2126f, 0.0722f, { 0.0f, 255.0f }, { 0.0f, 255.0f }), // BT709 JPEG - make_color_matrix(0.2627f, 0.0593f, { 16.0f, 235.0f }, { 16.0f, 240.0f }), // BT2020 MPEG - make_color_matrix(0.2627f, 0.0593f, { 0.0f, 255.0f }, { 0.0f, 255.0f }), // BT2020 JPEG - }; } // namespace video diff --git a/src/video.h b/src/video.h index d906a2f93b5..ba80474669f 100644 --- a/src/video.h +++ b/src/video.h @@ -7,35 +7,200 @@ #include "input.h" #include "platform/common.h" #include "thread_safe.h" +#include "video_colorspace.h" extern "C" { #include +#include } struct AVPacket; namespace video { - struct packet_raw_t { - void - init_packet() { - this->av_packet = av_packet_alloc(); - } + platf::mem_type_e + map_base_dev_type(AVHWDeviceType type); + platf::pix_fmt_e + map_pix_fmt(AVPixelFormat fmt); + + void + free_ctx(AVCodecContext *ctx); + void + free_frame(AVFrame *frame); + void + free_buffer(AVBufferRef *ref); + + using avcodec_ctx_t = util::safe_ptr; + using avcodec_frame_t = util::safe_ptr; + using avcodec_buffer_t = util::safe_ptr; + using sws_t = util::safe_ptr; + using img_event_t = std::shared_ptr>>; + + struct encoder_platform_formats_t { + virtual ~encoder_platform_formats_t() = default; + platf::mem_type_e dev_type; + platf::pix_fmt_e pix_fmt_8bit, pix_fmt_10bit; + }; + + struct encoder_platform_formats_avcodec: encoder_platform_formats_t { + using init_buffer_function_t = std::function(platf::avcodec_encode_device_t *)>; - template - explicit packet_raw_t(P *user_data): - channel_data { user_data } { - init_packet(); + encoder_platform_formats_avcodec( + const AVHWDeviceType &avcodec_base_dev_type, + const AVHWDeviceType &avcodec_derived_dev_type, + const AVPixelFormat &avcodec_dev_pix_fmt, + const AVPixelFormat &avcodec_pix_fmt_8bit, + const AVPixelFormat &avcodec_pix_fmt_10bit, + const init_buffer_function_t &init_avcodec_hardware_input_buffer_function): + avcodec_base_dev_type { avcodec_base_dev_type }, + avcodec_derived_dev_type { avcodec_derived_dev_type }, + avcodec_dev_pix_fmt { avcodec_dev_pix_fmt }, + avcodec_pix_fmt_8bit { avcodec_pix_fmt_8bit }, + avcodec_pix_fmt_10bit { avcodec_pix_fmt_10bit }, + init_avcodec_hardware_input_buffer { init_avcodec_hardware_input_buffer_function } { + dev_type = map_base_dev_type(avcodec_base_dev_type); + pix_fmt_8bit = map_pix_fmt(avcodec_pix_fmt_8bit); + pix_fmt_10bit = map_pix_fmt(avcodec_pix_fmt_10bit); } - explicit packet_raw_t(std::nullptr_t): - channel_data { nullptr } { - init_packet(); + AVHWDeviceType avcodec_base_dev_type, avcodec_derived_dev_type; + AVPixelFormat avcodec_dev_pix_fmt; + AVPixelFormat avcodec_pix_fmt_8bit, avcodec_pix_fmt_10bit; + + init_buffer_function_t init_avcodec_hardware_input_buffer; + }; + + struct encoder_platform_formats_nvenc: encoder_platform_formats_t { + encoder_platform_formats_nvenc( + const platf::mem_type_e &dev_type, + const platf::pix_fmt_e &pix_fmt_8bit, + const platf::pix_fmt_e &pix_fmt_10bit) { + encoder_platform_formats_t::dev_type = dev_type; + encoder_platform_formats_t::pix_fmt_8bit = pix_fmt_8bit; + encoder_platform_formats_t::pix_fmt_10bit = pix_fmt_10bit; } + }; - ~packet_raw_t() { - av_packet_free(&this->av_packet); + struct encoder_t { + std::string_view name; + enum flag_e { + PASSED, // Is supported + REF_FRAMES_RESTRICT, // Set maximum reference frames + CBR, // Some encoders don't support CBR, if not supported --> attempt constant quantatication parameter instead + DYNAMIC_RANGE, // hdr + VUI_PARAMETERS, // AMD encoder with VAAPI doesn't add VUI parameters to SPS + MAX_FLAGS + }; + + static std::string_view + from_flag(flag_e flag) { +#define _CONVERT(x) \ + case flag_e::x: \ + return std::string_view(#x) + switch (flag) { + _CONVERT(PASSED); + _CONVERT(REF_FRAMES_RESTRICT); + _CONVERT(CBR); + _CONVERT(DYNAMIC_RANGE); + _CONVERT(VUI_PARAMETERS); + _CONVERT(MAX_FLAGS); + } +#undef _CONVERT + + return { "unknown" }; } + struct option_t { + KITTY_DEFAULT_CONSTR_MOVE(option_t) + option_t(const option_t &) = default; + + std::string name; + std::variant *, std::function, std::string, std::string *> value; + + option_t(std::string &&name, decltype(value) &&value): + name { std::move(name) }, value { std::move(value) } {} + }; + + const std::unique_ptr platform_formats; + + struct codec_t { + std::vector common_options; + std::vector sdr_options; + std::vector hdr_options; + std::vector fallback_options; + + // QP option to set in the case that CBR/VBR is not supported + // by the encoder. If CBR/VBR is guaranteed to be supported, + // don't specify this option to avoid wasteful encoder probing. + std::optional qp; + + std::string name; + std::bitset capabilities; + + bool + operator[](flag_e flag) const { + return capabilities[(std::size_t) flag]; + } + + std::bitset::reference + operator[](flag_e flag) { + return capabilities[(std::size_t) flag]; + } + } av1, hevc, h264; + + uint32_t flags; + }; + + struct encode_session_t { + virtual ~encode_session_t() = default; + + virtual int + convert(platf::img_t &img) = 0; + + virtual void + request_idr_frame() = 0; + + virtual void + request_normal_frame() = 0; + + virtual void + invalidate_ref_frames(int64_t first_frame, int64_t last_frame) = 0; + }; + + // encoders + extern encoder_t software; + +#if !defined(__APPLE__) + extern encoder_t nvenc; // available for windows and linux +#endif + +#ifdef _WIN32 + extern encoder_t amdvce; + extern encoder_t quicksync; +#endif + +#ifdef __linux__ + extern encoder_t vaapi; +#endif + +#ifdef __APPLE__ + extern encoder_t videotoolbox; +#endif + + struct packet_raw_t { + virtual ~packet_raw_t() = default; + + virtual bool + is_idr() = 0; + + virtual int64_t + frame_index() = 0; + + virtual uint8_t * + data() = 0; + + virtual size_t + data_size() = 0; + struct replace_t { std::string_view old; std::string_view _new; @@ -46,11 +211,72 @@ namespace video { old { std::move(old) }, _new { std::move(_new) } {} }; + std::vector *replacements = nullptr; + void *channel_data = nullptr; + bool after_ref_frame_invalidation = false; + std::optional frame_timestamp; + }; + + struct packet_raw_avcodec: packet_raw_t { + packet_raw_avcodec() { + av_packet = av_packet_alloc(); + } + + ~packet_raw_avcodec() { + av_packet_free(&this->av_packet); + } + + bool + is_idr() override { + return av_packet->flags & AV_PKT_FLAG_KEY; + } + + int64_t + frame_index() override { + return av_packet->pts; + } + + uint8_t * + data() override { + return av_packet->data; + } + + size_t + data_size() override { + return av_packet->size; + } + AVPacket *av_packet; - std::vector *replacements; - void *channel_data; + }; - std::optional frame_timestamp; + struct packet_raw_generic: packet_raw_t { + packet_raw_generic(std::vector &&frame_data, int64_t frame_index, bool idr): + frame_data { std::move(frame_data) }, index { frame_index }, idr { idr } { + } + + bool + is_idr() override { + return idr; + } + + int64_t + frame_index() override { + return index; + } + + uint8_t * + data() override { + return frame_data.data(); + } + + size_t + data_size() override { + return frame_data.size(); + } + + std::vector frame_data; + int64_t index; + bool idr; }; using packet_t = std::unique_ptr; @@ -67,33 +293,30 @@ namespace video { using hdr_info_t = std::unique_ptr; + /* Encoding configuration requested by remote client */ struct config_t { - int width; - int height; - int framerate; - int bitrate; - int slicesPerFrame; - int numRefFrames; + int width; // Video width in pixels + int height; // Video height in pixels + int framerate; // Requested framerate, used in individual frame bitrate budget calculation + int bitrate; // Video bitrate in kilobits (1000 bits) for requested framerate + int slicesPerFrame; // Number of slices per frame + int numRefFrames; // Max number of reference frames + + /* Requested color range and SDR encoding colorspace, HDR encoding colorspace is always BT.2020+ST2084 + Color range (encoderCscMode & 0x1) : 0 - limited, 1 - full + SDR encoding colorspace (encoderCscMode >> 1) : 0 - BT.601, 1 - BT.709, 2 - BT.2020 */ int encoderCscMode; - int videoFormat; - int dynamicRange; - }; - using float4 = float[4]; - using float3 = float[3]; - using float2 = float[2]; + int videoFormat; // 0 - H.264, 1 - HEVC, 2 - AV1 - struct alignas(16) color_t { - float4 color_vec_y; - float4 color_vec_u; - float4 color_vec_v; - float2 range_y; - float2 range_uv; + /* Encoding color depth (bit depth): 0 - 8-bit, 1 - 10-bit + HDR encoding activates when color depth is higher than 8-bit and the display which is being captured is operating in HDR mode */ + int dynamicRange; }; - extern color_t colors[6]; - extern int active_hevc_mode; + extern int active_av1_mode; + extern bool last_encoder_probe_supported_ref_frames_invalidation; void capture( @@ -101,6 +324,8 @@ namespace video { config_t config, void *channel_data); + bool + validate_encoder(encoder_t &encoder, bool expect_failure); int probe_encoders(); } // namespace video diff --git a/src/video_colorspace.cpp b/src/video_colorspace.cpp new file mode 100644 index 00000000000..4f5955eed7e --- /dev/null +++ b/src/video_colorspace.cpp @@ -0,0 +1,181 @@ +#include "video_colorspace.h" + +#include "logging.h" +#include "video.h" + +extern "C" { +#include +} + +namespace video { + + bool + colorspace_is_hdr(const sunshine_colorspace_t &colorspace) { + return colorspace.colorspace == colorspace_e::bt2020; + } + + sunshine_colorspace_t + colorspace_from_client_config(const config_t &config, bool hdr_display) { + sunshine_colorspace_t colorspace; + + /* See video::config_t declaration for details */ + + if (config.dynamicRange > 0 && hdr_display) { + // Rec. 2020 with ST 2084 perceptual quantizer + colorspace.colorspace = colorspace_e::bt2020; + } + else { + switch (config.encoderCscMode >> 1) { + case 0: + // Rec. 601 + colorspace.colorspace = colorspace_e::rec601; + break; + + case 1: + // Rec. 709 + colorspace.colorspace = colorspace_e::rec709; + break; + + case 2: + // Rec. 2020 + colorspace.colorspace = colorspace_e::bt2020sdr; + break; + + default: + BOOST_LOG(error) << "Unknown video colorspace in csc, falling back to Rec. 709"; + colorspace.colorspace = colorspace_e::rec709; + break; + } + } + + colorspace.full_range = (config.encoderCscMode & 0x1); + + switch (config.dynamicRange) { + case 0: + colorspace.bit_depth = 8; + break; + + case 1: + colorspace.bit_depth = 10; + break; + + default: + BOOST_LOG(error) << "Unknown dynamicRange value, falling back to 10-bit color depth"; + colorspace.bit_depth = 10; + break; + } + + if (colorspace.colorspace == colorspace_e::bt2020sdr && colorspace.bit_depth != 10) { + BOOST_LOG(error) << "BT.2020 SDR colorspace expects 10-bit color depth, falling back to Rec. 709"; + colorspace.colorspace = colorspace_e::rec709; + } + + return colorspace; + } + + avcodec_colorspace_t + avcodec_colorspace_from_sunshine_colorspace(const sunshine_colorspace_t &sunshine_colorspace) { + avcodec_colorspace_t avcodec_colorspace; + + switch (sunshine_colorspace.colorspace) { + case colorspace_e::rec601: + // Rec. 601 + avcodec_colorspace.primaries = AVCOL_PRI_SMPTE170M; + avcodec_colorspace.transfer_function = AVCOL_TRC_SMPTE170M; + avcodec_colorspace.matrix = AVCOL_SPC_SMPTE170M; + avcodec_colorspace.software_format = SWS_CS_SMPTE170M; + break; + + case colorspace_e::rec709: + // Rec. 709 + avcodec_colorspace.primaries = AVCOL_PRI_BT709; + avcodec_colorspace.transfer_function = AVCOL_TRC_BT709; + avcodec_colorspace.matrix = AVCOL_SPC_BT709; + avcodec_colorspace.software_format = SWS_CS_ITU709; + break; + + case colorspace_e::bt2020sdr: + // Rec. 2020 + avcodec_colorspace.primaries = AVCOL_PRI_BT2020; + assert(sunshine_colorspace.bit_depth == 10); + avcodec_colorspace.transfer_function = AVCOL_TRC_BT2020_10; + avcodec_colorspace.matrix = AVCOL_SPC_BT2020_NCL; + avcodec_colorspace.software_format = SWS_CS_BT2020; + break; + + case colorspace_e::bt2020: + // Rec. 2020 with ST 2084 perceptual quantizer + avcodec_colorspace.primaries = AVCOL_PRI_BT2020; + assert(sunshine_colorspace.bit_depth == 10); + avcodec_colorspace.transfer_function = AVCOL_TRC_SMPTE2084; + avcodec_colorspace.matrix = AVCOL_SPC_BT2020_NCL; + avcodec_colorspace.software_format = SWS_CS_BT2020; + break; + } + + avcodec_colorspace.range = sunshine_colorspace.full_range ? AVCOL_RANGE_JPEG : AVCOL_RANGE_MPEG; + + return avcodec_colorspace; + } + + const color_t * + color_vectors_from_colorspace(const sunshine_colorspace_t &colorspace) { + return color_vectors_from_colorspace(colorspace.colorspace, colorspace.full_range); + } + + const color_t * + color_vectors_from_colorspace(colorspace_e colorspace, bool full_range) { + using float2 = float[2]; + auto make_color_matrix = [](float Cr, float Cb, const float2 &range_Y, const float2 &range_UV) -> color_t { + float Cg = 1.0f - Cr - Cb; + + float Cr_i = 1.0f - Cr; + float Cb_i = 1.0f - Cb; + + float shift_y = range_Y[0] / 255.0f; + float shift_uv = range_UV[0] / 255.0f; + + float scale_y = (range_Y[1] - range_Y[0]) / 255.0f; + float scale_uv = (range_UV[1] - range_UV[0]) / 255.0f; + return { + { Cr, Cg, Cb, 0.0f }, + { -(Cr * 0.5f / Cb_i), -(Cg * 0.5f / Cb_i), 0.5f, 0.5f }, + { 0.5f, -(Cg * 0.5f / Cr_i), -(Cb * 0.5f / Cr_i), 0.5f }, + { scale_y, shift_y }, + { scale_uv, shift_uv }, + }; + }; + + static const color_t colors[] { + make_color_matrix(0.299f, 0.114f, { 16.0f, 235.0f }, { 16.0f, 240.0f }), // BT601 MPEG + make_color_matrix(0.299f, 0.114f, { 0.0f, 255.0f }, { 0.0f, 255.0f }), // BT601 JPEG + make_color_matrix(0.2126f, 0.0722f, { 16.0f, 235.0f }, { 16.0f, 240.0f }), // BT709 MPEG + make_color_matrix(0.2126f, 0.0722f, { 0.0f, 255.0f }, { 0.0f, 255.0f }), // BT709 JPEG + make_color_matrix(0.2627f, 0.0593f, { 16.0f, 235.0f }, { 16.0f, 240.0f }), // BT2020 MPEG + make_color_matrix(0.2627f, 0.0593f, { 0.0f, 255.0f }, { 0.0f, 255.0f }), // BT2020 JPEG + }; + + const color_t *result = nullptr; + + switch (colorspace) { + case colorspace_e::rec601: + default: + result = &colors[0]; + break; + case colorspace_e::rec709: + result = &colors[2]; + break; + case colorspace_e::bt2020: + case colorspace_e::bt2020sdr: + result = &colors[4]; + break; + }; + + if (full_range) { + result++; + } + + return result; + } + +} // namespace video diff --git a/src/video_colorspace.h b/src/video_colorspace.h new file mode 100644 index 00000000000..858914ce6ed --- /dev/null +++ b/src/video_colorspace.h @@ -0,0 +1,56 @@ +#pragma once + +extern "C" { +#include +} + +namespace video { + + enum class colorspace_e { + rec601, + rec709, + bt2020sdr, + bt2020, + }; + + struct sunshine_colorspace_t { + colorspace_e colorspace; + bool full_range; + unsigned bit_depth; + }; + + bool + colorspace_is_hdr(const sunshine_colorspace_t &colorspace); + + // Declared in video.h + struct config_t; + + sunshine_colorspace_t + colorspace_from_client_config(const config_t &config, bool hdr_display); + + struct avcodec_colorspace_t { + AVColorPrimaries primaries; + AVColorTransferCharacteristic transfer_function; + AVColorSpace matrix; + AVColorRange range; + int software_format; + }; + + avcodec_colorspace_t + avcodec_colorspace_from_sunshine_colorspace(const sunshine_colorspace_t &sunshine_colorspace); + + struct alignas(16) color_t { + float color_vec_y[4]; + float color_vec_u[4]; + float color_vec_v[4]; + float range_y[2]; + float range_uv[2]; + }; + + const color_t * + color_vectors_from_colorspace(const sunshine_colorspace_t &colorspace); + + const color_t * + color_vectors_from_colorspace(colorspace_e colorspace, bool full_range); + +} // namespace video diff --git a/src_assets/common/assets/box.png b/src_assets/common/assets/box.png index 7a25260b1d1..2f196598375 100644 Binary files a/src_assets/common/assets/box.png and b/src_assets/common/assets/box.png differ diff --git a/src_assets/common/assets/desktop-alt.png b/src_assets/common/assets/desktop-alt.png index e2d5d097cb7..7d4df087340 100644 Binary files a/src_assets/common/assets/desktop-alt.png and b/src_assets/common/assets/desktop-alt.png differ diff --git a/src_assets/common/assets/desktop.png b/src_assets/common/assets/desktop.png index 50e7ffdda92..9219b1440f3 100644 Binary files a/src_assets/common/assets/desktop.png and b/src_assets/common/assets/desktop.png differ diff --git a/src_assets/common/assets/steam.png b/src_assets/common/assets/steam.png index 753cd54ebd0..29c9e69ebb2 100644 Binary files a/src_assets/common/assets/steam.png and b/src_assets/common/assets/steam.png differ diff --git a/src_assets/common/assets/web/Navbar.vue b/src_assets/common/assets/web/Navbar.vue new file mode 100644 index 00000000000..838c630f45a --- /dev/null +++ b/src_assets/common/assets/web/Navbar.vue @@ -0,0 +1,89 @@ + + + + + diff --git a/src_assets/common/assets/web/PlatformLayout.vue b/src_assets/common/assets/web/PlatformLayout.vue new file mode 100644 index 00000000000..31064fec620 --- /dev/null +++ b/src_assets/common/assets/web/PlatformLayout.vue @@ -0,0 +1,27 @@ + + + + + + diff --git a/src_assets/common/assets/web/ResourceCard.vue b/src_assets/common/assets/web/ResourceCard.vue new file mode 100644 index 00000000000..aee837689cf --- /dev/null +++ b/src_assets/common/assets/web/ResourceCard.vue @@ -0,0 +1,33 @@ + diff --git a/src_assets/common/assets/web/ThemeToggle.vue b/src_assets/common/assets/web/ThemeToggle.vue new file mode 100644 index 00000000000..7c34916adc9 --- /dev/null +++ b/src_assets/common/assets/web/ThemeToggle.vue @@ -0,0 +1,46 @@ + + + + + diff --git a/src_assets/common/assets/web/apps.html b/src_assets/common/assets/web/apps.html index f3243036f96..9a8c65d16ae 100644 --- a/src_assets/common/assets/web/apps.html +++ b/src_assets/common/assets/web/apps.html @@ -1,331 +1,369 @@ -
-
-

Applications

-
Applications are refreshed only when Client is restarted
-
-
- - - - - - - - - - - - - -
NameActions
{{app.name}} - - -
-
-
-
- -
- - -
- Application Name, as shown on Moonlight + + + + + <%- header %> + + + + + +
+
+

{{ $t('apps.applications_title') }}

+
{{ $t('apps.applications_desc') }}
+
+
+ + + + + + + + + + + + + +
{{ $t('apps.name') }}{{ $t('apps.actions') }}
{{app.name}} + + +
+
+
+
+ +
+ + +
{{ $t('apps.app_name_desc') }}
-
- -
- - -
- The file where the output of the command is stored, if it is not - specified, the output is ignored + +
+ + +
{{ $t('apps.output_desc') }}
-
- -
- - -
- Enable/Disable the execution of Global Prep Commands for this - application. + +
+ + +
{{ $t('apps.global_prep_desc') }}
-
-
- -
- A list of commands to be run before/after this application.
- If any of the prep-commands fail, starting the application is aborted. +
+ +
{{ $t('apps.cmd_prep_desc') }}
+
+ +
+ + + + + + + + + + + + + + + + + +
{{ $t('_common.do_cmd') }} {{ $t('_common.undo_cmd') }} + {{ $t('_common.run_as') }} +
+ + + + +
+ + +
+
+ + +
-
- + +
+ +
+ + +
+
+ +
+
+ {{ $t('apps.detached_cmds_desc') }}
+ {{ $t('_common.note') }} {{ $t('apps.detached_cmds_note') }} +
- - - - - - - - - - - - - - - - - -
Do Command Undo Command - Run as Admin -
- - - - -
- - -
-
- - -
-
- -
- -
-
{{c}}
- + +
+ + +
+ {{ $t('apps.cmd_desc') }}
+ {{ $t('_common.note') }} {{ $t('apps.cmd_note') }} +
-
- - + +
+ + +
{{ $t('apps.working_dir_desc') }}
-
- A list of commands to be run and forgotten about + +
+ + +
{{ $t('apps.run_as_desc') }}
-
- -
- - -
- The main application, if it is not specified, a processs is started - that sleeps indefinitely + +
+ + +
{{ $t('apps.auto_detach_desc') }}
-
- -
- - -
- The working directory that should be passed to the process. For - example, some applications use the working directory to search for - configuration files. If not set, Sunshine will default to the parent - directory of the command + +
+ + +
{{ $t('apps.wait_all_desc') }}
-
- -
- - -
- This can be necessary for some applications that require administrator - permissions to run properly. + +
+ + +
{{ $t('apps.exit_timeout_desc') }}
-
- -
- -
- - -