diff --git a/.codeql-prebuild-cpp.sh b/.codeql-prebuild-cpp.sh new file mode 100644 index 00000000000..ba4e4016e0e --- /dev/null +++ b/.codeql-prebuild-cpp.sh @@ -0,0 +1,55 @@ +# install dependencies for C++ analysis + +sudo apt-get update -y +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 \ + 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 \ + wget + +# 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 + +# 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 diff --git a/.dockerignore b/.dockerignore index 6ada538cff0..15191b977aa 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,6 +6,7 @@ # ignore repo directories and files docs/ +gh-pages-template/ scripts/ tools/ crowdin.yml 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/workflows/CI.yml b/.github/workflows/CI.yml index a2d7f99fb44..254a1940dd5 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -31,7 +31,7 @@ jobs: steps: - name: Checkout if: ${{ github.ref == 'refs/heads/master' || github.base_ref == 'master' }} - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Verify Changelog id: verify_changelog @@ -55,7 +55,7 @@ jobs: # base_ref for pull request check, ref for push steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Check CMakeLists.txt Version run: | @@ -157,8 +157,24 @@ jobs: matrix: ${{fromJson(needs.setup_flatpak_matrix.outputs.matrix)}} steps: + - name: Maximize build space + uses: easimon/maximize-build-space@v8 + with: + root-reserve-mb: 10240 + remove-dotnet: 'true' + remove-android: 'true' + remove-haskell: 'true' + remove-codeql: 'true' + remove-docker-images: 'false' + - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 + + - name: Checkout Flathub Shared Modules + uses: actions/checkout@v4 + with: + repository: flathub/shared-modules + path: build/shared-modules - name: Setup Dependencies Linux Flatpak run: | @@ -176,6 +192,7 @@ jobs: 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 @@ -254,20 +271,30 @@ jobs: prerelease: ${{ needs.setup_release.outputs.pre_release }} build_linux: - name: Linux + name: Linux ${{ matrix.type }} runs-on: ubuntu-${{ matrix.dist }} needs: [check_changelog, 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' + - type: AppImage + EXTRA_ARGS: '-DSUNSHINE_BUILD_APPIMAGE=ON' dist: 20.04 steps: + - name: Maximize build space + uses: easimon/maximize-build-space@v8 + with: + root-reserve-mb: 20480 + remove-dotnet: 'true' + remove-android: 'true' + remove-haskell: 'true' + remove-codeql: 'true' + remove-docker-images: 'false' + - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: recursive @@ -285,7 +312,6 @@ jobs: libboost-locale1.71-dev \ libboost-log1.71-dev \ libboost-regex1.71-dev \ - libboost-thread1.71-dev \ libboost-program-options1.71-dev # Install cmake @@ -311,7 +337,6 @@ jobs: libboost-filesystem-dev \ libboost-locale-dev \ libboost-log-dev \ - libboost-thread-dev \ libboost-program-options-dev fi @@ -319,13 +344,14 @@ jobs: build-essential \ gcc-10 \ g++-10 \ - libappindicator3-dev \ + libayatana-appindicator3-dev \ libavdevice-dev \ libcap-dev \ libcurl4-openssl-dev \ libdrm-dev \ libevdev-dev \ libmfx-dev \ + libnotify-dev \ libnuma-dev \ libopus-dev \ libpulse-dev \ @@ -388,6 +414,7 @@ jobs: make -j ${nproc} - name: Package Linux - CPACK + # todo - this is no longer used if: ${{ matrix.type == 'cpack' }} working-directory: build run: | @@ -400,13 +427,13 @@ jobs: fi - 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' && ( needs.check_changelog.outputs.next_version_bare != needs.check_changelog.outputs.last_version ) }} # yamllint disable-line rule:line-length run: | version=${{ needs.check_changelog.outputs.next_version_bare }} 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 @@ -425,14 +452,18 @@ jobs: wget 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 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 @@ -442,7 +473,7 @@ jobs: chmod +x ../artifacts/sunshine.AppImage - 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 @@ -478,7 +509,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: recursive @@ -546,17 +577,17 @@ jobs: 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 @@ -616,104 +647,33 @@ 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" - - - name: Build subports + port -q lint "Sunshine" + + - name: Build port env: subportlist: ${{ steps.subportlist.outputs.subportlist }} 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: Upload Artifacts uses: actions/upload-artifact@v3 @@ -742,7 +702,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: recursive @@ -759,7 +719,7 @@ jobs: mingw-w64-x86_64-boost mingw-w64-x86_64-cmake mingw-w64-x86_64-curl - mingw-w64-x86_64-libmfx + mingw-w64-x86_64-onevpl mingw-w64-x86_64-nsis mingw-w64-x86_64-openssl mingw-w64-x86_64-opus @@ -833,8 +793,11 @@ jobs: 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 + if: | + (github.repository_owner == 'LizardByte' && + needs.setup_release.outputs.create_release == 'true' && + github.ref == 'refs/heads/master') + runs-on: ubuntu-latest steps: - name: Release to WinGet uses: vedantmgoyal2009/winget-releaser@v2 diff --git a/.github/workflows/auto-create-pr.yml b/.github/workflows/auto-create-pr.yml index 811747c6517..13705dd539c 100644 --- a/.github/workflows/auto-create-pr.yml +++ b/.github/workflows/auto-create-pr.yml @@ -20,7 +20,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Create Pull Request uses: repo-sync/pull-request@v2 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..edeeb2bd9b7 100644 --- a/.github/workflows/ci-docker.yml +++ b/.github/workflows/ci-docker.yml @@ -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 @@ -86,7 +86,7 @@ jobs: steps: - name: Checkout if: ${{ github.ref == 'refs/heads/master' || github.base_ref == 'master' }} - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Verify Changelog id: verify_changelog @@ -162,7 +162,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Hadolint id: hadolint @@ -192,8 +192,18 @@ jobs: name: Docker${{ matrix.tag }} steps: + - name: Maximize build space + uses: easimon/maximize-build-space@v8 + 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 @@ -299,10 +309,10 @@ jobs: 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 @@ -315,14 +325,14 @@ jobs: - name: Log in to Docker Hub if: ${{ steps.prepare.outputs.push == 'true' }} # PRs do not have access to secrets - uses: docker/login-action@v2 + 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 + uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ secrets.GH_BOT_NAME }} @@ -331,7 +341,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 }} @@ -353,7 +363,7 @@ jobs: - name: Build and push id: build - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: context: ./ file: ${{ matrix.dockerfile }} diff --git a/.github/workflows/ci-qodana.yml b/.github/workflows/ci-qodana.yml index 91feb59fae8..efc56349b30 100644 --- a/.github/workflows/ci-qodana.yml +++ b/.github/workflows/ci-qodana.yml @@ -25,7 +25,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Prepare id: prepare @@ -165,7 +165,7 @@ jobs: continue-on-error: true steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: recursive @@ -214,7 +214,7 @@ jobs: - name: Qodana id: qodana continue-on-error: true # ensure dispatch-qodana job is run - uses: JetBrains/qodana-action@v2022.3.4 + uses: JetBrains/qodana-action@v2023.2.6 with: additional-cache-hash: ${{ github.ref }}-${{ matrix.language }} artifact-name: qodana-${{ matrix.language }} # yamllint disable-line rule:line-length diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000000..95349bbc477 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,147 @@ +--- +# 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", "nightly"] + pull_request: + branches: ["master", "nightly"] + 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@v6 + 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()) && + !matrix['include'].includes({"language": key.toLowerCase()})) { + console.log(`Found supported language: ${key}`) + matrix['include'].push({"language": key.toLowerCase()}) + } + } + + // print languages + console.log(`matrix: ${JSON.stringify(matrix)}`) + + return matrix + + - name: Continue + uses: actions/github-script@v6 + 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 + if: ${{ needs.languages.outputs.continue == 'true' }} + needs: [languages] + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || '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 + uses: easimon/maximize-build-space@v8 + with: + root-reserve-mb: 20480 + 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 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + 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 + + # Pre autobuild + # create a file named .codeql-prebuild-${{ matrix.language }}.sh in the root of your repository + - name: Prebuild + run: | + # check if .qodeql-prebuild-${{ matrix.language }}.sh exists + if [ -f "./.codeql-prebuild-${{ matrix.language }}.sh" ]; then + echo "Running .codeql-prebuild-${{ matrix.language }}.sh" + ./.codeql-prebuild-${{ matrix.language }}.sh + fi + + # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/cpp-lint.yml b/.github/workflows/cpp-lint.yml index fb1bf642dea..86a2b8d686b 100644 --- a/.github/workflows/cpp-lint.yml +++ b/.github/workflows/cpp-lint.yml @@ -23,25 +23,43 @@ 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.16.2 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 @@ -50,7 +68,7 @@ jobs: uses: actions/upload-artifact@v3 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,7 +76,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v4 @@ -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/issues-stale.yml b/.github/workflows/issues-stale.yml index c168034ee3b..aecc8243f34 100644 --- a/.github/workflows/issues-stale.yml +++ b/.github/workflows/issues-stale.yml @@ -31,12 +31,15 @@ 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 @@ -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/localize.yml b/.github/workflows/localize.yml index d2a236c54e5..eb7e77974a2 100644 --- a/.github/workflows/localize.yml +++ b/.github/workflows/localize.yml @@ -20,7 +20,7 @@ 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 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..4b0d30810da 100644 --- a/.github/workflows/python-flake8.yml +++ b/.github/workflows/python-flake8.yml @@ -21,7 +21,7 @@ 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 diff --git a/.github/workflows/release-notifier.yml b/.github/workflows/release-notifier.yml index ed7b3ef20ff..5161fc5f5d5 100644 --- a/.github/workflows/release-notifier.yml +++ b/.github/workflows/release-notifier.yml @@ -14,11 +14,14 @@ on: jobs: discord: - if: startsWith(github.repository, 'LizardByte/') + if: >- + (github.repository, 'LizardByte/') and + not(github.event.release.prerelease) and + not(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: >- + (github.repository, 'LizardByte/') and + not(github.event.release.prerelease) and + not(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: >- + (github.repository, 'LizardByte/') and + not(github.event.release.prerelease) and + not(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: >- + (github.repository, 'LizardByte/') and + not(github.event.release.prerelease) and + not(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: >- + (github.repository, 'LizardByte/') and + not(github.event.release.prerelease) and + not(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-pages.yml b/.github/workflows/update-pages.yml new file mode 100644 index 00000000000..5fbbf97f9f4 --- /dev/null +++ b/.github/workflows/update-pages.yml @@ -0,0 +1,62 @@ +--- +name: Build GH-Pages + +on: + pull_request: + branches: [master, nightly] + 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@v3 + 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.4 + 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/yaml-lint.yml b/.github/workflows/yaml-lint.yml index 6327d5d6326..7e1fd469409 100644 --- a/.github/workflows/yaml-lint.yml +++ b/.github/workflows/yaml-lint.yml @@ -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/.gitmodules b/.gitmodules index a3e39729c0a..92820c24c47 100644 --- a/.gitmodules +++ b/.gitmodules @@ -8,7 +8,7 @@ branch = master [submodule "third-party/ViGEmClient"] path = third-party/ViGEmClient - url = https://github.com/ViGEm/ViGEmClient + url = https://github.com/nefarius/ViGEmClient branch = master [submodule "third-party/miniupnp"] path = third-party/miniupnp @@ -17,7 +17,7 @@ [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/TPCircularBuffer"] path = third-party/TPCircularBuffer url = https://github.com/michaeltyson/TPCircularBuffer @@ -48,5 +48,21 @@ 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/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/ffmpeg-linux-powerpc64le"] + path = third-party/ffmpeg-linux-powerpc64le + url = https://github.com/LizardByte/build-deps + branch = ffmpeg-linux-powerpc64le +[submodule "third-party/wayland-protocols"] + path = third-party/wayland-protocols + url = https://gitlab.freedesktop.org/wayland/wayland-protocols + branch = main +[submodule "third-party/wlr-protocols"] + path = third-party/wlr-protocols + url = https://gitlab.freedesktop.org/wlroots/wlr-protocols 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 index 419e16025e0..bbeb4028382 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,106 @@ # Changelog +## [0.21.0] - 2023-10-15 +**Added** +- (Input) Add support for automatically selecting the emulated controller type based on the physical controller connected to the client +- (Input/Windows) Add support for Applications (context menu) key +- (Input/Windows) Implement touchpad, motion sensors, battery state, and LED control for the emulated DualShock 4 controller +- (Input) Advertise support for new input features to clients +- (Linux/Debian) Added Debian Bookworm package +- (Prep-Commands) Expose connection environment variables +- (Input/Windows) Implement pen and touch support +- (Capture/Windows) Add standalone NVENC encoder +- (Capture) Implement AV1 encoding +- (Network) Implement IPv6 support +- (Capture/Windows) Add option to disable realtime hags +- (Graphics/NVIDIA) Add an option to decrease GPU scheduling priority to workaround HAGS video hang +- (Capture/Linux) Add FFmpeg powerpc64le architecture for self compiling Sunshine +- (Capture/Windows) Add support for capturing rotated displays +- (System Tray) Implement streaming event notifications +- (UI) Add port configuration table +- (Applications) Added option to automatically treat launcher type apps as detached commands +- (Input/Gamepad) Allow the Misc button to work as Guide on emulated Xbox 360 controllers + +**Changed** +- (Input) Reduce latency by implementing input batching +- (Logging) Move input packet debug prints off the control stream thread +- (Input) Refactor gamepad emulation code to use DS4 extended report format +- (Graphics/NVIDIA) Modify and restore NVIDIA control panel settings before and after stream, respectively +- (Graphics/NVIDIA) New config page for NVENC +- (Graphics/Windows) Refactor DX shaders +- (Input/Windows) Use our own keycode mapping to avoid installing the US English keyboard layout + +**Fixed** +- (UI) Fix update notifications +- (Dependencies/Linux) Replace libboost chrono and thread with standard chrono and thread +- (Input) Increase maximum gamepad limit to 16 +- (Network) Allow use of multiple ENet channels +- (Network) Consider link-local addresses on LAN +- (Input) Fixed issue where button may sometimes stick on Windows +- (Input) Fix "ControllerNumber not allocated" warning when a gamepad is removed +- (Input) Fix handling of gamepad feedback with multiple clients connected +- (Input) Fix clamping mouse position to aspect ratio adjusted viewport +- (Graphics/AMD) Fix crash during startup on some older AMD GPUs +- (Logging) Fix crash when non-ASCII characters are logged +- (Prep-Commands) Fix resource exhaustion bug which could occur when many prep commands were used +- (Subprocesses) Fix race condition when inserting new processes +- (Logging) Log error if encoder doesn't produce IDR frame on demand +- (Audio) Improve audio capture logic and logging +- (Logging) Fix AMF logging to match configured log level +- (Logging) Log FFmpeg to log file instead of stdout +- (Capture) Reject codecs that are not supported by display device +- (Capture) Add fallbacks for unsupported codec settings +- (Capture) Avoid probing HEVC or AV1 codecs in some cases +- (Caputre) Remove DwmFlush() +- (Capture/Windows) Improvements to capture sleeps for better frame stability +- (Capture/Windows) Adjust capture rate to better match with display +- (Linux/ArchLinux) Fix package version in PKGBUILD and precompiled package +- (UI) Highlight fatal log messages in web ui +- (Commands) Allow stream if prep command fails +- (Capture/Linux) Fix KMS grab VRAM capture with libva 2.20 +- (Capture/macOS) Fix video capture backend +- (Misc/Windows) Don't start the session monitor window when launched in command mode +- (Linux/AppImage) Use the linuxdeploy GTK plugin to correctly deploy GTK3 dependencies +- (Input/Windows) Fix reWASD not recognizing emulated DualShock 4 input + +**Dependencies** +- Bump bootstrap from 5.2.3 to 5.3.2 +- Bump third-party/moonlight-common-c from c9426a6 to 7a6d12f +- Bump gcc-10 in Ubuntu 20.04 docker image +- Bump furo from 2023.5.20 to 2023.9.10 +- Bump sphinx from 7.0.1 to 7.2.6 +- Bump cmake from 3.26 to 3.27 in Fedora docker images +- Move third-party/nv-codec-headers from sdk/11.1 branch to sdk/12.0 branch +- Automatic bump ffmpeg +- Bump actions/checkout from 3 to 4 +- Bump boost from 1.80 to 1.81 in Macport manifest +- Bump @fortawesome/fontawesome-free from 6.4.0 to 6.4.2 + +**Misc** +- (Docs) Force badges to use svg +- (Docs) Add Linux SSH example +- (Docs) Add information about mesa for Linux +- (CI) Free additional space on Docker, Flatpak, and AppImage builds due to internal changes on GitHub runners +- (Docs/Logging/UI) Corrected various typos +- (Docs) Add blurb about Gamescope compatibility +- (Installer/Windows) Use system proxy to download ViGEmBus +- (CI) Ignore third-party directory for clang-format +- (Docs/Linux) Add Plasma-Compatible resolution example +- (Docs) Add Sunshine website available at https://app.lizardbyte.dev/Sunshine +- (Build/Windows) Fix audio code build with new MinGW headers +- (Build/Windows) Fix QoS code build with new MinGW headers +- (CI/Windows) Prevent winget action from creating an update when running in a fork +- (CI/Windows) Change winget job to ubuntu-latest runner +- (CI) Add CodeQL analysis +- (CI/Docker) Fix ArchLinux image caching issue +- (Windows) Manifest improvements +- (CI/macOS) Simplify macport build +- (Docs) Remove deprecated options from readthedocs config +- (CI/Docs) Lint rst files +- (Docs) Update localization information (after consolidating Crowdin projects) +- (Cmake) Split CMakelists into modules +- (Docs) Add Linux Headless/SSH Guide + ## [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. @@ -490,3 +591,4 @@ settings. In v0.17.0, games now run under your user account without elevated pri [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 +[0.21.0]: https://github.com/LizardByte/Sunshine/releases/tag/v0.21.0 diff --git a/CMakeLists.txt b/CMakeLists.txt index ccca6fcec60..9c21b380ab9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,959 +1,55 @@ cmake_minimum_required(VERSION 3.18) # `CMAKE_CUDA_ARCHITECTURES` 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 +project(Sunshine VERSION 0.21.0 DESCRIPTION "Sunshine is a self-hosted game stream host for Moonlight." - HOMEPAGE_URL "https://app.lizardbyte.dev") + 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 the module path, used for includes 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") +# set version info for this build +include(${CMAKE_MODULE_PATH}/prep/build_version.cmake) - 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) +# cmake build flags +include(${CMAKE_MODULE_PATH}/prep/options.cmake) - 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 special package files, such as sunshine.desktop, Flatpak manifest, Portfile , etc. +include(${CMAKE_MODULE_PATH}/prep/special_package_configuration.cmake) -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: - ") - - # 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: - ") - - # 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/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") - - # 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/README.rst b/README.rst index 0a8d1eb8c91..41ae6d8488e 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 ----- @@ -92,34 +92,34 @@ Integrations :alt: GitHub Workflow Status (localize) :target: https://github.com/LizardByte/Sunshine/actions/workflows/localize.yml?query=branch%3Anightly -.. 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 - 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/xml.svg?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 + 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 diff --git a/cmake/compile_definitions/common.cmake b/cmake/compile_definitions/common.cmake new file mode 100644 index 00000000000..f10b2006cf6 --- /dev/null +++ b/cmake/compile_definitions/common.cmake @@ -0,0 +1,129 @@ +# common compile definitions +# this file will also load platform specific definitions + +list(APPEND SUNSHINE_COMPILE_OPTIONS -Wall -Wno-sign-compare) +# Wall - enable all warnings +# Wno-sign-compare - disable warnings for signed/unsigned comparisons + +# 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 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(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/video_colorspace.cpp + src/video_colorspace.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") + +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_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() + +list(APPEND SUNSHINE_EXTERNAL_LIBRARIES + libminiupnpc-static + ${CMAKE_THREAD_LIBS_INIT} + enet + opus + ${FFMPEG_LIBRARIES} + ${Boost_LIBRARIES} + ${OPENSSL_LIBRARIES} + ${CURL_LIBRARIES} + ${PLATFORM_LIBRARIES}) diff --git a/cmake/compile_definitions/linux.cmake b/cmake/compile_definitions/linux.cmake new file mode 100644 index 00000000000..f28152ee9bb --- /dev/null +++ b/cmake/compile_definitions/linux.cmake @@ -0,0 +1,227 @@ +# 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() + +if(NOT DEFINED SUNSHINE_EXECUTABLE_PATH) + set(SUNSHINE_EXECUTABLE_PATH "sunshine") +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 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() + +# 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 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() + +# wayland +if(${SUNSHINE_ENABLE_WAYLAND}) + find_package(Wayland) +else() + set(WAYLAND_FOUND OFF) +endif() +if(WAYLAND_FOUND) + add_compile_definitions(SUNSHINE_BUILD_WAYLAND) + + GEN_WAYLAND("wayland-protocols" "unstable/xdg-output" xdg-output-unstable-v1) + GEN_WAYLAND("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 + src/platform/linux/wlgrab.cpp + 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 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})) + 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(APPINDICATOR_FOUND) + list(APPEND SUNSHINE_DEFINITIONS TRAY_LEGACY_APPINDICATOR=1) + else() + pkg_check_modules(APPINDICATOR ayatana-appindicator3-0.1) + if(APPINDICATOR_FOUND) + list(APPEND SUNSHINE_DEFINITIONS TRAY_AYATANA_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 third-party/tray/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 + 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) diff --git a/cmake/compile_definitions/macos.cmake b/cmake/compile_definitions/macos.cmake new file mode 100644 index 00000000000..3bcf9528361 --- /dev/null +++ b/cmake/compile_definitions/macos.cmake @@ -0,0 +1,49 @@ +# macos specific compile definitions + +add_compile_definitions(SUNSHINE_PLATFORM="macos") + +link_directories(/opt/local/lib) +link_directories(/usr/local/lib) +link_directories(/opt/homebrew/lib) +ADD_DEFINITIONS(-DBOOST_LOG_DYN_LINK) + +list(APPEND SUNSHINE_EXTERNAL_LIBRARIES + ${APP_SERVICES_LIBRARY} + ${AV_FOUNDATION_LIBRARY} + ${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) + +# todo - tray is not working on macos +set(SUNSHINE_TRAY 0) + +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 + ${APPLE_PLIST_FILE}) + +if(SUNSHINE_ENABLE_TRAY) + list(APPEND SUNSHINE_EXTERNAL_LIBRARIES + ${COCOA}) + list(APPEND PLATFORM_TARGET_FILES + third-party/tray/tray_darwin.m) +endif() diff --git a/cmake/compile_definitions/unix.cmake b/cmake/compile_definitions/unix.cmake new file mode 100644 index 00000000000..5232481ccfb --- /dev/null +++ b/cmake/compile_definitions/unix.cmake @@ -0,0 +1,9 @@ +# unix specific compile definitions +# put anything here that applies to both linux and macos + +list(APPEND SUNSHINE_EXTERNAL_LIBRARIES Boost::log) + +# 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..ec3e076f4de --- /dev/null +++ b/cmake/compile_definitions/windows.cmake @@ -0,0 +1,78 @@ +# 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") + +# curl +add_definitions(-DCURL_STATICLIB) +include_directories(SYSTEM ${CURL_STATIC_INCLUDE_DIRS}) +link_directories(${CURL_STATIC_LIBRARY_DIRS}) + +# 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 third-party/nvapi-open-source-sdk) +file(GLOB NVPREFS_FILES CONFIGURE_DEPENDS + "third-party/nvapi-open-source-sdk/*.h" + "src/platform/windows/nvprefs/*.cpp" + "src/platform/windows/nvprefs/*.h") + +# vigem +include_directories(SYSTEM third-party/ViGEmClient/include) +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") + +# sunshine icon +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/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 + ${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 + ${CURL_STATIC_LIBRARIES}) + +if(SUNSHINE_ENABLE_TRAY) + list(APPEND PLATFORM_TARGET_FILES + third-party/tray/tray_windows.c) +endif() diff --git a/cmake/dependencies/common.cmake b/cmake/dependencies/common.cmake new file mode 100644 index 00000000000..9bc7c56ceee --- /dev/null +++ b/cmake/dependencies/common.cmake @@ -0,0 +1,86 @@ +# 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(third-party/moonlight-common-c/enet) + +# web server +add_subdirectory(third-party/Simple-Web-Server) + +# miniupnp +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) + +# ffmpeg pre-compiled binaries +if(WIN32) + if(NOT CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64") + message(FATAL_ERROR "Unsupported system processor:" ${CMAKE_SYSTEM_PROCESSOR}) + endif() + set(FFMPEG_PLATFORM_LIBRARIES mfplat ole32 strmiids mfuuid vpl) + set(FFMPEG_PREPARED_BINARIES "${CMAKE_CURRENT_SOURCE_DIR}/third-party/ffmpeg-windows-x86_64") +elseif(APPLE) + if(CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64") + set(FFMPEG_PREPARED_BINARIES "${CMAKE_CURRENT_SOURCE_DIR}/third-party/ffmpeg-macos-x86_64") + elseif(CMAKE_SYSTEM_PROCESSOR STREQUAL "arm64") + set(FFMPEG_PREPARED_BINARIES "${CMAKE_CURRENT_SOURCE_DIR}/third-party/ffmpeg-macos-aarch64") + elseif(CMAKE_SYSTEM_PROCESSOR STREQUAL "powerpc") + message(FATAL_ERROR "PowerPC is not supported on macOS") + else() + message(FATAL_ERROR "Unsupported system processor:" ${CMAKE_SYSTEM_PROCESSOR}) + endif() +elseif(UNIX) + set(FFMPEG_PLATFORM_LIBRARIES va va-drm va-x11 vdpau X11) + if(CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64") + list(APPEND FFMPEG_PLATFORM_LIBRARIES mfx) + set(FFMPEG_PREPARED_BINARIES "${CMAKE_CURRENT_SOURCE_DIR}/third-party/ffmpeg-linux-x86_64") + set(CPACK_DEB_PLATFORM_PACKAGE_DEPENDS "libmfx1,") + set(CPACK_RPM_PLATFORM_PACKAGE_REQUIRES "intel-mediasdk >= 22.3.0,") + elseif(CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64") + set(FFMPEG_PREPARED_BINARIES "${CMAKE_CURRENT_SOURCE_DIR}/third-party/ffmpeg-linux-aarch64") + elseif(CMAKE_SYSTEM_PROCESSOR STREQUAL "ppc64le" OR CMAKE_SYSTEM_PROCESSOR STREQUAL "ppc64") + set(FFMPEG_PREPARED_BINARIES "${CMAKE_CURRENT_SOURCE_DIR}/third-party/ffmpeg-linux-powerpc64le") + else() + message(FATAL_ERROR "Unsupported system processor:" ${CMAKE_SYSTEM_PROCESSOR}) + 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}) + +# common dependencies +find_package(OpenSSL REQUIRED) +find_package(PkgConfig REQUIRED) +find_package(Threads REQUIRED) +pkg_check_modules(CURL REQUIRED libcurl) + +# 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..7d8e211e242 --- /dev/null +++ b/cmake/dependencies/macos.cmake @@ -0,0 +1,12 @@ +# macos specific dependencies + +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..3ce9a9da234 --- /dev/null +++ b/cmake/dependencies/windows.cmake @@ -0,0 +1,6 @@ +# windows specific dependencies + +set(Boost_USE_STATIC_LIBS ON) # cmake-lint: disable=C0103 +# Boost >= 1.82.0 is required for boost::json::value::set_at_pointer() support +# todo - are we actually using json? I think this was attempted to be used in a PR, but we ended up not using json +find_package(Boost 1.82.0 COMPONENTS locale log filesystem program_options json REQUIRED) 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..d84d0452568 --- /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 \ +${CMAKE_SOURCE_DIR}/third-party/${wayland_directory}/${subdirectory}/${filename}.xml \ +${CMAKE_BINARY_DIR}/generated-src/${filename}.c") + message("wayland-scanner client-header \ +${CMAKE_SOURCE_DIR}/third-party/${wayland_directory}/${subdirectory}/${filename}.xml \ +${CMAKE_BINARY_DIR}/generated-src/${filename}.h") + execute_process( + COMMAND wayland-scanner private-code + ${CMAKE_SOURCE_DIR}/third-party/${wayland_directory}/${subdirectory}/${filename}.xml + ${CMAKE_BINARY_DIR}/generated-src/${filename}.c + COMMAND wayland-scanner client-header + ${CMAKE_SOURCE_DIR}/third-party/${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..0b41524f6cb --- /dev/null +++ b/cmake/packaging/common.cmake @@ -0,0 +1,32 @@ +# 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 npm modules +install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/node_modules" + DESTINATION "${SUNSHINE_ASSETS_DIR}/web") + +# 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..aee40cb7f1b --- /dev/null +++ b/cmake/packaging/linux.cmake @@ -0,0 +1,100 @@ +# linux specific packaging + +install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/linux/assets/" + DESTINATION "${SUNSHINE_ASSETS_DIR}") +if(${SUNSHINE_BUILD_APPIMAGE} OR ${SUNSHINE_BUILD_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() + +# 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, \ + 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, \ + 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_PREFIX}/share/icons/hicolor/scalable/apps") + +# tray icon +if(${SUNSHINE_TRAY} STREQUAL 1) + install(FILES "${CMAKE_SOURCE_DIR}/sunshine.svg" + DESTINATION "${CMAKE_INSTALL_PREFIX}/share/icons/hicolor/scalable/status" + RENAME "sunshine-tray.svg") + install(FILES "${SUNSHINE_SOURCE_ASSETS_DIR}/common/assets/web/images/sunshine-playing.svg" + DESTINATION "${CMAKE_INSTALL_PREFIX}/share/icons/hicolor/scalable/status") + install(FILES "${SUNSHINE_SOURCE_ASSETS_DIR}/common/assets/web/images/sunshine-pausing.svg" + DESTINATION "${CMAKE_INSTALL_PREFIX}/share/icons/hicolor/scalable/status") + install(FILES "${SUNSHINE_SOURCE_ASSETS_DIR}/common/assets/web/images/sunshine-locked.svg" + DESTINATION "${CMAKE_INSTALL_PREFIX}/share/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_PREFIX}/share/applications") +if(${SUNSHINE_BUILD_FLATPAK}) + install(FILES "${CMAKE_CURRENT_BINARY_DIR}/sunshine_kms.desktop" + DESTINATION "${CMAKE_INSTALL_PREFIX}/share/applications") +endif() + +# metadata file +# todo - validate file with `appstream-util validate-relax` +install(FILES "${CMAKE_CURRENT_BINARY_DIR}/sunshine.appdata.xml" + DESTINATION "${CMAKE_INSTALL_PREFIX}/share/metainfo") diff --git a/cmake/packaging/macos.cmake b/cmake/packaging/macos.cmake new file mode 100644 index 00000000000..2b173393bbf --- /dev/null +++ b/cmake/packaging/macos.cmake @@ -0,0 +1,26 @@ +# 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(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) +else() + 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}") +endif() diff --git a/cmake/packaging/unix.cmake b/cmake/packaging/unix.cmake new file mode 100644 index 00000000000..660811f3b33 --- /dev/null +++ b/cmake/packaging/unix.cmake @@ -0,0 +1,18 @@ +# 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}") + +install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/common/assets/" + DESTINATION "${SUNSHINE_ASSETS_DIR}") diff --git a/cmake/packaging/windows.cmake b/cmake/packaging/windows.cmake new file mode 100644 index 00000000000..0d4f1dd9cce --- /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}/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-gamepad.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/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..49f85f9b585 --- /dev/null +++ b/cmake/prep/build_version.cmake @@ -0,0 +1,58 @@ +# 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() diff --git a/cmake/prep/constants.cmake b/cmake/prep/constants.cmake new file mode 100644 index 00000000000..4f7a9e48aeb --- /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_CURRENT_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/options.cmake b/cmake/prep/options.cmake new file mode 100644 index 00000000000..1a216d263c8 --- /dev/null +++ b/cmake/prep/options.cmake @@ -0,0 +1,31 @@ +# 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) + +if(APPLE) + 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_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..cd285a65acc --- /dev/null +++ b/cmake/prep/special_package_configuration.cmake @@ -0,0 +1,40 @@ +if (APPLE) + if(${SUNSHINE_CONFIGURE_PORTFILE}) + configure_file(packaging/macos/Portfile Portfile @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) + else() + configure_file(packaging/linux/sunshine.desktop sunshine.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) + endif() + + # configure the flatpak manifest + if(${SUNSHINE_CONFIGURE_FLATPAK_MAN}) + configure_file(packaging/linux/flatpak/dev.lizardbyte.sunshine.yml dev.lizardbyte.sunshine.yml @ONLY) + 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..72f89bf5182 --- /dev/null +++ b/cmake/targets/common.cmake @@ -0,0 +1,35 @@ +# 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 17 + VERSION ${PROJECT_VERSION} + SOVERSION ${PROJECT_VERSION_MAJOR}) + +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 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..341d7c2e74e --- /dev/null +++ b/cmake/targets/windows.cmake @@ -0,0 +1,6 @@ +# 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 + Wtsapi32.lib) diff --git a/docker/archlinux.dockerfile b/docker/archlinux.dockerfile index 5ff4a4ab0b8..15cb5d23685 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 @@ -38,7 +38,7 @@ SHELL ["/bin/bash", "-o", "pipefail", "-c"] RUN <<_DEPS #!/bin/bash set -e -pacman -Syu --disable-download-timeout --noconfirm \ +pacman -Syu --disable-download-timeout --needed --noconfirm \ base-devel \ cmake \ cuda \ @@ -68,9 +68,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 @@ -102,7 +104,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/debian-bookworm.dockerfile b/docker/debian-bookworm.dockerfile new file mode 100644 index 00000000000..1172d35a5e9 --- /dev/null +++ b/docker/debian-bookworm.dockerfile @@ -0,0 +1,175 @@ +# 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.* \ + git \ + libavdevice-dev \ + 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 \ + 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 \ + wget +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 npm dependencies +RUN npm install + +# setup build directory +WORKDIR /build/sunshine/build + +# cmake and cpack +RUN <<_MAKE +#!/bin/bash +set -e +cmake \ + -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 + +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..6d7ebdd30af 100644 --- a/docker/debian-bullseye.dockerfile +++ b/docker/debian-bullseye.dockerfile @@ -34,15 +34,16 @@ apt-get install -y --no-install-recommends \ cmake=3.18.* \ git \ libavdevice-dev \ + 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 \ + libnotify-dev \ libnuma-dev \ libopus-dev \ libpulse-dev \ diff --git a/docker/fedora-37.dockerfile b/docker/fedora-37.dockerfile index b05f8cb1c21..c6aa0593a6b 100644 --- a/docker/fedora-37.dockerfile +++ b/docker/fedora-37.dockerfile @@ -31,7 +31,7 @@ dnf -y update dnf -y group install "Development Tools" dnf -y install \ boost-devel-1.78.* \ - cmake-3.26.* \ + cmake-3.27.* \ gcc-12.2.* \ gcc-c++-12.2.* \ git \ @@ -40,6 +40,7 @@ dnf -y install \ libcurl-devel \ libdrm-devel \ libevdev-devel \ + libnotify-devel \ libva-devel \ libvdpau-devel \ libX11-devel \ diff --git a/docker/fedora-38.dockerfile b/docker/fedora-38.dockerfile index 9635f192cc7..2245ba9b9aa 100644 --- a/docker/fedora-38.dockerfile +++ b/docker/fedora-38.dockerfile @@ -31,15 +31,16 @@ 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.* \ + cmake-3.27.* \ + gcc-13.2.* \ + gcc-c++-13.2.* \ git \ libappindicator-gtk3-devel \ libcap-devel \ libcurl-devel \ libdrm-devel \ libevdev-devel \ + libnotify-devel \ libva-devel \ libvdpau-devel \ libX11-devel \ diff --git a/docker/ubuntu-20.04.dockerfile b/docker/ubuntu-20.04.dockerfile index 5689ba7134b..5634d48e0c0 100644 --- a/docker/ubuntu-20.04.dockerfile +++ b/docker/ubuntu-20.04.dockerfile @@ -31,20 +31,20 @@ set -e apt-get update -y apt-get install -y --no-install-recommends \ build-essential \ - gcc-10=10.3.* \ - g++-10=10.3.* \ + gcc-10=10.5.* \ + g++-10=10.5.* \ git \ - libappindicator3-dev \ + libayatana-appindicator3-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.* \ libcap-dev \ libcurl4-openssl-dev \ libdrm-dev \ libevdev-dev \ + libnotify-dev \ libnuma-dev \ libopus-dev \ libpulse-dev \ @@ -102,7 +102,7 @@ url="${cmake_prefix}${CMAKE_VERSION}/cmake-${CMAKE_VERSION}-linux-${cmake_arch}. 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 +rm ./cmake.sh _INSTALL_CMAKE # install cuda diff --git a/docker/ubuntu-22.04.dockerfile b/docker/ubuntu-22.04.dockerfile index 143fc1e4b24..c9c6b70db89 100644 --- a/docker/ubuntu-22.04.dockerfile +++ b/docker/ubuntu-22.04.dockerfile @@ -33,17 +33,17 @@ apt-get install -y --no-install-recommends \ build-essential \ cmake=3.22.* \ git \ - libappindicator3-dev \ + libayatana-appindicator3-dev \ libavdevice-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 \ + libnotify-dev \ libnuma-dev \ libopus-dev \ libpulse-dev \ diff --git a/docs/requirements.txt b/docs/requirements.txt index 7ce0be80b83..ecdb40c475f 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,5 +1,8 @@ breathe==4.35.0 -furo==2023.5.20 +furo==2023.9.10 m2r2==0.3.3.post2 -Sphinx==7.0.1 +rstcheck[sphinx]==6.2.0 +rstfmt==0.0.14 +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..e9bba6781f1 100644 --- a/docs/source/about/advanced_usage.rst +++ b/docs/source/about/advanced_usage.rst @@ -140,17 +140,18 @@ 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 = x360 + gamepad = auto back_button_timeout ^^^^^^^^^^^^^^^^^^^ @@ -225,7 +226,7 @@ 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. @@ -412,27 +413,6 @@ resolutions 3840x1600, ] -dwmflush -^^^^^^^^ - -**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. - - .. 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. - -**Default** - ``enabled`` - -**Example** - .. code-block:: text - - dwmflush = enabled - Audio ----- @@ -458,8 +438,8 @@ audio_sink **macOS** Sunshine can only access microphones on macOS due to system limitations. To stream system audio use - `Soundflower `_ or - `BlackHole `_. + `Soundflower `__ or + `BlackHole `__. **Windows** .. code-block:: batch @@ -505,7 +485,7 @@ virtual_sink - 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) + - `Virtual Audio Cable `__ (macOS, Windows) **Example** .. code-block:: text @@ -572,65 +552,65 @@ port **Default** ``47989`` +**Range** + ``1029-65514`` + **Example** .. code-block:: text port = 47989 -pkey -^^^^ +address_family +^^^^^^^^^^^^^^ **Description** - The private key. This must be 2048 bits. + Set the address family that Sunshine will use. + +.. table:: + :widths: auto + + ===== =========== + Value Description + ===== =========== + ipv4 IPv4 only + both IPv4+IPv6 + ===== =========== **Default** - ``credentials/cakey.pem`` + ``ipv4`` **Example** .. code-block:: text - pkey = /dir/pkey.pem + address_family = both -cert +pkey ^^^^ **Description** - The certificate. Must be signed with a 2048 bit key. + The private key. This must be 2048 bits. **Default** - ``credentials/cacert.pem`` + ``credentials/cakey.pem`` **Example** .. code-block:: text - cert = /dir/cert.pem + pkey = /dir/pkey.pem -origin_pin_allowed -^^^^^^^^^^^^^^^^^^ +cert +^^^^ **Description** - The origin of the remote endpoint address that is not denied for HTTP method /pin. - -**Choices** - -.. table:: - :widths: auto - - ===== =========== - Value Description - ===== =========== - pc Only localhost may access /pin - lan Only LAN devices may access /pin - wan Anyone may access /pin - ===== =========== + The certificate. Must be signed with a 2048 bit key. **Default** - ``pc`` + ``credentials/cacert.pem`` **Example** .. code-block:: text - origin_pin_allowed = pc + cert = /dir/cert.pem origin_web_ui_allowed ^^^^^^^^^^^^^^^^^^^^^ @@ -795,7 +775,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,6 +789,37 @@ hevc_mode hevc_mode = 2 +av1_mode +^^^^^^^^^ + +**Description** + Allows the client to request AV1 Main 8-bit or 10-bit video streams. + + .. Warning:: AV1 is more CPU-intensive to encode, so enabling this may reduce performance when using software + encoding. + +**Choices** + +.. table:: + :widths: auto + + ===== =========== + 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 ^^^^^^^ @@ -827,14 +838,14 @@ capture ========= =========== 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 `_. + `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 `. + See :ref:`Linux Setup `. x11 Uses XCB. This is the slowest and most CPU intensive so should be avoided if possible. ========= =========== - + **Default** Automatic. Sunshine will use the first capture method available in the order of the table above. @@ -842,7 +853,7 @@ capture .. code-block:: text capture = kms - + encoder ^^^^^^^ @@ -879,7 +890,7 @@ sw_preset .. Note:: This option only applies when using software `encoder`_. - .. Note:: From `FFmpeg `_. + .. 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 @@ -923,7 +934,7 @@ sw_tune .. Note:: This option only applies when using software `encoder`_. - .. Note:: From `FFmpeg `_. + .. Note:: From `FFmpeg `__. You can optionally use -tune to change settings based upon the specifics of your input. @@ -951,14 +962,15 @@ sw_tune sw_tune = zerolatency -nv_preset -^^^^^^^^^ +nvenc_preset +^^^^^^^^^^^^ **Description** - The encoder preset to use. + 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. - .. 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,60 +980,65 @@ 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) + 1 P1 (fastest) + 2 P2 + 3 P3 + 4 P4 + 5 P5 + 6 P6 + 7 P7 (slowest) ========== =========== **Default** - ``p4`` + ``1`` **Example** .. code-block:: text - nv_preset = p4 + nvenc_preset = 1 -nv_tune -^^^^^^^ +nvenc_twopass +^^^^^^^^^^^^^ **Description** - The encoder tuning profile. + 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. - .. Note:: This option only applies when using nvenc `encoder`_. + .. Note:: This option only applies when using NVENC `encoder`_. **Choices** .. table:: :widths: auto - ========== =========== - Value Description - ========== =========== - hq high quality - ll low latency - ull ultra low latency - lossless lossless - ========== =========== + =========== =========== + Value Description + =========== =========== + 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** - ``ull`` + ``quarter_res`` **Example** .. code-block:: text - nv_tune = ull + nvenc_twopass = quarter_res -nv_rc -^^^^^ +nvenc_realtime_hags +^^^^^^^^^^^^^^^^^^^ **Description** - The encoder rate control. + 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** @@ -1031,26 +1048,26 @@ nv_rc ========== =========== Value Description ========== =========== - constqp constant QP mode - vbr variable bitrate - cbr constant bitrate + disabled Use high priority + enabled Use realtime priority ========== =========== **Default** - ``cbr`` + ``enabled`` **Example** .. code-block:: text - nv_rc = cbr + nvenc_realtime_hags = enabled -nv_coder -^^^^^^^^ +nvenc_h264_cavlc +^^^^^^^^^^^^^^^^ **Description** - The entropy encoding to use. + 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 H264 with nvenc `encoder`_. + .. Note:: This option only applies when using H.264 format with NVENC `encoder`_. **Choices** @@ -1060,18 +1077,17 @@ 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 Prefer CABAC + enabled Prefer CAVLC ========== =========== **Default** - ``auto`` + ``disabled`` **Example** .. code-block:: text - nv_coder = auto + nvenc_h264_cavlc = disabled qsv_preset ^^^^^^^^^^ diff --git a/docs/source/about/app_examples.rst b/docs/source/about/guides/app_examples.rst similarity index 72% rename from docs/source/about/app_examples.rst rename to docs/source/about/guides/app_examples.rst index 0e39029797b..4cb45b7d5c1 100644 --- a/docs/source/about/app_examples.rst +++ b/docs/source/about/guides/app_examples.rst @@ -125,24 +125,77 @@ 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`` | -+----------------------+--------------------------------------------------------------+ ++----------------------+---------------------------------------------------------------------------------------------------------------------------------------+ +| **Field** | **Value** | ++----------------------+---------------------------------------------------------------------------------------------------------------------------------------+ +| 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 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`` | -+----------------------+-------------------------------------------------------------+ ++----------------------+-------------------------------------------------------------------------------------------------------------------------------------+ +| **Field** | **Value** | ++----------------------+-------------------------------------------------------------------------------------------------------------------------------------+ +| 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`` | ++----------------------+-------------------------------------------------------------------------------------------------------------------------------------+ + +Changing Resolution and Refresh Rate (Linux - KDE Plasma - Wayland and X11) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + ++----------------------+----------------------------------------------------------------------------------------------------------------------------------+ +| **Field** | **Value** | ++----------------------+----------------------------------------------------------------------------------------------------------------------------------+ +| 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`` | ++----------------------+----------------------------------------------------------------------------------------------------------------------------------+ Flatpak ^^^^^^^ @@ -158,7 +211,7 @@ 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 `_. + `GitHub repository `__. +----------------------+-----------------------------------------------------------------------------------------------+ | **Field** | **Value** | @@ -175,19 +228,15 @@ 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. - + This tool can be downloaded from their `SourceForge repository `__. + ++----------------------+------------------------------------------------------------------------------------------------------------------+ +| **Field** | **Value** | ++----------------------+------------------------------------------------------------------------------------------------------------------+ +| 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`` | ++----------------------+------------------------------------------------------------------------------------------------------------------+ Elevating Commands (Windows) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/source/about/guides/guides.rst b/docs/source/about/guides/guides.rst new file mode 100644 index 00000000000..3bda39ede6d --- /dev/null +++ b/docs/source/about/guides/guides.rst @@ -0,0 +1,10 @@ +Guides +====== + +Collection of guides written by the community! + +.. toctree:: + :maxdepth: 2 + + app_examples + linux/linux 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..1a4e9439b3e --- /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 precondtions 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/linux/linux.rst b/docs/source/about/guides/linux/linux.rst new file mode 100644 index 00000000000..4af2a8c1aa2 --- /dev/null +++ b/docs/source/about/guides/linux/linux.rst @@ -0,0 +1,9 @@ +Linux +====== + +Collection of Sunshine Linux host guides. + +.. toctree:: + :maxdepth: 1 + + headless_ssh diff --git a/docs/source/about/installation.rst b/docs/source/about/installation.rst index 323d78f4610..3d20a020b30 100644 --- a/docs/source/about/installation.rst +++ b/docs/source/about/installation.rst @@ -27,7 +27,7 @@ Follow the instructions for your preferred package type below. CUDA is used for NVFBC capture. -.. Tip:: See `CUDA GPUS `_ to cross reference Compute Capability to your GPU. +.. Tip:: See `CUDA GPUS `__ to cross reference Compute Capability to your GPU. .. table:: :widths: auto @@ -36,14 +36,15 @@ CUDA is used for NVFBC capture. 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.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-bullseye-{arch}.deb 11.8.0 450.80.02 50;52;60;61;62;70;75;80;86;90;35 + 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-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 + sunshine-ubuntu-20.04-{arch}.deb 11.8.0 450.80.02 35;50;52;60;61;62;70;75;80;86;90 + sunshine-ubuntu-22.04-{arch}.deb 11.8.0 450.80.02 35;50;52;60;61;62;70;75;80;86;90 =========================================== ============== ============== ================================ AppImage @@ -127,7 +128,7 @@ Uninstall: Flatpak Package ^^^^^^^^^^^^^^^ -#. Install `Flatpak `_ as required. +#. 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. @@ -204,7 +205,7 @@ Uninstall: Portfile ^^^^^^^^ -#. Install `MacPorts `_ +#. Install `MacPorts `__ #. Update the Macports sources. .. code-block:: bash @@ -243,7 +244,7 @@ Installer .. 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 +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 diff --git a/docs/source/about/third_party_packages.rst b/docs/source/about/third_party_packages.rst index fb921c9360a..69204dc266e 100644 --- a/docs/source/about/third_party_packages.rst +++ b/docs/source/about/third_party_packages.rst @@ -3,59 +3,57 @@ Third Party Packages .. Danger:: These packages are not maintained by LizardByte. Use at your own risk. +AOSC +---- + +.. image:: https://img.shields.io/badge/dynamic/xml.svg?color=orange&label=AOSC&style=for-the-badge&prefix=v&query=%2F%2Ftr%5B%40id%3D%27aosc%27%5D%2Ftd%5B3%5D%2Fspan%2Fa&url=https%3A%2F%2Frepology.org%2Fproject%2Fsunshine%2Fversions&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQEAAAEBCAMAAABPMuDPAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAMAUExURQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAP/AAABwwP/////LAMkAAI0AAIhnAP/CAP/JAP/IAMcAAMsAAAEAAMwAAAB3zMoAAAAIDsEAAMIAAP/MAABptP/GAMQAAAByxP/KAABxwsgAAABzxsYAAMMAAAB1yf/EAAgGAQwAAAgAAAUBAAB2ywB0x/m9AAECBCQAAC0AAFoAAB4WAL8AAKYAAKEAACoAAB8AABQPAJsAAAAKEvW6AK0AAGAAAPK4AGYAAFAAAIEAAMqaAI1rAJYAAL0AACkeAOCqAP/NADQnAOuzALYAAAAmQi4jAEAwALyOAEEAALaKADgAAFUAAHZZABsAAF1HAEgAALMAABoTABYAAOWvAABnsQBiqSMaAAAwU7GGAG9UAHcAAJRwANqlAJAAANKeAA8AAIgAAA4LAAAhOUg2AMaWAHxeADAAALAAAIoAAKkAAJ4AAFlEAGRLAAAZK6mAAAAGChMAAABSjc8AAABPhwBlrnIAALsAAGtQAD0AAABWlKF5AGoAADQAAPrBAEwAAAAOGJMAAAAWJQASIPj4+G4AAJx2AFE9AABeogAdM8KTAFVAAABsuQwMDBMTEycnJ6ampkw6AABFdqV9AAA8ZnwAAABJfoRjAABAbzkrALq6unV1deXl5a6urr+/v4QAAAAsTABbnABuvNakADAwMHx8fJKSkh4eHjw8PIYAANCdAAA1W52dnUpKSsjIyNnZ2f39/UJCQtHR0f3AAAA4YYqKivHx8WJiYmVlZf/YAFBQUGFhYdUAANQAAFGemg8AAABAdFJOUwAWxv0T8QHmCfeYDc4GMNWB2t0Er47MU7RxHWwlyS6mqz/uooZOYyG5Gjnr4mi+nH7DXlmLRLt4SCmTM0sD9HS9ZbqkAAAXLElEQVR42uWdeVxTV77ALyI7IijIJrigAooIUq17T4KJsiQhJjEhBJIoIJuCigp1QbHgrrWudaVO3Wu11VGpe1tbq9V2bKfW7naZLtqZdqbtrO/Ni5IAQnLPcs9NLr7fX/1UcnLPN/f8tvM7v8MwThZPv9jRI4aGB46KCY5L7NQkw+N6DwlJikwZFh3hzTzC4jdyTPioMH+vcXLgSOQeCVHBIYOHxT5yIDoPHOyb7OUOUMWn2/CQoQF+j8bkPWNDQ5IHAQJx94oLHBHfwafv1n+Uvw/gIh7JgX066rvgOTI8ri+gIV69h0Z0vOmPDvR3B/RkXGJkbEea/8gkqtO3asfEwR1EKXQekOgD+BGP3mOEbyZH+w4CfEq3QEGvBu+UZHfAt/j07uMpVNP3RDfgHIlKEeJiiOg3CDhPgiKF5iREPOYBnCsJ4UJiEO/0+T9gECmUteCX1Be4RoKGdhfA/LsPSACukx7DXA6gTxRwrQT3dK0CjJEDV8u4QNepg7GRfYEQJGiMiwAEJAOhSIwrYqYuST5AOOKV4nQA0VFAWNLLzbkAwscBoUmCM7VBfBgQooR0cRaAEV5AmBI10jkpwCSqPoDcItQG6+sMhdiZxgooOF6y5lLZ+Z13G2oqK8p3UgT6GO+RQkAQx7lvurD29ITyelWmUiFTKqWGzLzfN9B8p+J4tgmhXMLgqaVrF1Q05slk0jyjSiWyiXQCXQ8xgE8Aj5P/9jPLDh/SKZR5RlFboUwAePTnLxD2Jf3xz5wuNyqkOpXIntAmANwj+cqEkOnA/DO3DhlkefZnzwsBiz7kxw3qRGLrSk/Pk8p0IjbhgQCI4cEk9OyK/xx75lfqFOzT54kACKaeNIjG3wuY9eYGhUElErmEABhOOZUcgO0Iz1ywGP7z80gAdOrsUgClh3UytUjkSgIgys11AGYe1ilVIpGLCVBEEI0HYNYCI8b8eSQAkinpgp5YSnDP+UaFWiQSBAEwnIpFiMcxg/Jn5imMIhEBgY/XHV1/fd/HS6Y/TzP4DqbgF/jhJARnTVDmiUREBBb9W5+eoTX/Wr353updO95bMoUOgiHcU8IYrnBB2WKlSERIYKJE/EDMZm26RJJRVLty4lvPFnBH0I8rgSHo31VSg70A7BCwSpo5XZJe9Nyu6xu5IuAYJiWhf9NaohfAEYEmDFpJevXKHc9yixQ5BcspyN9z/C6iB4hD4AGFdE3R2zumc0keRpMDGIi8K3Bhnkwl4oVAE4S5298jV41diT2jeOScYJkxUyTijcB9CPqM3euIVUIwYfHZWFQzkH9LZhTxS8AiWk31riXONQj9EIffVKlQifgnYDGUmqJzhAxCidLCqGHQPKVI5BQClsVgYUBkGQYRVJrEIoZDZxoNIqcRuM9g7sTZBAgSsd1jzzi0kS8ZdSJnErAwWLr5qNwJqgBxZ2BtJmcAuAQs+kDy9lP4OdsRmJ4AWoFIldQocj4BsVh/8Ta2exCElTXz7oEGQKYWuYSAZSnc+5jXMLGfEwEQERCL0y8eKODPJA5EOiVQRgcAIQHLa7ASM1rohuwdd0FKisw30AFASkAslmzeh4fAl6odWKUzilxMQKw1r8NDgFiF3BOlRuDgYp3I5QQs/tE5LJvgj5Y57Y2SENyQKRIAAbF46dtYEePjKAD6o6TEK5QiYRAQS77BCZb6IsQH3v4I/tXvFCKhEBDrN+M4iDEI1aIIwzxJEwBXAuKMuhUYCPpA80IIIeGqPLWQCIi11RgIEmEEHkPQgvV5IkERwEMQytkSFtQoRQIjYEGAHiX4s1cf+zpbCdAhINbWLUdGMIB1nxweFF/QqQVIQJxeixwkdPXmtEc2tdwgEiIBsWTh8xRegpHwV+A07TVAi4BYs72A+0swygVrgBoB8dJFqC/BUIfpYaghyK+gvgboEUjLWI9IoMdY4sxQFd01oNJlKmWK/75Gh4DYXI3qHzvYTu4MPUA8q5FaSCxSZ8pk6kOVd86XPVNKiYA4/R6iNgyzT2Aw9IMLZJSmb5TKFh/ZeWnmnpbEPw0C4qVXEGsKBtrdI+kBV4MqKu++QdZ4eP6sNrseVAiIM17kkC8bBo2Ja6Q0fn6Zrmbtpvaj0yGg3YzmGA2yd0y3F+xTz1CIB4yKxluldve86BAQa15CewnCCUxhQQXnxJha0Xj6aQfDUyIgTkdbBz3aF1U8Ac2Oc1aDSvXOpx0OT4uAthZta7l9pgS2R5BfzjEroJO9NpNl/IlL6RBAdQ3b6cKB0G1yjlpAWT+fdc/7drr5fiWlmTOBtItIfpFX243UEJghOJLJTQM02F0AxXMuH7u2t9jyX7OXr7i+7sr+OrMmPY0bAv1qklyRN6xsbBWXgGBGnq6q3Qvw8rFt75/dOjk123RjUsv/nb3iwMpqCTcIGW+hEOiN6Qw0cPEFpBvWtBnu1W0nT2VnFZpys3NysidPevgfpx9deVHDYTlkLESJk/vGY8XFM9Uc3EFZxayHf/1lJ74uLMxOtUo7AhZZvqiOAwM9UpA4FGsR7OQQFCoa9rQeas5vpwpNOaktYo+AZTncrtOQrgXtc1Nwl0EfWNXwBvKgUHE4v/X8P7iRlZv6kNgnYGGwqEjP50vwkDXox583pLjbalEWb9uSlZ2aikbAshZWSsw8aoL+GO5QDbElUExo9Qa8/lXb35+dAJC/Up1Otg7ew3OKekIypCUq0uygrHJqyzA/pppSU7EIALBkoYY3n6Bry97JAL42SQwvtMTBc060XwBwAmDKOQmBQkz7FcExdA9ADYzlpFGhrrElEnh9a1ZqKgEBAA5oCZSBZhfCS9B8AMcbcqBwppHMGVBJn2keY+8NUyohAXD0Ij4C82aEEDG4+VCtnJ9FoGhpMrMsJzeVmAB4kQABikFM8ENMkVaSLQJDxVQUAAgEwHpzGi+6cLSVQAwkR76YyBKojRealwAbABQCYAd2rJQ2F+EgwmBrg5WuvLhDijebleANNgBIBMAibKMoeQW5qigW4g3cIcqN5M2zRQNztppSORMoWI2bRUtfieARjEUpn5s6jygmkF2yDXAyK5U7AbCxVou5DKrhiXOfpjbwgex/VUq0T2KotBmYHyEAEAmA93C1IYo1aDp2EQwpISexhSrDGZsS+DobQiAXjQDYhakKJH9ErDGFKUIiNWCotH38bCH79AsLc7eiEZiNuQ7MtfAsQa8HPYfZj9bmv0CiBpq9wW1sAHJMplMffnlsDuKe93rMdEEaPDbwR8iOEHkDeeXWmHjSKRZDWJhzYtnLGOWg8rfxQmXJDvj+oRs8MFxFkiJVlFk//YFjNZhrOnEM73QAeA9vGegR9hCj4VWkJEGBsdG6OTBnskM1WDh5GcAWvJdAuxuuCELhxwlIFKHyd9YP/+bwFcg6+yo+AHA9A0sPFMEd43BohkxeQbBdqLTqwZe3OtIChR8WEwAAU77BWgcIWye+DOOdAMkS4x8nMtZbM0PLHBmCrPcBmRzA8gk0B1AqimLZjWEJwVZJc4ulkw4IZJ1s9Q3v/O3bm5988cmfP/0BIcm/pCiNrir092RGs//FGYIsscxqCV69YV8Pmq622MDv//7ZeJv8/O270CdeiaMLtffgmwZ+zBj2v1iLbwpURmt28Ev7r0B2arMVLP70H+Nby2f/hCYKJFipMmh1nUcEzB14Ez85oJtnzQ19aJ9A1ge2wd/9ZHxbufkO+/Ms/zUNxxgsQXAIIMcrFyiJ1YADS9ASBnz/y/j28gk7ggIsa6CFnz3pA3OICHbNZeebPnosJ8euIdxmU4E/jbcnn7PbyXM4yyB9PYJLNIR66YjNG7CvBrK32AKhP4+3L9/SUwQa+KncwZDsAEFkqNKVNn32I7sECm2uwF8cABj/xp/YnmgFTuJcAq+qeoIZzvrve/C3zdW2oOCE3fTgtL3Wt+snRwTGf872RM/OxVCF+nNQAoEMex/mTfVGUlNQfDXX7iKw6sG/OQQw/o0/sDnGtRgvgX47lEAIw37I9ulG7OxAXkVThnDSFnv+kOkERAtANcFCLdV8sS/DniMrmYFNwFBj9Qi/tmcKCj+y/pQ/sxD4gu2RVmN4hRn7EfJkEAIibALSw9YUaWoOiy38/g0WAp+x+QR/lOC4xXI4gQTaqXLlAjZ3oNCaF/mBBQC7IrjiZAJ5xASuZdsjYNoLsYX35a9s9nARXQJh/+8JxPGwCu50qFUAewdINKE1MLrcUTQhf9bwBps1LP6FhcAnArKGszh4RKfsekRnrSN/x0LgUyd6RL0gPiEHrxhcncYWGv4gEK/YFxIXcImMTrJGRuALhwRuOjMyCoHEhkTR8UG2HbPm6NhhaPQPtlcA7NPSjY4DIfkBkgyJrXrE/m5B9uQ5EE3wqVMzJE/A2k404GfLZdYSqstf28+S/Whb0V/grwGLMdTTzZINhuUJCTKlhtcAS4IgNfeU7SV41x6Cz9n3TabsxlkFZnimNBSWKybJlh+yVpG97yBb/lHzdtHNdgC+mwLLlovpZsv7wPYLSHZMbKpwmf0qupyca83D//Ozh+b/839gD7yO8o4JiIbtGZHsmimqAEuWyGIQt7bUzbz76S9/tcVDP/3rHXgFAc7+uXYhdDyPCNi+IcnOqU0RONo0Si080WpPoPhP//ru5uc3//4/f0DYUKe+c+rlB9s7Pl5PsHtu84muOSofyCIrH+Bh99zfE1Y/QFRBIZtv/fRX0xwhOPkyCQA8SyDOgJ82CoOfsrpLUEUjbQCsu8f3EXx1mYDAi1h1RChVNL7wOiKSSir1jBJYGU2q6cY2fAL7seqItLvhZ+7C4bVkZNV0T8ILKrMLz+7FBPAW/Wq6UHg9IVFFZXOEXHyVpbLeNO3sl3MwABQsxHoFUCoqH9QTQmpKyapqm3Xh3mk5bFW1hZNP/ngNta7uKO4hXHhV7YOaUkhdMZEqFGVW2Jbgh+zF9dmmLNMWtMrqjZvxzluZEQoq/VFqy4mq65uLCMCrW3LpnC8AV3Cr6+Hpkabactj5goNEJywybbXVYBnbOsAgcB3zjIlYj9CX53GUMyaEp2yai6vBR1RO2Ty7mYdTNtYzJrA23USKQKSrt7VeKT6RxZ3AlJXYJ60QDhxazxnBzpqtJTttJ7tlG2DO1kLOBHbxctrOetYMpgoJdk0epAkyV9lGuLzFxJHAOuyOVUiLIAbtzKmc8NRt3rzjtiFeZ0OAQOAVLf6p2+0IyiUS8dwx6clr2Z3mIV4/VciBAAEAtA5toxHPnpeSdmaUNdsDcPlqFjEBEgDmWoSGnc1nz2H9BwoqCJuyGdUtnZgmncjKISNwm6RDlQalP1swcnNG4i4cefUlLdmwD3KnERB4/iWStkRpRcsx1AC8Dwl5JxZpeauGnHtP2X0NWAk89RxZJxYUPdiqDwmsFw15N54ZsspWDakmvZ9rwiJQsK4ogwSAWIty01GrXjTQfkRryTsyyWpa9+S6drZwGjqBp/ZLyBpzZexHuf/PF6Mn1fF6HQcEx1sPtewrkykHicD0KxfJ2jGhNivtj9OhcieHJqWyioeaMxbvPZGTlZsDIzB9UTVxZza0plwP9SWD9qYr5dKsWXmo9OHRXv/ganaWKdsxAflTV6o5dOdDewV64fXt59ShMXPGpTbDFR/77ezk3MJC07Tc7Lb9B+RL1i38VcOhS2U6khZo08Mf2qNyFad+1Trpzvx2Q066tu2jk19t3fL1qRYC8o37Ji4s0mSIuQiSIWjboxK6DDj2a1YpKhy06n150pwHmdLZH+87evule9VaSQbHTq0SFF+gXZ9SaK9azg2bDeon89nGn7hUK5HotRxnj+wOtr/XCGoN8ss5Nu02KsrPsBHQiOnI0olIANr1K4Y6RRS6dmdKJzjuWk2ta/dutFscfPH7lnN+CSzaQKm7U8o3AaRmxfb6lsOvcaHRvV+t1DWsyueRgOYcGoAoT4L7C+SVNC6yUUmlL5yfKeeJgLYW8crLcJI7LMCaTCq3eIh0CvWRJy9M5YFAGuoasHuHBfweE3CX1k0uqkxF3qHDVWdm5dMlsHQXGgAH9/7C77KZ1Ujrht/7NXdShWHxvJoFb1adoUVAvxDx5lv7d9kg3GdE/WY/tc6glP1vDSUC5jrUS1/DiO+0EvatXtrrqLUY/YnvNQNrBHyzm+Y2KgCH95oh3G0n4Nv9lr6ECsDx3XYo9xtOLc8UJgHJ28g3PLJdcgm/45KHdUCFgP4b9Nu/B3C75xScF+JNrxmb0e/+Zr3nFOWu24JK4d32m1GHcfP3AK73HYNZ9TqBEcACALnvGOXOa/CMQVi3fmfUod/3DL3zGunec8qqgCuBdJw3AH7vOROOMIr8sEI4BPS1yzEA2MmMtBVvf4Rh9pRLhUJAc286DoAYBi79UQYqqc8UBoGlq2fjAOjbE4EA7MBBk1wQ6QRAwCzZVYADwFpDChMUi2gxCHlGlxPQXnwFa/7A3xuJAOwQpq2mQKp2MQFN7Qo8AGAYGgCmSxTScFVKtSsJmCXbZ2MC8GVQZaA70oBPKlSuI5BetANz/qCbGzIBhGyR1TNSu4hAmmbhclwAUG/wIacAbR1YFoLRJQQkc9cVYAMYwuDIQB+0UcsMRucT0KavXoI9fxDUGYsAoj0AYL5O52QCZk3tdfz5A/kIPACMZxziyKsWZzqTQJqm+sDzBABAPwZXYr0Qhz64Qek0Ammaubs2kswfJHbHJsCEog7+9BGFcwiYNdW7niWaPxjUkyGQfqjDT13AySoiEtBq6iZOB4QSSgKAGRuGrGWqdHn8EjBLtPdemU06fwIlYM0XBSF/xZpDMv4IpOkldedWFBDPHwR7EhJgBo5D/pJNExRGXgiY9fq5q4+S//z30+NuDLGkYNjbMpGUNoG0DI22bvvR6YCT9I1mOEgSxjfNPEL2GtglkKbVa7TV+yfumw04int/hpMMwfiugqoZSo4E0tLM2gy9Rm+eu3v7gX0bAQWJ5AaA6RKG820lr8l0hAQW/VuSrjX/WlS3e/9LE4+umF4A6Eg/hqv4ReF8n/zSIWzfoInA8vUvvrVvxfLps6cAmjKE4S7xXbG+cs+bM2QqAgL8SHB3CgSYnt3wvrXkjk6pEgaB4d4MFYn2wvzigxMyMRjwRyDZj6EkAbgIwJoGHXIelTcCUW4M4zoE4OBdlULnUgI0ARAhACWn6xUodbg8EejUmaEq0d0IHuL42iN5cAeBHwLD/RjK0rMryXPID+48pFTqnE8g2JuhLvGdyJ5l6qoFG5QylvOKfBCI6c7wIH5hpM+zZ9WtF3QKqQMKPBDox/Aj3X3Jnym/tKphQ55CaacakToB90iGN3mc05PtOVh2p7zRIJMZdEYVfwQ8+jM8SqgHx8creHpN2c6aFxrVBoVMJpMaMjN1v2+gCiAogOFVAoJoPGX+ppln5ledXnD4tcoj5Rtu0QQQ58bwLJ3DqP5icnlBfgHF8R7rzvAunklyIFTpm8I4RUZ4CRRA1EjGSRIfJkgAIV0Y50n4OMHNP2EM41SJjhIYgF5ujJOlS5KPgObvlcK4QAKSBQMgJp5xiYyN7CuI+QeNYVwmETGu9w3GBXozrpQ+rtaIwT0ZF0v3AQkunH+PYYwAxC/JVeogaGh3RhgS/5iHC+afEOnNCEcinM4gIdyPEZZE9BvkzPc/Umjzvy9uT3Rz0vyjUrwZYYp3SrI779P36d3HkxGwjPbldzF0C4xlhC6dByTyFTN59B7jzXQIGfm4P/3V4JM4OJ7pOOI5OpAqhHGJkbFMRxPPkeFxdJxFr95DI5gOKm79R/lzUwoeyYF9/JgOLZ6xoSHJRPbB3SsucEQ882hI54GDfZO90BWDT7fhIUMD/JhHTPxGjgkfFebvNc5xVkXukRAVHDJ4WKw38+iKp1/s6BFDwwNHxQTHJXZqkuFxvYeEJEWmDIuOcP7U/w+C7XCu0wHHigAAAABJRU5ErkJggg== + :alt: AOSC Version + :target: https://packages.aosc.io/packages/sunshine + 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 +.. image:: https://img.shields.io/static/v1.svg?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 +.. image:: https://img.shields.io/github/last-commit/loki-47-6F-64/sunshine.svg?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 +.. image:: https://img.shields.io/github/release-date/loki-47-6F-64/sunshine.svg?style=for-the-badge&logo=github :alt: GitHub Release Date diff --git a/docs/source/about/usage.rst b/docs/source/about/usage.rst index fa680ba6c2d..b59df1281d3 100644 --- a/docs/source/about/usage.rst +++ b/docs/source/about/usage.rst @@ -21,9 +21,33 @@ Usage .. 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 + 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 @@ -78,7 +102,7 @@ Sunshine needs access to `uinput` to create mouse and gamepad events. - filename: ``~/.config/systemd/user/sunshine.service`` - contents: - .. code-block:: + .. code-block:: cfg [Unit] Description=Sunshine self-hosted game stream host for Moonlight. @@ -140,8 +164,8 @@ Sunshine needs access to `uinput` to create mouse and gamepad events. macOS ^^^^^ Sunshine can only access microphones on macOS due to system limitations. To stream system audio use -`Soundflower `_ or -`BlackHole `_. +`Soundflower `__ or +`BlackHole `__. .. Note:: Command Keys are not forwarded by Moonlight. Right Option-Key is mapped to CMD-Key. @@ -155,7 +179,7 @@ Configure autostart service Windows ^^^^^^^ -For gamepad support, install `ViGEmBus `_ +For gamepad support, install `Nefarius Virtual Gamepad `__ Sunshine firewall **Add rule** @@ -230,6 +254,7 @@ Application List - ``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 - ``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 @@ -243,10 +268,13 @@ Application List - ``working-dir`` - The working directory to use. If not specified, Sunshine will use the application directory. -- For more examples see :ref:`app examples `. +- 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. @@ -270,10 +298,17 @@ You must have an HDR-capable display or EDID emulator dongle connected to your h - 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 ---------- +.. 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 are community generated. Want to contribute? Reach out to us on our discord server. + Tutorials and Guides 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..82a3ed17462 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 @@ -15,16 +16,17 @@ Install Requirements 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 \ libmfx-dev \ # x86_64 only + libnotify-dev \ libnuma-dev \ libopus-dev \ libpulse-dev \ @@ -44,9 +46,8 @@ Install Requirements nvidia-cuda-dev \ # Cuda, NvFBC nvidia-cuda-toolkit # Cuda, NvFBC -Fedora 36, 37 +Fedora 37, 38 ^^^^^^^^^^^^^ -End of Life: TBD Install Requirements .. code-block:: bash @@ -64,6 +65,7 @@ Install Requirements libcurl-devel \ libdrm-devel \ libevdev-devel \ + libnotify-devel \ libva-devel \ libvdpau-devel \ libX11-devel \ # X11 @@ -95,17 +97,17 @@ Install Requirements build-essential \ cmake \ g++-10 \ - libappindicator3-dev \ + libayatana-appindicator3-dev \ libavdevice-dev \ libboost-filesystem-dev \ libboost-locale-dev \ libboost-log-dev \ - libboost-thread-dev \ libboost-program-options-dev \ libcap-dev \ # KMS libdrm-dev \ # KMS libevdev-dev \ libmfx-dev \ # x86_64 only + libnotify-dev \ libnuma-dev \ libopus-dev \ libpulse-dev \ @@ -149,12 +151,12 @@ Install Requirements libboost-filesystem-dev \ libboost-locale-dev \ libboost-log-dev \ - libboost-thread-dev \ libboost-program-options-dev \ libcap-dev \ # KMS libdrm-dev \ # KMS libevdev-dev \ libmfx-dev \ # x86_64 only + libnotify-dev \ libnuma-dev \ libopus-dev \ libpulse-dev \ @@ -177,14 +179,14 @@ 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. + 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 diff --git a/docs/source/building/macos.rst b/docs/source/building/macos.rst index 5ff3e74e980..2d39ec17951 100644 --- a/docs/source/building/macos.rst +++ b/docs/source/building/macos.rst @@ -5,7 +5,7 @@ Requirements ------------ macOS Big Sur and Xcode 12.5+ -Use either `MacPorts `_ or `Homebrew `_ +Use either `MacPorts `__ or `Homebrew `__ MacPorts """""""" @@ -19,7 +19,7 @@ Homebrew Install Requirements .. code-block:: bash - brew install boost cmake node opus + brew install boost cmake node opus pkg-config # if there are issues with an SSL header that is not found: cd /usr/local/include ln -s ../opt/openssl/include/openssl . diff --git a/docs/source/building/windows.rst b/docs/source/building/windows.rst index 0cfb53a488a..623bd16a409 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 MinGW 64-bit" and execute the following codes. Update all packages: @@ -16,12 +16,12 @@ Install dependencies: 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-onevpl 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 nodejs and npm. Downloads available `here `__. Install npm dependencies. .. code-block:: bash diff --git a/docs/source/conf.py b/docs/source/conf.py index e581783865f..3c9e338bb07 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -51,6 +51,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 +69,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', '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, diff --git a/docs/source/contributing/contributing.rst b/docs/source/contributing/contributing.rst index 217bda130bd..a50300d2327 100644 --- a/docs/source/contributing/contributing.rst +++ b/docs/source/contributing/contributing.rst @@ -2,4 +2,4 @@ Contributing ============ Read our contribution guide in our organization level -`docs `_. +`docs `__. diff --git a/docs/source/contributing/localization.rst b/docs/source/contributing/localization.rst index dc3e26da877..ec6a43a5daf 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://badges.awesome-crowdin.com/translation-15178612-606145.png 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. @@ -49,10 +37,12 @@ situations. For example if a system tray icon is added it should be localized as .. code-block:: cpp #include - boost::locale::translate("Hello world!") + #include + + std::string msg = boost::locale::translate("Hello world!"); .. Tip:: More examples can be found in the documentation for - `boost locale `_. + `boost locale `__. .. Warning:: This is for information only. Contributors should never include manually updated template files, or manually compiled language files in Pull Requests. @@ -68,7 +58,7 @@ any of the following paths are modified. 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. +`xgettext `__ must be installed. **Extract, initialize, and update** .. code-block:: bash diff --git a/docs/source/contributing/testing.rst b/docs/source/contributing/testing.rst index aca4a82dc8d..e3ed073447a 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,6 +42,21 @@ 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 diff --git a/docs/source/gamestream/gamestream.rst b/docs/source/gamestream/gamestream.rst index aed014b287c..a0cabbd3f17 100644 --- a/docs/source/gamestream/gamestream.rst +++ b/docs/source/gamestream/gamestream.rst @@ -7,7 +7,7 @@ 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 +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 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/legal/legal.rst b/docs/source/legal/legal.rst index 13bc3660140..4fb55b0450b 100644 --- a/docs/source/legal/legal.rst +++ b/docs/source/legal/legal.rst @@ -4,7 +4,7 @@ Legal 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.rst b/docs/source/source/src.rst index b5b79228cf9..bdba0c359c4 100644 --- a/docs/source/source/src.rst +++ b/docs/source/source/src.rst @@ -10,7 +10,7 @@ Example Documentation Blocks **file.h** -.. code-block:: cpp +.. code-block:: c // functions int main(int argc, char *argv[]); diff --git a/docs/source/toc.rst b/docs/source/toc.rst index e4c4398bda6..6121e4402e3 100644 --- a/docs/source/toc.rst +++ b/docs/source/toc.rst @@ -7,7 +7,7 @@ about/docker about/third_party_packages about/usage - about/app_examples + about/guides/guides about/advanced_usage about/changelog diff --git a/docs/source/troubleshooting/general.rst b/docs/source/troubleshooting/general.rst index 3b6d6aad241..a936f35aa33 100644 --- a/docs/source/troubleshooting/general.rst +++ b/docs/source/troubleshooting/general.rst @@ -6,7 +6,7 @@ Forgotten Credentials If you forgot your credentials to the web UI, try this. .. code-block:: bash - sunshine --creds + sunshine --creds {new-username} {new-password} Web UI Access ------------- @@ -17,8 +17,8 @@ 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. diff --git a/docs/source/troubleshooting/linux.rst b/docs/source/troubleshooting/linux.rst index 5c2d7c8a37c..7458aa587d2 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..9190fc300f7 100644 --- a/docs/source/troubleshooting/windows.rst +++ b/docs/source/troubleshooting/windows.rst @@ -3,4 +3,4 @@ Windows No gamepad detected ------------------- -#. Verify that you've installed `ViGEmBus `_. +#. Verify that you've installed `Nefarius Virtual Gamepad `__. 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..2d34248c5d6 --- /dev/null +++ b/gh-pages-template/index.html @@ -0,0 +1,412 @@ + + + + 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..f5152b6a54a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "dependencies": { - "@fortawesome/fontawesome-free": "6.4.0", - "bootstrap": "5.2.3", + "@fortawesome/fontawesome-free": "6.4.2", + "bootstrap": "5.3.2", "vue": "2.6.12" } } diff --git a/packaging/linux/AppImage/sunshine.desktop b/packaging/linux/AppImage/sunshine.desktop new file mode 100644 index 00000000000..a345e5dc467 --- /dev/null +++ b/packaging/linux/AppImage/sunshine.desktop @@ -0,0 +1,12 @@ +[Desktop Entry] +Type=Application +Name=@PROJECT_NAME@ +Exec=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 diff --git a/packaging/linux/aur/PKGBUILD b/packaging/linux/Arch/PKGBUILD similarity index 77% rename from packaging/linux/aur/PKGBUILD rename to packaging/linux/Arch/PKGBUILD index 02eaf0b5c33..11b699823b0 100644 --- a/packaging/linux/aur/PKGBUILD +++ b/packaging/linux/Arch/PKGBUILD @@ -9,8 +9,31 @@ 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') +depends=('avahi' + 'boost-libs' + 'curl' + 'libayatana-appindicator' + 'libevdev' + 'libmfx' + 'libnotify' + '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') @@ -51,6 +74,10 @@ build() { npm install popd + export BRANCH="@GITHUB_BRANCH@" + export BUILD_VERSION="@GITHUB_BUILD_VERSION@" + export COMMIT="@GITHUB_COMMIT@" + export CFLAGS="${CFLAGS/-Werror=format-security/}" export CXXFLAGS="${CXXFLAGS/-Werror=format-security/}" diff --git a/packaging/linux/flatpak/dev.lizardbyte.sunshine.yml b/packaging/linux/flatpak/dev.lizardbyte.sunshine.yml index 403c6db01d4..be1ae8adbf2 100644 --- a/packaging/linux/flatpak/dev.lizardbyte.sunshine.yml +++ b/packaging/linux/flatpak/dev.lizardbyte.sunshine.yml @@ -5,6 +5,7 @@ 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: @@ -27,6 +28,10 @@ cleanup: - /lib/*.a - /share/man +build-options: + append-path: /usr/lib/sdk/vala/bin + prepend-ld-library-path: /usr/lib/sdk/vala/lib + modules: - name: boost disabled: false @@ -41,8 +46,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 +93,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 +185,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 @@ -223,7 +325,7 @@ modules: - -DSUNSHINE_ENABLE_X11=ON - -DSUNSHINE_ENABLE_DRM=ON - -DSUNSHINE_ENABLE_CUDA=ON - - -DSUNSHINE_CONFIGURE_FLATPAK=ON + - -DSUNSHINE_BUILD_FLATPAK=ON sources: - type: git url: "@GITHUB_CLONE_URL@" diff --git a/packaging/linux/flatpak/sunshine.desktop b/packaging/linux/flatpak/sunshine.desktop new file mode 100644 index 00000000000..acfced8d7a6 --- /dev/null +++ b/packaging/linux/flatpak/sunshine.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Type=Application +Name=@PROJECT_NAME@ +Exec=flatpak run dev.lizardbyte.sunshine +Version=1.0 +Comment=@PROJECT_DESCRIPTION@ +Icon=sunshine +Categories=Utility; +Terminal=true diff --git a/packaging/linux/flatpak/sunshine_kms.desktop b/packaging/linux/flatpak/sunshine_kms.desktop new file mode 100644 index 00000000000..139fd0c4cb7 --- /dev/null +++ b/packaging/linux/flatpak/sunshine_kms.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Type=Application +Name=@PROJECT_NAME@ (KMS) +Exec=sudo -i PULSE_SERVER=unix:$(pactl info | awk '/Server String/{print$3}') flatpak run dev.lizardbyte.sunshine +Version=1.0 +Comment=@PROJECT_DESCRIPTION@ +Icon=sunshine +Categories=Utility; +Terminal=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..d5cf7c03f18 100644 --- a/packaging/linux/sunshine.desktop +++ b/packaging/linux/sunshine.desktop @@ -7,6 +7,3 @@ Comment=@PROJECT_DESCRIPTION@ Icon=sunshine Categories=Utility; Terminal=true -X-AppImage-Name=sunshine -X-AppImage-Version=@PROJECT_VERSION@ -X-AppImage-Arch=x86_64 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/macos/Portfile b/packaging/macos/Portfile index fb5fb13a8f0..c22c1cb9dca 100644 --- a/packaging/macos/Portfile +++ b/packaging/macos/Portfile @@ -37,7 +37,7 @@ depends_lib port:avahi \ port:npm9 \ port:pkgconfig -boost.version 1.80 +boost.version 1.81 configure.args -DCMAKE_INSTALL_PREFIX=${prefix} \ -DSUNSHINE_ASSETS_DIR=etc/sunshine/assets diff --git a/scripts/icons/convert_and_pack.sh b/scripts/icons/convert_and_pack.sh new file mode 100644 index 00000000000..dd1183b9992 --- /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/images/sunshine-locked.svg" + "../../src_assets/common/assets/web/images/sunshine-pausing.svg" + "../../src_assets/common/assets/web/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..4cb6e551faf 100644 --- a/scripts/requirements.txt +++ b/scripts/requirements.txt @@ -1 +1 @@ -Babel==2.12.1 +Babel==2.13.0 diff --git a/scripts/update_clang_format.py b/scripts/update_clang_format.py index 2624e794405..8cf9b9f0d97 100644 --- a/scripts/update_clang_format.py +++ b/scripts/update_clang_format.py @@ -8,7 +8,6 @@ '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/cbs.cpp b/src/cbs.cpp index 52a3ed93f5e..c06b7c4a8b5 100644 --- a/src/cbs.cpp +++ b/src/cbs.cpp @@ -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..726f67f8ca7 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -16,6 +16,8 @@ #include "config.h" #include "main.h" +#include "nvhttp.h" +#include "rtsp.h" #include "utility.h" #include "platform/common.h" @@ -24,6 +26,11 @@ #include #endif +#ifndef __APPLE__ + // For NVENC legacy constants + #include +#endif + namespace fs = std::filesystem; using namespace std::literals; @@ -35,107 +42,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; - } - - 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; + 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; } - 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,6 +78,10 @@ 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_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_HEVC_USAGE_TRANSCONDING 0 #define AMF_VIDEO_ENCODER_HEVC_USAGE_ULTRA_LOW_LATENCY 1 #define AMF_VIDEO_ENCODER_HEVC_USAGE_LOW_LATENCY 2 @@ -156,10 +94,17 @@ namespace config { #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,6 +117,13 @@ namespace config { balanced = AMF_VIDEO_ENCODER_QUALITY_PRESET_BALANCED }; + enum class rc_av1_e : int { + 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, + cbr = AMF_VIDEO_ENCODER_AV1_RATE_CONTROL_METHOD_CBR + }; + enum class rc_hevc_e : int { cqp = AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_CONSTANT_QP, vbr_latency = AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_LATENCY_CONSTRAINED_VBR, @@ -186,6 +138,13 @@ namespace config { cbr = AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_CBR }; + enum class usage_av1_e : int { + transcoding = AMF_VIDEO_ENCODER_AV1_USAGE_TRANSCODING, + webcam = AMF_VIDEO_ENCODER_AV1_USAGE_WEBCAM, + 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, webcam = AMF_VIDEO_ENCODER_HEVC_USAGE_WEBCAM, @@ -206,10 +165,11 @@ 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) { #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_(quality); _CONVERT_(speed); _CONVERT_(balanced); @@ -217,10 +177,11 @@ namespace config { return std::nullopt; } + template std::optional - rc_from_view(const std::string_view &rc, int codec) { + rc_from_view(const std::string_view &rc) { #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_(cqp); _CONVERT_(vbr_latency); _CONVERT_(vbr_peak); @@ -229,10 +190,11 @@ namespace config { return std::nullopt; } + template std::optional - usage_from_view(const std::string_view &rc, int codec) { + usage_from_view(const std::string_view &rc) { #define _CONVERT_(x) \ - if (rc == #x##sv) return codec == 0 ? (int) usage_hevc_e::x : (int) usage_h264_e::x + if (rc == #x##sv) return (int) T::x _CONVERT_(transcoding); _CONVERT_(webcam); _CONVERT_(lowlatency); @@ -333,23 +295,41 @@ 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 { "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 + {}, // nv_legacy { qsv::medium, // preset @@ -359,10 +339,13 @@ namespace config { { (int) amd::quality_h264_e::balanced, // quality (h264) (int) amd::quality_hevc_e::balanced, // quality (hevc) + (int) amd::quality_av1_e::balanced, // quality (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) (int) amd::usage_h264_e::ultralowlatency, // usage (h264) (int) amd::usage_hevc_e::ultralowlatency, // usage (hevc) + (int) amd::usage_av1_e::ultralowlatency, // usage (av1) 0, // preanalysis 1, // vbaq (int) amd::coder_e::_auto, // coder @@ -379,7 +362,6 @@ namespace config { {}, // encoder {}, // adapter_name {}, // output_name - true // dwmflush }; audio_t audio { @@ -398,7 +380,6 @@ namespace config { }; nvhttp_t nvhttp { - "pc", // origin_pin "lan", // origin web manager PRIVATE_KEY_FILE, @@ -453,7 +434,8 @@ 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 {}, // prep commands }; @@ -581,6 +563,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; @@ -924,12 +916,25 @@ 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 }); + 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); + +#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; +#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); @@ -937,23 +942,26 @@ namespace config { 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_hevc = amd::quality_from_view(quality); + video.amd.amd_quality_av1 = amd::quality_from_view(quality); } 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_hevc = amd::rc_from_view(rc); + video.amd.amd_rc_av1 = amd::rc_from_view(rc); } 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(rc); + video.amd.amd_usage_hevc = amd::usage_from_view(rc); + video.amd.amd_usage_av1 = amd::usage_from_view(rc); } bool_f(vars, "amd_preanalysis", (bool &) video.amd.amd_preanalysis); @@ -968,7 +976,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 +996,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; @@ -1043,9 +1049,11 @@ namespace config { bool_f(vars, "always_send_scancodes", input.always_send_scancodes); 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); diff --git a/src/config.h b/src/config.h index 2c32e7afd1b..090e6b6864d 100644 --- a/src/config.h +++ b/src/config.h @@ -11,25 +11,31 @@ #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; + struct { - std::optional nv_preset; - std::optional nv_tune; - std::optional nv_rc; - int nv_coder; - } nv; + int preset; + int multipass; + int h264_coder; + } nv_legacy; struct { std::optional qsv_preset; @@ -39,10 +45,13 @@ namespace config { struct { std::optional amd_quality_h264; std::optional amd_quality_hevc; + std::optional amd_quality_av1; std::optional amd_rc_h264; std::optional amd_rc_hevc; + std::optional amd_rc_av1; std::optional amd_usage_h264; std::optional amd_usage_hevc; + std::optional amd_usage_av1; std::optional amd_preanalysis; std::optional amd_vbaq; int amd_coder; @@ -59,7 +68,6 @@ namespace config { std::string encoder; std::string adapter_name; std::string output_name; - bool dwmflush; }; struct audio_t { @@ -82,7 +90,6 @@ namespace config { 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 @@ -151,6 +158,8 @@ namespace config { } cmd; std::uint16_t port; + std::string address_family; + std::string log_file; std::vector prep_cmds; diff --git a/src/confighttp.cpp b/src/confighttp.cpp index 6e8b2393730..0de9cfb90c6 100644 --- a/src/confighttp.cpp +++ b/src/confighttp.cpp @@ -76,7 +76,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 +86,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 +96,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) { @@ -267,7 +267,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); @@ -368,7 +368,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); @@ -731,6 +731,7 @@ namespace confighttp { auto shutdown_event = mail::man->event(mail::shutdown); auto port_https = 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; @@ -754,11 +755,11 @@ namespace confighttp { server.resource["^/api/clients/unpair$"]["POST"] = unpairAll; 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.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/httpcommon.cpp b/src/httpcommon.cpp index 2cfbfe0dd42..b6ea0958105 100644 --- a/src/httpcommon.cpp +++ b/src/httpcommon.cpp @@ -41,13 +41,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) { 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..a2c85256031 100644 --- a/src/input.cpp +++ b/src/input.cpp @@ -10,6 +10,10 @@ extern "C" { } #include +#include +#include +#include +#include #include #include "config.h" @@ -19,8 +23,7 @@ extern "C" { #include "thread_pool.h" #include "utility.h" -#include -#include +#include using namespace std::literals; namespace input { @@ -78,6 +81,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,23 +153,26 @@ 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 } {} // 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; @@ -251,7 +279,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 +289,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 +422,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 +453,78 @@ 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. + */ + std::pair + 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(); + } + + 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 { (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 +535,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 +549,15 @@ 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 }); + 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 @@ -613,83 +818,302 @@ 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 }); + + 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 }); + + 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; + } + + 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 (!((input->active_gamepad_state >> packet->controllerNumber) & 1)) { + 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 +1162,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 +1185,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; @@ -793,12 +1555,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 +1605,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 +1633,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/main.cpp b/src/main.cpp index b347cc0ae5a..6cd3545ef4f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -46,6 +46,11 @@ safe::mail_t mail::man; using namespace std::literals; namespace bl = boost::log; +#ifdef _WIN32 +// Define global singleton used for NVIDIA control panel modifications +nvprefs::nvprefs_interface nvprefs_instance; +#endif + thread_pool_util::ThreadPool task_pool; bl::sources::severity_logger verbose(0); // Dominating output bl::sources::severity_logger debug(1); // Follow what is happening @@ -112,6 +117,22 @@ namespace version { } } // namespace version +#ifdef _WIN32 +namespace restore_nvprefs_undo { + int + entry(const char *name, int argc, char *argv[]) { + // Restore global NVIDIA control panel settings to the undo file + // left by improper termination of sunshine.exe, if it exists. + // This entry point is typically called by the uninstaller. + if (nvprefs_instance.load()) { + nvprefs_instance.restore_from_and_delete_undo_file_if_exists(); + nvprefs_instance.unload(); + } + return 0; + } +} // namespace restore_nvprefs_undo +#endif + namespace lifetime { static char **argv; static std::atomic_int desired_exit_code; @@ -369,6 +390,20 @@ launch_ui() { 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(map_port(confighttp::PORT_HTTPS)) + path; + platf::open_url(url); +} + /** * @brief Flush the log. * @@ -413,13 +448,22 @@ namespace gen_creds { std::map> cmd_to_func { { "creds"sv, gen_creds::entry }, { "help"sv, help::entry }, - { "version"sv, version::entry } + { "version"sv, version::entry }, +#ifdef _WIN32 + { "restore-nvprefs-undo"sv, restore_nvprefs_undo::entry }, +#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,49 +493,13 @@ main(int argc, char *argv[]) { task_pool_util::TaskPool::task_id_t force_shutdown = nullptr; #ifdef _WIN32 - // 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([]() { - 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; - return; - } - - auto wnd = CreateWindowExA( - 0, - wnd_class.lpszClassName, - "Sunshine Session Monitor Window", - 0, - CW_USEDEFAULT, - CW_USEDEFAULT, - CW_USEDEFAULT, - CW_USEDEFAULT, - nullptr, - nullptr, - nullptr, - nullptr); - if (!wnd) { - std::cout << "Failed to create session monitor window"sv << std::endl; - return; - } - - ShowWindow(wnd, SW_HIDE); - - // Run the message loop for our window - MSG msg {}; - while (GetMessage(&msg, nullptr, 0, 0) > 0) { - TranslateMessage(&msg); - DispatchMessage(&msg); - } - }); - window_thread.detach(); + // Switch default C standard library locale to UTF-8 on Windows 10 1803+ + setlocale(LC_ALL, ".UTF-8"); #endif + // Use UTF-8 conversion for the default C++ locale (used by boost::log) + std::locale::global(std::locale(std::locale(), new std::codecvt_utf8)); + mail::man = std::make_shared(); if (config::parse(argc, argv)) { @@ -504,6 +512,31 @@ main(int argc, char *argv[]) { 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_FATAL) { + BOOST_LOG(fatal) << buffer; + } + else if (level <= AV_LOG_ERROR) { + 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; + } + }); sink = boost::make_shared(); @@ -568,6 +601,95 @@ main(int argc, char *argv[]) { 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 reinstallation + 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::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)) { + session_monitor_hwnd_promise.set_value(NULL); + BOOST_LOG(error) << "Failed to register session monitor window class"sv << std::endl; + return; + } + + auto wnd = CreateWindowExA( + 0, + wnd_class.lpszClassName, + "Sunshine Session Monitor Window", + 0, + CW_USEDEFAULT, + CW_USEDEFAULT, + CW_USEDEFAULT, + CW_USEDEFAULT, + nullptr, + nullptr, + nullptr, + nullptr); + + session_monitor_hwnd_promise.set_value(wnd); + + if (!wnd) { + BOOST_LOG(error) << "Failed to create session monitor window"sv << std::endl; + return; + } + + ShowWindow(wnd, SW_HIDE); + + // Run the message loop for our window + MSG msg {}; + while (GetMessage(&msg, nullptr, 0, 0) > 0) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + }); + + 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); + } + + 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"; + } + + session_monitor_thread.detach(); + }); + +#endif + BOOST_LOG(info) << PROJECT_NAME << " version: " << PROJECT_VER << std::endl; task_pool.start(1); @@ -675,6 +797,14 @@ main(int argc, char *argv[]) { system_tray::end_tray(); #endif +#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 + return lifetime::desired_exit_code; } @@ -744,7 +874,15 @@ write_file(const char *path, const std::string_view &contents) { */ std::uint16_t map_port(int port) { - // TODO: Ensure port is in the range of 21-65535 + // 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 (std::uint16_t)((int) config::sunshine.port + port); + + return mapped_port; } diff --git a/src/main.h b/src/main.h index e98206f3838..8eb3e7ee277 100644 --- a/src/main.h +++ b/src/main.h @@ -17,6 +17,12 @@ #include "thread_pool.h" #include "thread_safe.h" +#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 + extern thread_pool_util::ThreadPool task_pool; extern bool display_cursor; @@ -42,6 +48,8 @@ std::uint16_t map_port(int port); void launch_ui(); +void +launch_ui_with_path(std::string path); // namespaces namespace mail { @@ -62,7 +70,8 @@ namespace mail { // Local mail MAIL(touch_port); MAIL(idr); - MAIL(rumble); + MAIL(invalidate_ref_frames); + MAIL(gamepad_feedback); MAIL(hdr); #undef MAIL diff --git a/src/network.cpp b/src/network.cpp index f65bdfaae89..c843cfc0552 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -7,54 +7,28 @@ #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 +41,35 @@ namespace net { return PC; } + net_e from_address(const std::string_view &view) { - auto addr = ip(view); + auto addr = 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 +91,96 @@ 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()) { + return "["s + address.to_string() + ']'; + } + else { + return address.to_string(); + } + } + 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) { + 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) }; + return host_t { enet_host_create(af == IPV4 ? AF_INET : AF_INET6, &addr, peers, 0, 0, 0) }; } void diff --git a/src/network.h b/src/network.h index e1ca36c7531..b54f63ce7ee 100644 --- a/src/network.h +++ b/src/network.h @@ -6,6 +6,8 @@ #include +#include + #include #include "utility.h" @@ -24,6 +26,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 +40,39 @@ 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 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); } // namespace net diff --git a/src/nvenc/nvenc_base.cpp b/src/nvenc/nvenc_base.cpp new file mode 100644 index 00000000000..f305f7682ad --- /dev/null +++ b/src/nvenc/nvenc_base.cpp @@ -0,0 +1,583 @@ +#include "nvenc_base.h" + +#include "src/config.h" +#include "src/utility.h" + +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) { + 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 = { NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS_VER }; + session_params.device = device; + session_params.deviceType = device_type; + session_params.apiVersion = NVENCAPI_VERSION; + if (nvenc_failed(nvenc->nvEncOpenEncodeSessionEx(&session_params, &encoder))) { + BOOST_LOG(error) << "NvEncOpenEncodeSessionEx failed"; + 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 = { 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 = { 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 = { NV_ENC_PRESET_CONFIG_VER, { NV_ENC_CONFIG_VER } }; + 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; + } + + 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 = { 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 = { 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 (encoder_params.rfi) extra += " rfi"; + if (init_params.enableWeightedPrediction) extra += " weighted-prediction"; + if (enc_config.rcParams.enableAQ) extra += " adaptive-quantization"; + 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 = { 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 = { 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 = { NV_ENC_PIC_PARAMS_VER }; + 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 = { NV_ENC_LOCK_BITSTREAM_VER }; + 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; + } + +} // namespace nvenc diff --git a/src/nvenc/nvenc_base.h b/src/nvenc/nvenc_base.h new file mode 100644 index 00000000000..aa02fbef620 --- /dev/null +++ b/src/nvenc/nvenc_base.h @@ -0,0 +1,80 @@ +#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); + + 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; + + 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..632146b7db0 --- /dev/null +++ b/src/nvenc/nvenc_config.h @@ -0,0 +1,48 @@ +#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; + + // 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..e86d2268634 --- /dev/null +++ b/src/nvenc/nvenc_d3d11.cpp @@ -0,0 +1,104 @@ +#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 = 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 = { NV_ENC_REGISTER_RESOURCE_VER }; + 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..261e90969a2 --- /dev/null +++ b/src/nvenc/nvenc_utils.cpp @@ -0,0 +1,76 @@ +#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..5bde7b079bc 100644 --- a/src/nvhttp.cpp +++ b/src/nvhttp.cpp @@ -17,6 +17,7 @@ #include #include #include +#include // local includes #include "config.h" @@ -28,6 +29,7 @@ #include "platform/common.h" #include "process.h" #include "rtsp.h" +#include "system_tray.h" #include "utility.h" #include "uuid.h" #include "video.h" @@ -152,9 +154,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; @@ -267,12 +273,28 @@ namespace nvhttp { launch_session.host_audio = host_audio; launch_session.gcm_key = util::from_hex(get_arg(args, "rikey"), true); + 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.gcmap = util::from_view(get_arg(args, "gcmap", "0")); + launch_session.enable_hdr = util::from_view(get_arg(args, "hdrMode", "0")); + 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); - return launch_session; } @@ -486,7 +508,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 +517,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(); @@ -563,32 +587,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) { @@ -618,19 +616,39 @@ namespace nvhttp { 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.mac", platf::get_mac_address(net::addr_to_normalized_string(local_endpoint.address()))); 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"); - } - else if (video::active_hevc_mode == 2) { - tree.put("root.ServerCodecModeSupport", "259"); + // 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) { @@ -757,8 +775,11 @@ namespace nvhttp { } } + host_audio = util::from_view(get_arg(args, "localAudioPlayMode")); + auto launch_session = make_launch_session(host_audio, args); + 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,11 +789,10 @@ namespace nvhttp { } } - host_audio = util::from_view(get_arg(args, "localAudioPlayMode")); - rtsp_stream::launch_session_raise(make_launch_session(host_audio, args)); + rtsp_stream::launch_session_raise(launch_session); 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", "rtsp://"s + net::addr_to_url_escaped_string(request->local_endpoint().address()) + ':' + std::to_string(map_port(rtsp_stream::RTSP_SETUP_PORT))); tree.put("root.gamesession", 1); } @@ -843,7 +863,7 @@ namespace nvhttp { 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", "rtsp://"s + net::addr_to_url_escaped_string(request->local_endpoint().address()) + ':' + std::to_string(map_port(rtsp_stream::RTSP_SETUP_PORT))); tree.put("root.resume", 1); } @@ -906,6 +926,7 @@ namespace nvhttp { auto port_http = map_port(PORT_HTTP); auto port_https = 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]; @@ -993,21 +1014,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) { diff --git a/src/platform/common.h b/src/platform/common.h index cdb5785e076..3301803a916 100644 --- a/src/platform/common.h +++ b/src/platform/common.h @@ -13,6 +13,7 @@ #include "src/main.h" #include "src/thread_safe.h" #include "src/utility.h" +#include "src/video_colorspace.h" extern "C" { #include @@ -45,37 +46,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 +187,7 @@ namespace platf { vaapi, dxgi, cuda, + videotoolbox, unknown }; @@ -153,8 +223,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 +241,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 +349,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 +380,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 +393,13 @@ namespace platf { prepare_to_derive_context(int hw_device_type) { return 0; }; + }; - virtual ~hwdevice_t() = default; + 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; + + nvenc::nvenc_base *nvenc = nullptr; }; enum class capture_e : int { @@ -284,7 +444,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 +460,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 +481,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. @@ -369,7 +545,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 * @@ -383,7 +559,7 @@ namespace platf { display_names(mem_type_e hwdevice_type); 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,10 +587,23 @@ 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 @@ -448,11 +637,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/cuda.cpp b/src/platform/linux/cuda.cpp index c2a2e0fd591..0a04893682f 100644 --- a/src/platform/linux/cuda.cpp +++ b/src/platform/linux/cuda.cpp @@ -88,7 +88,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 +145,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 +183,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 +223,19 @@ namespace cuda { } }; - std::shared_ptr - make_hwdevice(int width, int height, bool vram) { + 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)) { @@ -675,9 +675,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 diff --git a/src/platform/linux/cuda.cu b/src/platform/linux/cuda.cu index 107075d99cd..863e3f944fe 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) { @@ -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..d5b97d65051 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,8 @@ 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); int init(); } // namespace cuda @@ -109,7 +111,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..448051f446b 100644 --- a/src/platform/linux/graphics.cpp +++ b/src/platform/linux/graphics.cpp @@ -193,7 +193,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()); @@ -607,27 +607,8 @@ namespace egl { } 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]; - }; - - if (color_range > 1) { - // Full range - ++color_p; - } + 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), @@ -741,7 +722,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)), diff --git a/src/platform/linux/graphics.h b/src/platform/linux/graphics.h index fbb0e92d3b9..ee72c46e766 100644 --- a/src/platform/linux/graphics.h +++ b/src/platform/linux/graphics.h @@ -14,6 +14,7 @@ #include "src/main.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) @@ -327,7 +328,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..bccbf8ad8d2 100644 --- a/src/platform/linux/input.cpp +++ b/src/platform/linux/input.cpp @@ -133,7 +133,7 @@ namespace platf { } }); - using mail_evdev_t = std::tuple; + using mail_evdev_t = std::tuple; struct keycode_t { std::uint32_t keycode; @@ -452,7 +452,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 { @@ -628,13 +628,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; @@ -770,9 +770,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 +791,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 +801,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, @@ -877,7 +884,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 +1048,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)); } } } @@ -1480,9 +1487,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 +1532,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 +1567,69 @@ namespace platf { libevdev_uinput_write_event(uinput.get(), EV_SYN, SYN_REPORT, 0); } + /** + * @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 + } + /** * @brief Initialize a new keyboard and return it. * @@ -1818,4 +1896,13 @@ namespace platf { 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/linux/kmsgrab.cpp b/src/platform/linux/kmsgrab.cpp index 094aee5f4e9..bfbe3935c1d 100644 --- a/src/platform/linux/kmsgrab.cpp +++ b/src/platform/linux/kmsgrab.cpp @@ -242,6 +242,23 @@ namespace platf { return -1; } + // 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; return -1; @@ -423,6 +440,7 @@ namespace platf { } file_t fd; + file_t render_fd; plane_res_t plane_res; }; @@ -550,7 +568,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; @@ -592,7 +610,7 @@ 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; @@ -768,13 +786,13 @@ 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 { if (mem_type == mem_type_e::vaapi) { - return va::make_hwdevice(width, height, false); + return va::make_avcodec_encode_device(width, height, false); } - return std::make_shared(); + return std::make_unique(); } capture_e @@ -843,10 +861,10 @@ 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 { 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); } BOOST_LOG(error) << "Unsupported pixel format for egl::display_vram_t: "sv << platf::from_pix_fmt(pix_fmt); @@ -966,7 +984,7 @@ namespace platf { return -1; } - if (!va::validate(card.fd.el)) { + if (!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; } @@ -1023,7 +1041,7 @@ namespace platf { // 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; diff --git a/src/platform/linux/misc.cpp b/src/platform/linux/misc.cpp index 73f6011a144..aee35b9e28a 100644 --- a/src/platform/linux/misc.cpp +++ b/src/platform/linux/misc.cpp @@ -2,6 +2,12 @@ * @file src/misc.cpp * @brief todo */ + +// Required for in6_pktinfo with glibc headers +#ifndef _GNU_SOURCE + #define _GNU_SOURCE 1 +#endif + // standard includes #include @@ -157,7 +163,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 +249,102 @@ namespace platf { lifetime::exit_sunshine(0, true); } + 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; + + msg.msg_control = cmbuf.buf; + msg.msg_controllen = sizeof(cmbuf.buf); + + // 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; - auto addr_bytes = address_v6.to_bytes(); - memcpy(&saddr_v6.sin6_addr, addr_bytes.data(), sizeof(saddr_v6.sin6_addr)); + cmbuflen += CMSG_SPACE(sizeof(pktInfo)); - addr = (struct sockaddr *) &saddr_v6; - addr_len = sizeof(saddr_v6); + 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 +353,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 +412,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,6 +452,101 @@ 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; + } + class qos_t: public deinit_t { public: qos_t(int sockfd, int level, int option): diff --git a/src/platform/linux/publish.cpp b/src/platform/linux/publish.cpp index 367c7aa3a20..e83bfb7009e 100644 --- a/src/platform/linux/publish.cpp +++ b/src/platform/linux/publish.cpp @@ -55,10 +55,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 +107,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 */ diff --git a/src/platform/linux/vaapi.cpp b/src/platform/linux/vaapi.cpp index 4a1e7df23ba..18b0dff8998 100644 --- a/src/platform/linux/vaapi.cpp +++ b/src/platform/linux/vaapi.cpp @@ -290,9 +290,9 @@ namespace va { } int - vaapi_make_hwdevice_ctx(platf::hwdevice_t *base, AVBufferRef **hw_device_buf); + vaapi_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *encode_device, 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) { @@ -304,7 +304,7 @@ namespace va { 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) { @@ -398,8 +398,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; @@ -526,7 +526,7 @@ namespace va { } int - vaapi_make_hwdevice_ctx(platf::hwdevice_t *base, AVBufferRef **hw_device_buf) { + vaapi_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *base, AVBufferRef **hw_device_buf) { if (!va::initialize) { BOOST_LOG(warning) << "libva not loaded"sv; return -1; @@ -653,10 +653,10 @@ namespace va { 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 +665,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 +674,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 +686,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..95760e55bd4 100644 --- a/src/platform/linux/vaapi.h +++ b/src/platform/linux/vaapi.h @@ -18,12 +18,12 @@ 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 diff --git a/src/platform/linux/wlgrab.cpp b/src/platform/linux/wlgrab.cpp index 6cf7fb78070..b57b332d7b2 100644 --- a/src/platform/linux/wlgrab.cpp +++ b/src/platform/linux/wlgrab.cpp @@ -215,13 +215,13 @@ 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 { if (mem_type == platf::mem_type_e::vaapi) { - return va::make_hwdevice(width, height, false); + return va::make_avcodec_encode_device(width, height, false); } - return std::make_shared(); + return std::make_unique(); } std::shared_ptr @@ -323,13 +323,13 @@ 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 { 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); } - return std::make_shared(); + return std::make_unique(); } int diff --git a/src/platform/linux/x11grab.cpp b/src/platform/linux/x11grab.cpp index ad8ef0343ff..6bd3018cf74 100644 --- a/src/platform/linux/x11grab.cpp +++ b/src/platform/linux/x11grab.cpp @@ -553,19 +553,19 @@ 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 { if (mem_type == mem_type_e::vaapi) { - return va::make_hwdevice(width, height, false); + return va::make_avcodec_encode_device(width, height, false); } #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 diff --git a/src/platform/macos/av_img_t.h b/src/platform/macos/av_img_t.h index f42f9ceb12e..6002bdcb221 100644 --- a/src/platform/macos/av_img_t.h +++ b/src/platform/macos/av_img_t.h @@ -10,10 +10,43 @@ #include namespace platf { - struct av_img_t: public img_t { - CVPixelBufferRef pixel_buffer = nullptr; - CMSampleBufferRef sample_buffer = nullptr; + struct av_sample_buf_t { + av_sample_buf_t(CMSampleBufferRef buf): + buf((CMSampleBufferRef) CFRetain(buf)) {} + + ~av_sample_buf_t() { + CFRelease(buf); + } + + CMSampleBufferRef buf; + }; + + struct av_pixel_buf_t { + av_pixel_buf_t(CVPixelBufferRef buf): + buf((CVPixelBufferRef) CFRetain(buf)), + locked(false) {} - ~av_img_t(); + uint8_t * + lock() { + if (!locked) { + CVPixelBufferLockBaseAddress(buf, kCVPixelBufferLock_ReadOnly); + } + return (uint8_t *) CVPixelBufferGetBaseAddress(buf); + } + + ~av_pixel_buf_t() { + if (locked) { + CVPixelBufferUnlockBaseAddress(buf, kCVPixelBufferLock_ReadOnly); + } + CFRelease(buf); + } + + CVPixelBufferRef buf; + bool locked; + }; + + struct av_img_t: public img_t { + std::shared_ptr sample_buffer; + std::shared_ptr pixel_buffer; }; } // namespace platf diff --git a/src/platform/macos/av_video.h b/src/platform/macos/av_video.h index 83eabb8ebd4..4bfa00ac006 100644 --- a/src/platform/macos/av_video.h +++ b/src/platform/macos/av_video.h @@ -20,11 +20,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); diff --git a/src/platform/macos/av_video.m b/src/platform/macos/av_video.m index 6e3a9f81f66..c64597656cc 100644 --- a/src/platform/macos/av_video.m +++ b/src/platform/macos/av_video.m @@ -39,11 +39,6 @@ - (id)initWithDisplay:(CGDirectDisplayID)displayID frameRate:(int)frameRate { 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.minFrameDuration = CMTimeMake(1, frameRate); self.session = [[AVCaptureSession alloc] init]; self.videoOutputs = [[NSMapTable alloc] init]; @@ -77,48 +72,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 +83,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..f424cf4ece2 100644 --- a/src/platform/macos/display.mm +++ b/src/platform/macos/display.mm @@ -20,18 +20,6 @@ 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; @@ -43,11 +31,6 @@ 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); - std::shared_ptr img_out; if (!pull_free_image_cb(img_out)) { // got interrupt signal @@ -56,25 +39,18 @@ } 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); + CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); - size_t extraPixels[4]; - CVPixelBufferGetExtendedPixels(pixelBuffer, &extraPixels[0], &extraPixels[1], &extraPixels[2], &extraPixels[3]); + av_img->sample_buffer = std::make_shared(sampleBuffer); + av_img->pixel_buffer = std::make_shared(pixelBuffer); + img_out->data = av_img->pixel_buffer->lock(); - img_out->width = CVPixelBufferGetWidth(pixelBuffer) + extraPixels[0] + extraPixels[1]; - img_out->height = CVPixelBufferGetHeight(pixelBuffer) + extraPixels[2] + extraPixels[3]; + img_out->width = CVPixelBufferGetWidth(pixelBuffer); + img_out->height = CVPixelBufferGetHeight(pixelBuffer); img_out->row_pitch = CVPixelBufferGetBytesPerRow(pixelBuffer); img_out->pixel_pitch = img_out->row_pitch / img_out->width; - if (!push_captured_image_cb(std::move(img_out), false)) { + if (!push_captured_image_cb(std::move(img_out), true)) { // got interrupt signal // returning false here stops capture backend return false; @@ -94,17 +70,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; } @@ -119,28 +95,14 @@ auto signal = [av_capture capture:^(CMSampleBufferRef sampleBuffer) { auto av_img = (av_img_t *) img; - 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); - - av_img->sample_buffer = sampleBuffer; - av_img->pixel_buffer = pixelBuffer; - img->data = (uint8_t *) CVPixelBufferGetBaseAddress(pixelBuffer); - size_t extraPixels[4]; - CVPixelBufferGetExtendedPixels(pixelBuffer, &extraPixels[0], &extraPixels[1], &extraPixels[2], &extraPixels[3]); + av_img->sample_buffer = std::make_shared(sampleBuffer); + av_img->pixel_buffer = std::make_shared(pixelBuffer); + img->data = av_img->pixel_buffer->lock(); - img->width = CVPixelBufferGetWidth(pixelBuffer) + extraPixels[0] + extraPixels[1]; - img->height = CVPixelBufferGetHeight(pixelBuffer) + extraPixels[2] + extraPixels[3]; + img->width = CVPixelBufferGetWidth(pixelBuffer); + img->height = CVPixelBufferGetHeight(pixelBuffer); img->row_pitch = CVPixelBufferGetBytesPerRow(pixelBuffer); img->pixel_pitch = img->row_pitch / img->width; @@ -173,7 +135,7 @@ 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; } diff --git a/src/platform/macos/input.cpp b/src/platform/macos/input.cpp index 78079e3f386..1401c169cae 100644 --- a/src/platform/macos/input.cpp +++ b/src/platform/macos/input.cpp @@ -288,8 +288,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; } @@ -440,6 +448,69 @@ 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() }; @@ -470,7 +541,7 @@ const KeyCodeMap kKeyCodesMap[] = { 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 +563,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/misc.mm b/src/platform/macos/misc.mm index fb2b41b2b39..5281cc864ea 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 + #define __APPLE_USE_RFC_3542 1 +#endif + #include #include #include @@ -15,6 +21,7 @@ #include "src/main.h" #include "src/platform/common.h" +#include #include using namespace std::literals; @@ -161,7 +168,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,12 +252,134 @@ lifetime::exit_sunshine(0, true); } + 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; + } + 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 diff --git a/src/platform/macos/nv12_zero_device.cpp b/src/platform/macos/nv12_zero_device.cpp index 21046be3054..f376bdcd413 100644 --- a/src/platform/macos/nv12_zero_device.cpp +++ b/src/platform/macos/nv12_zero_device.cpp @@ -3,7 +3,6 @@ * @brief todo */ #include "src/platform/macos/nv12_zero_device.h" -#include "src/platform/macos/av_img_t.h" #include "src/video.h" @@ -18,45 +17,29 @@ namespace platf { av_frame_free(&frame); } - util::safe_ptr av_frame; + void + free_buffer(void *opaque, uint8_t *data) { + CVPixelBufferRelease((CVPixelBufferRef) data); + } 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); + // Release any existing CVPixelBuffer previously retained for encoding + av_buffer_unref(&av_frame->buf[0]); - const uint8_t *data = (const uint8_t *) CVPixelBufferGetBaseAddressOfPlane(av_img->pixel_buffer, 0) - left_pad - (top_pad * img.width); - - 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, NULL, 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 +53,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, 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; // we never use this pointer but it's existence is checked/used - // by the platform independed code + // 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..f1ee2702aad 100644 --- a/src/platform/macos/nv12_zero_device.h +++ b/src/platform/macos/nv12_zero_device.h @@ -5,10 +5,15 @@ #pragma once #include "src/platform/common.h" +#include "src/platform/macos/av_img_t.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 +26,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, pixel_format_fn_t pixel_format_fn); int convert(img_t &img); int set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx); - void - set_colorspace(std::uint32_t colorspace, std::uint32_t color_range); + + private: + util::safe_ptr av_frame; }; } // namespace platf diff --git a/src/platform/macos/publish.cpp b/src/platform/macos/publish.cpp index 4cb80b8dbdb..8fca07b848a 100644 --- a/src/platform/macos/publish.cpp +++ b/src/platform/macos/publish.cpp @@ -55,10 +55,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 +107,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 */ diff --git a/src/platform/windows/audio.cpp b/src/platform/windows/audio.cpp index eac55e89e22..c02f1626fd6 100644 --- a/src/platform/windows/audio.cpp +++ b/src/platform/windows/audio.cpp @@ -2,6 +2,7 @@ * @file src/platform/windows/audio.cpp * @brief todo */ +#define INITGUID #include #include #include @@ -12,9 +13,7 @@ #include -#define INITGUID -#include -#undef INITGUID +#include #include "src/config.h" #include "src/main.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) @@ -457,6 +451,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 +477,10 @@ namespace platf::audio { if (audio_client) { audio_client->Stop(); } + + if (mmcss_task_handle) { + AvRevertMmThreadCharacteristics(mmcss_task_handle); + } } private: @@ -537,9 +543,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 +593,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 { diff --git a/src/platform/windows/display.h b/src/platform/windows/display.h index 2496cd3f55e..2d480c5954c 100644 --- a/src/platform/windows/display.h +++ b/src/platform/windows/display.h @@ -13,6 +13,7 @@ #include "src/platform/common.h" #include "src/utility.h" +#include "src/video.h" namespace platf::dxgi { extern const char *format_str[]; @@ -80,23 +81,71 @@ 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; + LONG texture_width; + LONG texture_height; + + LONG topleft_x; + LONG topleft_y; + + LONG display_width; + LONG display_height; + DXGI_MODE_ROTATION display_rotation; + shader_res_t input_res; D3D11_VIEWPORT cursor_view; @@ -108,7 +157,6 @@ namespace platf::dxgi { public: dup_t dup; bool has_frame {}; - bool use_dwmflush {}; std::chrono::steady_clock::time_point last_protected_content_warning_time {}; capture_e @@ -126,21 +174,32 @@ 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,7 +209,44 @@ 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; @@ -193,6 +289,9 @@ namespace platf::dxgi { 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; @@ -215,8 +314,14 @@ namespace platf::dxgi { int init(const ::video::config_t &config, const std::string &display_name); - std::shared_ptr - make_hwdevice(pix_fmt_e pix_fmt) 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; sampler_state_t sampler_linear; @@ -224,8 +329,8 @@ namespace platf::dxgi { 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; diff --git a/src/platform/windows/display_base.cpp b/src/platform/windows/display_base.cpp index e4483ae77d1..50f72698859 100644 --- a/src/platform/windows/display_base.cpp +++ b/src/platform/windows/display_base.cpp @@ -17,6 +17,7 @@ typedef long NTSTATUS; #include "src/config.h" #include "src/main.h" #include "src/platform/common.h" +#include "src/stat_trackers.h" #include "src/video.h" namespace platf { @@ -32,10 +33,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 +75,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 +97,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 +154,8 @@ namespace platf::dxgi { SetThreadExecutionState(ES_CONTINUOUS); }); + stat_trackers::min_max_avg_tracker sleep_overshoot_tracker; + 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 +164,65 @@ 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); + + 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() - sleep_target; + sleep_overshoot_tracker.collect_and_callback_on_interval(overshoot_ns.count() / 1000000., print_info, 20s); + } + + 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, 1000ms, *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; + } } - 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 +242,11 @@ namespace platf::dxgi { BOOST_LOG(error) << "Unrecognized capture status ["sv << (int) status << ']'; return status; } + + status = dup.release_frame(); + if (status != platf::capture_e::ok) { + return status; + } } return capture_e::ok; @@ -334,8 +408,6 @@ namespace platf::dxgi { // 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); @@ -389,6 +461,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); @@ -470,21 +553,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 +575,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; @@ -606,6 +725,14 @@ namespace platf::dxgi { 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) << ']'; + display_refresh_rate = dup_desc.ModeDesc.RefreshRate; + double display_refresh_rate_decimal = (double) display_refresh_rate.Numerator / display_refresh_rate.Denominator; + BOOST_LOG(info) << "Display refresh rate [" << display_refresh_rate_decimal << "Hz]"; + display_refresh_rate_rounded = lround(display_refresh_rate_decimal); + + client_frame_rate = config.framerate; + BOOST_LOG(info) << "Requested frame rate [" << client_frame_rate << "fps]"; + dxgi::output6_t output6 {}; status = output->QueryInterface(IID_IDXGIOutput6, (void **) &output6); if (SUCCEEDED(status)) { @@ -628,6 +755,17 @@ namespace platf::dxgi { // 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; } diff --git a/src/platform/windows/display_ram.cpp b/src/platform/windows/display_ram.cpp index 631abce7be8..ad078d12704 100644 --- a/src/platform/windows/display_ram.cpp +++ b/src/platform/windows/display_ram.cpp @@ -389,4 +389,10 @@ namespace platf::dxgi { 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..71310c90c16 100644 --- a/src/platform/windows/display_vram.cpp +++ b/src/platform/windows/display_vram.cpp @@ -16,9 +16,17 @@ extern "C" { #include "display.h" #include "misc.h" +#include "src/config.h" #include "src/main.h" +#include "src/nvenc/nvenc_config.h" +#include "src/nvenc/nvenc_d3d11.h" +#include "src/nvenc/nvenc_utils.h" #include "src/video.h" +#include + +#include + #define SUNSHINE_SHADERS_DIR SUNSHINE_ASSETS_DIR "/shaders/directx" 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 @@ -336,7 +345,7 @@ namespace platf::dxgi { 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 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 +370,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()) { @@ -413,110 +422,32 @@ namespace platf::dxgi { } 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 +464,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 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); - float info_in[16 / sizeof(float)] { 1.0f / (float) out_width_f }; // aligned to 16-byte - info_scene = make_buffer(device.get(), info_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 +497,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 +513,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 +552,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 +567,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 +582,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 +596,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 +647,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 +654,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 +721,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 +754,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 @@ -892,7 +955,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 +991,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 +1014,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 +1115,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 +1136,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, @@ -1157,8 +1223,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()) { @@ -1278,36 +1344,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; } } @@ -1332,8 +1409,8 @@ namespace platf::dxgi { 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++; return img; @@ -1464,82 +1541,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/input.cpp b/src/platform/windows/input.cpp index c94904267b3..ca9cfba7b93 100644 --- a/src/platform/windows/input.cpp +++ b/src/platform/windows/input.cpp @@ -2,17 +2,29 @@ * @file src/platform/windows/input.cpp * @brief todo */ +#define WINVER 0x0A00 #include #include #include +#include "keylayout.h" #include "misc.h" #include "src/config.h" #include "src/main.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 +38,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,6 +54,140 @@ 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 @@ -71,32 +208,57 @@ 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(); 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 +268,94 @@ 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(); } + /** + * @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 +366,7 @@ namespace platf { } } - std::vector> gamepads; + std::vector gamepads; client_t client; }; @@ -164,7 +382,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 +390,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 +409,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 +426,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 +452,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 +514,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 +568,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 +575,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 +591,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 +606,7 @@ namespace platf { case VK_LEFT: case VK_RIGHT: case VK_DIVIDE: + case VK_APPS: ki.dwFlags |= KEYEVENTF_EXTENDEDKEY; break; default: @@ -394,6 +620,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 +1152,54 @@ 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 == "ps4"sv || 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 (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 (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; + } + + return raw->vigem->alloc_gamepad_internal(id, feedback_queue, selectedGamepadType); } void @@ -445,11 +1213,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 {}; - return vigem_target_x360_update(client, gp, xusb); + 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; + } + + /** + * @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 +1298,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 +1331,9 @@ 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; + return (DS4_SPECIAL_BUTTONS) buttons; } @@ -535,13 +1349,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 = ds4_buttons(gamepad_state) | ds4_dpad(gamepad_state); report.bSpecial = ds4_special_buttons(gamepad_state); report.bTriggerL = gamepad_state.lt; @@ -552,10 +1369,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); + + // 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; + } - return vigem_target_ds4_update(client, gp, report); + // 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 +1422,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; + } } - if (!VIGEM_SUCCESS(status)) { - BOOST_LOG(warning) << "Couldn't send gamepad input to ViGEm ["sv << util::hex(status).to_string_view() << ']'; + // 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; + } + + 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 +1666,41 @@ 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) { + 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..455d5681817 100644 --- a/src/platform/windows/misc.cpp +++ b/src/platform/windows/misc.cpp @@ -33,14 +33,18 @@ #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 @@ -412,11 +416,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 +436,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; } @@ -492,17 +490,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,6 +532,20 @@ 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; } @@ -552,15 +565,21 @@ 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) { + 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) { 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); + 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(); @@ -591,14 +610,14 @@ 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); ret = CreateProcessAsUserW(user_token, NULL, (LPWSTR) wcmd.c_str(), @@ -626,12 +645,12 @@ 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); ret = CreateProcessW(NULL, (LPWSTR) wcmd.c_str(), NULL, @@ -645,7 +664,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); } /** @@ -740,6 +759,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 +780,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 @@ -909,19 +938,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 +961,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): diff --git a/src/platform/windows/nvprefs/driver_settings.cpp b/src/platform/windows/nvprefs/driver_settings.cpp new file mode 100644 index 00000000000..54529fd5ddd --- /dev/null +++ b/src/platform/windows/nvprefs/driver_settings.cpp @@ -0,0 +1,289 @@ +#include "nvprefs_common.h" + +#include "driver_settings.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; + + auto [opengl_swapchain_saved, opengl_swapchain_our_value, opengl_swapchain_undo_value] = undo_data.get_opengl_swapchain(); + + if (opengl_swapchain_saved) { + 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 == opengl_swapchain_our_value) { + if (opengl_swapchain_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 = *opengl_swapchain_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; + + 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 (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..fbbc9aa1f57 --- /dev/null +++ b/src/platform/windows/nvprefs/driver_settings.h @@ -0,0 +1,36 @@ +#pragma once + +#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..e1010737d55 --- /dev/null +++ b/src/platform/windows/nvprefs/nvapi_opensource_wrapper.cpp @@ -0,0 +1,127 @@ +#include "nvprefs_common.h" + +#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..ba15dfe3084 --- /dev/null +++ b/src/platform/windows/nvprefs/nvprefs_common.cpp @@ -0,0 +1,25 @@ +#include "nvprefs_common.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; + } + +} // 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..7d4a661924e --- /dev/null +++ b/src/platform/windows/nvprefs/nvprefs_common.h @@ -0,0 +1,70 @@ +#pragma once + +// sunshine utility header for generic smart pointers +#include "src/utility.h" + +// sunshine boost::log severity levels +#include "src/main.h" + +// standard library headers +#include +#include +#include +#include +#include +#include +#include +#include + +// winapi headers +// disable clang-format header reordering +// clang-format off +#include +#include +// clang-format on + +// nvapi headers +// disable clang-format header reordering +// clang-format off +#include +#include +// clang-format on + +// boost headers +#include + +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); + +} // 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..961788aed2c --- /dev/null +++ b/src/platform/windows/nvprefs/nvprefs_interface.cpp @@ -0,0 +1,225 @@ +#include "nvprefs_common.h" + +#include "nvprefs_interface.h" + +#include "driver_settings.h" +#include "undo_data.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..43d588c37a1 --- /dev/null +++ b/src/platform/windows/nvprefs/nvprefs_interface.h @@ -0,0 +1,36 @@ +#pragma once + +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..7abd81f7279 --- /dev/null +++ b/src/platform/windows/nvprefs/undo_data.cpp @@ -0,0 +1,71 @@ +#include "nvprefs_common.h" + +#include "undo_data.h" + +namespace { + + const auto opengl_swapchain_our_value_key = "/opengl_swapchain/our_value"; + const auto opengl_swapchain_undo_value_key = "/opengl_swapchain/undo_value"; + +} // namespace + +namespace nvprefs { + + void + undo_data_t::set_opengl_swapchain(uint32_t our_value, std::optional undo_value) { + data.set_at_pointer(opengl_swapchain_our_value_key, our_value); + if (undo_value) { + data.set_at_pointer(opengl_swapchain_undo_value_key, *undo_value); + } + else { + data.set_at_pointer(opengl_swapchain_undo_value_key, nullptr); + } + } + + std::tuple> + undo_data_t::get_opengl_swapchain() const { + auto get_value = [this](const auto &key) -> std::tuple> { + try { + auto value = data.at_pointer(key); + if (value.is_null()) { + return { true, std::nullopt }; + } + else if (value.is_number()) { + return { true, value.template to_number() }; + } + } + catch (...) { + } + error_message(std::string("Couldn't find ") + key + " element"); + return { false, std::nullopt }; + }; + + auto [our_value_present, our_value] = get_value(opengl_swapchain_our_value_key); + auto [undo_value_present, undo_value] = get_value(opengl_swapchain_undo_value_key); + + if (!our_value_present || !undo_value_present || !our_value) { + return { false, 0, std::nullopt }; + } + + return { true, *our_value, undo_value }; + } + + std::string + undo_data_t::write() const { + return boost::json::serialize(data); + } + + void + undo_data_t::read(const std::vector &buffer) { + data = boost::json::parse(std::string_view(buffer.data(), buffer.size())); + } + + void + undo_data_t::merge(const undo_data_t &newer_data) { + auto [opengl_swapchain_saved, opengl_swapchain_our_value, opengl_swapchain_undo_value] = newer_data.get_opengl_swapchain(); + if (opengl_swapchain_saved) { + set_opengl_swapchain(opengl_swapchain_our_value, opengl_swapchain_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..999483e190f --- /dev/null +++ b/src/platform/windows/nvprefs/undo_data.h @@ -0,0 +1,32 @@ +#pragma once + +namespace nvprefs { + + class undo_data_t { + public: + void + set_opengl_swapchain(uint32_t our_value, std::optional undo_value); + + std::tuple> + get_opengl_swapchain() const; + + void + write(std::ostream &stream) const; + + std::string + write() const; + + void + read(std::istream &stream); + + void + read(const std::vector &buffer); + + void + merge(const undo_data_t &newer_data); + + private: + boost::json::value 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..8a45955005d --- /dev/null +++ b/src/platform/windows/nvprefs/undo_file.cpp @@ -0,0 +1,154 @@ +#include "nvprefs_common.h" + +#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..46dcba61226 --- /dev/null +++ b/src/platform/windows/nvprefs/undo_file.h @@ -0,0 +1,29 @@ +#pragma once + +#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..187868b6fa7 100644 --- a/src/platform/windows/publish.cpp +++ b/src/platform/windows/publish.cpp @@ -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 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..227c6fda148 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 @@ -24,6 +24,7 @@ #include "crypto.h" #include "main.h" #include "platform/common.h" +#include "system_tray.h" #include "utility.h" #ifdef _WIN32 @@ -101,7 +102,7 @@ namespace proc { } int - proc_t::execute(int app_id) { + proc_t::execute(int app_id, rtsp_stream::launch_session_t launch_session) { // Ensure starting from a clean slate terminate(); @@ -116,10 +117,32 @@ 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; + } + 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 @@ -158,13 +181,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; } @@ -200,6 +227,8 @@ namespace proc { } } + _app_launch_time = std::chrono::steady_clock::now(); + fg.disable(); return 0; @@ -210,9 +239,17 @@ namespace proc { if (placebo || _process.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(); } @@ -221,9 +258,8 @@ namespace proc { void proc_t::terminate() { + bool has_run = _app_id > 0; std::error_code ec; - - // Ensure child process is terminated placebo = false; process_end(_process, _process_handle); _process = bp::child(); @@ -256,6 +292,13 @@ namespace proc { } _pipe.reset(); +#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 + // 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 } const std::vector & @@ -281,6 +324,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 +558,7 @@ 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); std::vector prep_cmds; if (!exclude_global_prep.value_or(false)) { @@ -568,6 +617,7 @@ namespace proc { } ctx.elevated = elevated.value_or(false); + ctx.auto_detach = auto_detach.value_or(true); 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..c1ff28afd5b 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 { @@ -51,6 +52,7 @@ namespace proc { std::string image_path; std::string id; bool elevated; + bool auto_detach; }; class proc_t { @@ -65,7 +67,7 @@ namespace proc { _apps(std::move(apps)) {} int - execute(int app_id); + execute(int app_id, rtsp_stream::launch_session_t launch_session); /** * @return _app_id if a process is running, otherwise returns 0 @@ -81,7 +83,8 @@ namespace proc { get_apps(); std::string get_app_image(int app_id); - + std::string + get_last_run_app_name(); void terminate(); @@ -91,6 +94,7 @@ 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 {}; diff --git a/src/rtsp.cpp b/src/rtsp.cpp index a2e4a2fc3b1..5a12253de17 100644 --- a/src/rtsp.cpp +++ b/src/rtsp.cpp @@ -225,7 +225,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 +233,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; } @@ -492,10 +492,22 @@ 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; + if (video::active_hevc_mode != 1) { ss << "sprop-parameter-sets=AAAAAU"sv << std::endl; } + if (video::last_encoder_probe_supported_ref_frames_invalidation) { + ss << "x-nv-video[0].refPicInvalidation=1"sv << std::endl; + } + + if (video::active_av1_mode != 1) { + ss << "a=rtpmap:98 AV1/90000"sv << 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]; @@ -693,13 +705,20 @@ namespace rtsp_stream { } } - 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, {}); return; } + if (config.monitor.videoFormat == 2 && video::active_av1_mode == 1) { + BOOST_LOG(warning) << "AV1 is disabled, yet the client requested AV1"sv; + + respond(sock, &option, 400, "BAD REQUEST", req->sequenceNumber, {}); + return; + } + auto session = stream::session::alloc(config, launch_session->gcm_key, launch_session->iv); auto slot = server->accept(session); @@ -747,7 +766,7 @@ namespace rtsp_stream { server.map("PLAY"sv, &cmd_play); boost::system::error_code ec; - if (server.bind(map_port(rtsp_stream::RTSP_SETUP_PORT), ec)) { + if (server.bind(net::af_from_enum_string(config::sunshine.address_family), 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(); shutdown_event->raise(true); diff --git a/src/rtsp.h b/src/rtsp.h index 2b0355fd947..4d426070c2c 100644 --- a/src/rtsp.h +++ b/src/rtsp.h @@ -17,6 +17,15 @@ namespace rtsp_stream { crypto::aes_t iv; bool host_audio; + std::string unique_id; + int width; + int height; + int fps; + int gcmap; + int appid; + int surround_info; + bool enable_hdr; + bool enable_sops; }; void 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..c212b255f8c 100644 --- a/src/stream.cpp +++ b/src/stream.cpp @@ -25,6 +25,7 @@ extern "C" { #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( @@ -146,6 +157,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; @@ -222,8 +258,8 @@ 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; } @@ -320,10 +356,13 @@ namespace stream { safe::shared_t::ptr_t broadcast_ref; + boost::asio::ip::address localAddress; + struct { int lowseq; udp::endpoint peer; safe::mail_raw_t::event_t idr_events; + safe::mail_raw_t::event_t> invalidate_ref_frames_events; std::unique_ptr qos; } video; @@ -350,7 +389,7 @@ namespace stream { net::peer_t peer; std::uint8_t seq; - platf::rumble_queue_t rumble_queue; + platf::feedback_queue_t feedback_queue; safe::mail_raw_t::event_t hdr_queue; } control; @@ -430,6 +469,12 @@ namespace stream { 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); + return session_p; } @@ -621,38 +666,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; } @@ -733,7 +847,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) { @@ -759,7 +873,6 @@ namespace stream { std::copy(payload.end() - 16, payload.end(), std::begin(iv)); } - input::print(plaintext.data()); input::passthrough(session->input, std::move(plaintext)); }); @@ -796,30 +909,23 @@ 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]) { + // 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); - - return; } - - // 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 @@ -860,22 +966,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; @@ -1028,13 +1132,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 +1182,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 +1244,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 @@ -1168,7 +1281,7 @@ 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(); } auto peer_address = session->video.peer.address(); @@ -1179,6 +1292,7 @@ namespace stream { (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 +1300,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[x].data(), + shards[x].size(), + (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; @@ -1267,9 +1390,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 +1418,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 +1443,41 @@ namespace stream { int start_broadcast(broadcast_ctx_t &ctx) { + 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 = map_port(CONTROL_PORT); auto video_port = map_port(VIDEO_STREAM_PORT); auto audio_port = 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(); @@ -1591,6 +1733,11 @@ namespace stream { // 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(); } @@ -1635,6 +1782,9 @@ 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; @@ -1650,7 +1800,7 @@ namespace stream { session->config = config; - session->control.rumble_queue = mail->queue(mail::rumble); + session->control.feedback_queue = mail->queue(mail::gamepad_feedback); session->control.hdr_queue = mail->event(mail::hdr); session->control.iv = iv; session->control.cipher = crypto::cipher::gcm_t { @@ -1658,6 +1808,7 @@ namespace stream { }; session->video.idr_events = mail->event(mail::idr); + session->video.invalidate_ref_frames_events = mail->event>(mail::invalidate_ref_frames); session->video.lowseq = 0; constexpr auto max_block_size = crypto::cipher::round_to_pkcs7_padded(2048); diff --git a/src/system_tray.cpp b/src/system_tray.cpp index ed66357a6ac..8a06a0a79c1 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 @@ -100,17 +109,18 @@ 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 @@ -268,5 +278,93 @@ namespace system_tray { 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) { + 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]; + sprintf(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) { + 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]; + sprintf(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) { + 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]; + sprintf(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 = "Sunshine"; + tray_update(&tray); + } + + /** + * @brief Spawns a notification for PIN Pairing. Clicking it opens the PIN Web UI Page + */ + void + update_tray_require_pin() { + 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 = "Sunshine"; + 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..f4713bcf014 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,7 +81,7 @@ namespace safe { return val; } - // pop and view shoud not be used interchangeably + // pop and view should not be used interchangeably const 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..55ef25fd173 100644 --- a/src/upnp.cpp +++ b/src/upnp.cpp @@ -91,6 +91,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() @@ -232,6 +310,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 +318,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; } @@ -270,6 +349,14 @@ namespace upnp { 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)); diff --git a/src/video.cpp b/src/video.cpp index 545e2fbdfea..1191a8684bf 100644 --- a/src/video.cpp +++ b/src/video.cpp @@ -7,6 +7,8 @@ #include #include +#include + extern "C" { #include #include @@ -16,6 +18,7 @@ extern "C" { #include "config.h" #include "input.h" #include "main.h" +#include "nvenc/nvenc_base.h" #include "platform/common.h" #include "sync.h" #include "video.h" @@ -44,9 +47,9 @@ 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 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>>; @@ -85,17 +88,16 @@ namespace video { 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); - - int - hwframe_ctx(ctx_t &ctx, platf::hwdevice_t *hwdevice, buffer_t &hwdevice_ctx, AVPixelFormat format); + 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 *); - 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 { @@ -157,10 +159,11 @@ 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); } @@ -247,12 +250,10 @@ namespace video { return sws ? 0 : -1; } - ~swdevice_t() override {} - // Store ownership when frame is hw_frame - frame_t hw_frame; + avcodec_frame_t hw_frame; - frame_t sw_frame; + avcodec_frame_t sw_frame; sws_t sws; // offset of input image to output frame in pixels @@ -260,15 +261,61 @@ namespace video { int offsetY; }; - 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 + enum flag_e : uint32_t { + DEFAULT = 0, + PARALLEL_ENCODING = 1 << 1, + 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 + }; + + 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 *)>; + + 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); + } + + 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; + } }; struct encoder_t { @@ -305,16 +352,13 @@ namespace video { option_t(const option_t &) = default; std::string name; - std::variant *, std::string, std::string *> value; + 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) } {} }; - AVHWDeviceType base_dev_type, derived_dev_type; - AVPixelFormat dev_pix_fmt; - - AVPixelFormat static_pix_fmt, dynamic_pix_fmt; + const std::unique_ptr platform_formats; struct { std::vector common_options; @@ -334,31 +378,45 @@ namespace video { operator[](flag_e flag) { return capabilities[(std::size_t) flag]; } - } hevc, h264; + } 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; - int flags; + virtual void + request_normal_frame() = 0; - std::function(platf::hwdevice_t *hwdevice)> make_hwdevice_ctx; + virtual void + invalidate_ref_frames(int64_t first_frame, int64_t last_frame) = 0; }; - 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 +426,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 +468,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 +528,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 +565,90 @@ 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); +#ifdef _WIN32 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, + 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 + {}, + std::nullopt, // QP + "av1_nvenc"s, + }, + { + // Common options + {}, + // SDR-specific options + {}, + // HDR-specific options + {}, + std::nullopt, // QP + "hevc_nvenc"s, + }, + { + // Common options + {}, + // SDR-specific options + {}, + // HDR-specific options + {}, + std::nullopt, // QP + "h264_nvenc"s, + }, + PARALLEL_ENCODING | REF_FRAMES_INVALIDATION // flags + }; +#elif !defined(__APPLE__) + static 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 }, + }, + // SDR-specific options + {}, + // HDR-specific options + {}, + std::nullopt, + "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 }, }, // SDR-specific options { @@ -469,10 +666,11 @@ 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 }, }, // SDR-specific options { @@ -482,22 +680,34 @@ namespace video { std::make_optional({ "qp"s, &config::video.qp }), "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 { "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 + {}, + std::make_optional({ "qp"s, &config::video.qp }), + "av1_qsv"s, + }, { // Common options { @@ -542,22 +752,39 @@ namespace video { std::make_optional({ "qp"s, &config::video.qp }), "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 { "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 }, + }, + {}, // SDR-specific options + {}, // HDR-specific options + std::make_optional({ "qp_p"s, &config::video.qp }), + "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 }, @@ -573,9 +800,9 @@ namespace video { { // 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 }, @@ -588,16 +815,39 @@ namespace video { std::make_optional({ "qp_p"s, &config::video.qp }), "h264_amf"s, }, - PARALLEL_ENCODING, - dxgi_make_hwdevice_ctx + PARALLEL_ENCODING }; #endif static 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 + std::make_optional("qp"s, &config::video.qp), + +#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. @@ -625,17 +875,28 @@ namespace video { std::make_optional("qp"s, &config::video.qp), "libx264"s, }, - H264_ONLY | PARALLEL_ENCODING, - - nullptr + H264_ONLY | PARALLEL_ENCODING }; #ifdef __linux__ static 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_YUV420P10, + vaapi_init_avcodec_hardware_input_buffer), + { + // Common options + { + { "async_depth"s, 1 }, + { "idr_interval"s, std::numeric_limits::max() }, + }, + {}, // SDR-specific options + {}, // HDR-specific options + std::make_optional("qp"s, &config::video.qp), + "av1_vaapi"s, + }, { // Common options { @@ -660,24 +921,38 @@ namespace video { 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 { "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 + 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 @@ -690,15 +965,14 @@ 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 std::nullopt, "h264_videotoolbox"s, }, - DEFAULT, - - nullptr + DEFAULT }; #endif @@ -721,13 +995,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; } @@ -761,7 +1037,7 @@ namespace video { // 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)); + auto display_names = platf::display_names(encoder.platform_formats->dev_type); int display_p = 0; if (display_names.empty()) { @@ -783,7 +1059,7 @@ 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); + auto disp = platf::display(encoder.platform_formats->dev_type, display_names[display_p], capture_ctxs.front().config); if (!disp) { return; } @@ -967,7 +1243,7 @@ namespace video { while (capture_ctx_queue->running()) { // 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 +1270,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 +1289,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 +1300,10 @@ namespace video { return ret; } + 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); @@ -1059,42 +1340,87 @@ namespace video { return 0; } - 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; + 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 -1; + } + + 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; + return nullptr; } - ctx_t ctx { avcodec_alloc_context3(codec) }; + avcodec_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; + switch (config.videoFormat) { + case 0: + ctx->profile = FF_PROFILE_H264_HIGH; + break; + + case 1: + ctx->profile = config.dynamicRange ? FF_PROFILE_HEVC_MAIN_10 : FF_PROFILE_HEVC_MAIN; + break; + + case 2: + // AV1 supports both 8 and 10 bit encoding with the same Main profile + ctx->profile = FF_PROFILE_AV1_MAIN; + break; } // B-frames delay decoder output, so never use them @@ -1120,96 +1446,70 @@ namespace video { ctx->flags |= (AV_CODEC_FLAG_CLOSED_GOP | AV_CODEC_FLAG_LOW_DELAY); ctx->flags2 |= AV_CODEC_FLAG2_FAST; - ctx->color_range = (config.encoderCscMode & 0x1) ? AVCOL_RANGE_JPEG : AVCOL_RANGE_MPEG; + auto colorspace = encode_device->colorspace; + auto avcodec_colorspace = avcodec_colorspace_from_sunshine_colorspace(colorspace); - 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) { - 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; - break; + ctx->color_range = avcodec_colorspace.range; + ctx->color_primaries = avcodec_colorspace.primaries; + ctx->color_trc = avcodec_colorspace.transfer_function; + ctx->colorspace = avcodec_colorspace.matrix; - 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; - 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; - break; - } - } - - BOOST_LOG(info) << "Color range: ["sv << ((config.encoderCscMode & 0x1) ? "JPEG"sv : "MPEG"sv) << ']'; - - AVPixelFormat sw_fmt; - if (config.dynamicRange == 0) { - sw_fmt = encoder.static_pix_fmt; - } - else { - sw_fmt = encoder.dynamic_pix_fmt; - } + auto sw_fmt = (colorspace.bit_depth == 10) ? platform_formats->avcodec_pix_fmt_10bit : platform_formats->avcodec_pix_fmt_8bit; // Used by cbs::make_sps_hevc ctx->sw_pix_fmt = sw_fmt; if (hardware) { - buffer_t hwdevice_ctx; + avcodec_buffer_t encoding_stream_context; - ctx->pix_fmt = encoder.dev_pix_fmt; + ctx->pix_fmt = platform_formats->avcodec_dev_pix_fmt; // Create the base hwdevice context - auto buf_or_error = encoder.make_hwdevice_ctx(hwdevice.get()); + auto buf_or_error = platform_formats->init_avcodec_hardware_input_buffer(encode_device.get()); if (buf_or_error.has_right()) { - return std::nullopt; + return nullptr; } - hwdevice_ctx = std::move(buf_or_error.left()); + 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 (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; + if (encode_device->prepare_to_derive_context(platform_formats->avcodec_derived_dev_type)) { + return nullptr; } - auto err = av_hwdevice_ctx_create_derived(&derived_hwdevice_ctx, encoder.derived_dev_type, hwdevice_ctx.get(), 0); + 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 std::nullopt; + return nullptr; } - hwdevice_ctx = std::move(derived_hwdevice_ctx); + encoding_stream_context = std::move(derived_context); } - if (hwframe_ctx(ctx, hwdevice.get(), hwdevice_ctx, sw_fmt)) { - return std::nullopt; + // 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; + } + + ctx->hw_frames_ctx = av_buffer_ref(frame_ref.get()); } ctx->slices = config.slicesPerFrame; @@ -1237,6 +1537,7 @@ namespace video { [&](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); @@ -1268,7 +1569,7 @@ namespace video { } if (!(encoder.flags & NO_RC_BUF_LIMIT)) { - if (!hardware && (ctx->slices > 1 || config.videoFormat != 0)) { + 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 @@ -1285,7 +1586,7 @@ namespace video { } else { BOOST_LOG(error) << "Couldn't set video quality: encoder "sv << encoder.name << " doesn't support qp"sv; - return std::nullopt; + return nullptr; } if (auto status = avcodec_open2(ctx.get(), codec, &options)) { @@ -1295,16 +1596,21 @@ namespace video { << video_format.name << "]: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, status); - return std::nullopt; + return nullptr; } - 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 +1638,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; - return std::make_optional(std::move(session)); + 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) << ']'; + } + + 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 +1727,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 +1747,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 +1757,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 +1790,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 +1830,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 +1867,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; } @@ -1506,7 +1898,7 @@ namespace video { std::vector> &synced_session_ctxs, encode_session_ctx_queue_t &encode_session_ctx_queue) { const auto &encoder = *chosen_encoder; - auto display_names = platf::display_names(map_base_dev_type(encoder.base_dev_type)); + auto display_names = platf::display_names(encoder.platform_formats->dev_type); int display_p = 0; if (display_names.empty()) { @@ -1536,7 +1928,7 @@ namespace video { while (encode_session_ctx_queue.running()) { // 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 +1974,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 +1992,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,15 +2008,14 @@ 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; }) @@ -1739,9 +2127,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 +2138,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 +2152,7 @@ namespace video { frame_nr, mail, images, config, display, - std::move(hwdevice), + std::move(encode_device), ref->reinit_event, *ref->encoder_p, channel_data); } @@ -1803,18 +2195,17 @@ 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); + reset_display(disp, encoder.platform_formats->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) { + 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 +2213,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 +2261,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 +2317,66 @@ 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); + auto autoselect_hevc = max_ref_frames_hevc >= 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); + auto autoselect_av1 = max_ref_frames_av1 >= 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 +2384,11 @@ 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; // 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 +2396,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 +2418,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. @@ -1984,6 +2431,29 @@ namespace video { 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 +2467,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 +2484,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 +2495,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 +2514,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,6 +2532,9 @@ 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; }); @@ -2073,12 +2551,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 +2569,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 +2622,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 +2636,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 +2657,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 +2728,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 +2755,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..fec5c38b343 100644 --- a/src/video.h +++ b/src/video.h @@ -7,6 +7,7 @@ #include "input.h" #include "platform/common.h" #include "thread_safe.h" +#include "video_colorspace.h" extern "C" { #include @@ -16,25 +17,19 @@ struct AVPacket; namespace video { struct packet_raw_t { - void - init_packet() { - this->av_packet = av_packet_alloc(); - } + virtual ~packet_raw_t() = default; - template - explicit packet_raw_t(P *user_data): - channel_data { user_data } { - init_packet(); - } + virtual bool + is_idr() = 0; - explicit packet_raw_t(std::nullptr_t): - channel_data { nullptr } { - init_packet(); - } + virtual int64_t + frame_index() = 0; - ~packet_raw_t() { - av_packet_free(&this->av_packet); - } + virtual uint8_t * + data() = 0; + + virtual size_t + data_size() = 0; struct replace_t { std::string_view old; @@ -46,11 +41,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 +123,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( diff --git a/src/video_colorspace.cpp b/src/video_colorspace.cpp new file mode 100644 index 00000000000..ca5e489bb4b --- /dev/null +++ b/src/video_colorspace.cpp @@ -0,0 +1,181 @@ +#include "video_colorspace.h" + +#include "main.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/apps.html b/src_assets/common/assets/web/apps.html index f3243036f96..bd09051d669 100644 --- a/src_assets/common/assets/web/apps.html +++ b/src_assets/common/assets/web/apps.html @@ -190,7 +190,7 @@

Applications

v-model="editForm.cmd" />
- The main application, if it is not specified, a processs is started + The main application, if it is not specified, a process is started that sleeps indefinitely
@@ -229,6 +229,25 @@

Applications

permissions to run properly. + +
+ + +
+ This will attempt to automatically detect launcher-type apps that close + quickly after launching another program or instance of themselves. When + a launcher-type app is detected, it is treated as a detached app. +
+
@@ -302,6 +321,26 @@ must be a PNG file. If not set, Sunshine will send default box image.
+
+
About Environment Variables: All commands get these environment variables by default:
+ + + + + + + + + + + + +
Var Name
SUNSHINE_APP_IDApp ID
SUNSHINE_APP_NAMEApp Name
SUNSHINE_CLIENT_WIDTHThe Width requested by the client
SUNSHINE_CLIENT_HEIGHTThe Height requested by the client
SUNSHINE_CLIENT_FPSThe FPS requested by the client
SUNSHINE_CLIENT_HDR(true/false) if HDR is enabled by the client
SUNSHINE_CLIENT_GCMAP(int) the requested gamepad mask, in a bitset/bitfield format
SUNSHINE_CLIENT_HOST_AUDIO(true/false) if the client has requested host audio
SUNSHINE_CLIENT_ENABLE_SOPS(true/false) if the client has requested the option to optimize the game for optimal streaming
SUNSHINE_CLIENT_AUDIO_CONFIGURATIONThe Audio Configuration requested by the client (2.0/5.1/7.1)
+
Example - QRes for Resolution Automation:
cmd /C <qres path>\QRes.exe /X:%SUNSHINE_CLIENT_WIDTH% /Y:%SUNSHINE_CLIENT_HEIGHT%
+
Example - Xrandr for Resolution Automation:
sh -c "xrandr --output HDMI-1 --mode \"${SUNSHINE_CLIENT_WIDTH}x${SUNSHINE_CLIENT_HEIGHT}\" --rate 60"
+
Example - displayplacer for Resolution Automation:
sh -c "displayplacer "id: res:${SUNSHINE_CLIENT_WIDTH}x${SUNSHINE_CLIENT_HEIGHT} hz:60 scaling:on origin:(0,0) degree:0""
+ +
@@ -94,10 +99,11 @@

Configuration

-
Choose which type of gamepad to Emulate on the host
+
Choose which type of gamepad to emulate on the host
@@ -565,7 +571,7 @@

Configuration

-
+
Configuration tools\dxgi-info.exe
- -
- - -
- Improves capture latency/smoothness during mouse movement.
- Disable if you encounter any VSync-related issues. -
-
-
+
Configuration
+ +
+ + +
Set the address family used by Sunshine
+
Set the family of ports used by Sunshine
+ +
+ Sunshine cannot use ports below 1024! +
+ +
+ Ports above 65535 are not available! +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ProtocolPortNote
TCP{{+effectivePort - 5}}
TCP{{+effectivePort}} + +
TCP{{+effectivePort + 1}}Web UI
TCP{{+effectivePort + 21}}
UDP{{+effectivePort + 9}} - {{+effectivePort + 11}}
+ +
+ Exposing the Web UI to the internet is a security risk! + Proceed at your own risk! +
@@ -673,7 +745,7 @@

Configuration

+ + + + + +
+ Allows the client to request AV1 Main 8-bit or 10-bit video streams.
+ AV1 is more CPU-intensive to encode, so enabling this may reduce performance when using software encoding. +
+
@@ -777,24 +871,6 @@

Configuration

Store Username/Password separately from Sunshine's state file.
- -
- - -
- The origin of the remote endpoint address that is not denied for HTTP method /pin -
-
@@ -848,41 +924,73 @@

Configuration

- - -
-
- - + + + + + + + +
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. +
- - + + + +
Adds preliminary encoding pass.
+ 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. +
-
- - +
+
+

+ +

+
+
+
+ + +
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. +
+
+
+ + +
Simpler form of entropy coding.
+ CAVLC needs around 10% more bitrate for same quality.
+ Only relevant for really old decoding devices. +
+
+
+
+
@@ -927,7 +1035,7 @@

Configuration

@@ -1002,10 +1110,11 @@

Configuration

- Success! Click 'Apply' to restart Sunshine and apply changes. This will terminate any running sessions. + Click 'Apply' to restart Sunshine and apply changes. + This will terminate any running sessions.
-
- Success! Sunshine is restarting to apply changes. +
+ Sunshine is restarting to apply changes.
@@ -1016,6 +1125,7 @@

Configuration

diff --git a/src_assets/common/assets/web/header.html b/src_assets/common/assets/web/header.html index 5e463612e34..cf93a0c2333 100644 --- a/src_assets/common/assets/web/header.html +++ b/src_assets/common/assets/web/header.html @@ -5,7 +5,7 @@ Sunshine - + diff --git a/src_assets/common/assets/web/images/favicon.ico b/src_assets/common/assets/web/images/favicon.ico deleted file mode 100644 index 4d07b085281..00000000000 Binary files a/src_assets/common/assets/web/images/favicon.ico and /dev/null differ diff --git a/src_assets/common/assets/web/images/logo-sunshine-16.png b/src_assets/common/assets/web/images/logo-sunshine-16.png index 0f16ac8be30..1360941fa7a 100644 Binary files a/src_assets/common/assets/web/images/logo-sunshine-16.png and b/src_assets/common/assets/web/images/logo-sunshine-16.png differ diff --git a/src_assets/common/assets/web/images/logo-sunshine-45.png b/src_assets/common/assets/web/images/logo-sunshine-45.png index a4a62c9672f..44481f46963 100644 Binary files a/src_assets/common/assets/web/images/logo-sunshine-45.png and b/src_assets/common/assets/web/images/logo-sunshine-45.png differ diff --git a/src_assets/common/assets/web/images/sunshine-locked-16.png b/src_assets/common/assets/web/images/sunshine-locked-16.png new file mode 100644 index 00000000000..4e952c0a009 Binary files /dev/null and b/src_assets/common/assets/web/images/sunshine-locked-16.png differ diff --git a/src_assets/common/assets/web/images/sunshine-locked-45.png b/src_assets/common/assets/web/images/sunshine-locked-45.png new file mode 100644 index 00000000000..a5a29c3b4e2 Binary files /dev/null and b/src_assets/common/assets/web/images/sunshine-locked-45.png differ diff --git a/src_assets/common/assets/web/images/sunshine-locked.ico b/src_assets/common/assets/web/images/sunshine-locked.ico new file mode 100644 index 00000000000..d2f852aba3a Binary files /dev/null and b/src_assets/common/assets/web/images/sunshine-locked.ico differ diff --git a/src_assets/common/assets/web/images/sunshine-locked.png b/src_assets/common/assets/web/images/sunshine-locked.png new file mode 100644 index 00000000000..9433555bc71 Binary files /dev/null and b/src_assets/common/assets/web/images/sunshine-locked.png differ diff --git a/src_assets/common/assets/web/images/sunshine-locked.svg b/src_assets/common/assets/web/images/sunshine-locked.svg new file mode 100644 index 00000000000..6a5078658c5 --- /dev/null +++ b/src_assets/common/assets/web/images/sunshine-locked.svg @@ -0,0 +1,75 @@ + + + + diff --git a/src_assets/common/assets/web/images/sunshine-pausing-16.png b/src_assets/common/assets/web/images/sunshine-pausing-16.png new file mode 100644 index 00000000000..66dbe6b18c9 Binary files /dev/null and b/src_assets/common/assets/web/images/sunshine-pausing-16.png differ diff --git a/src_assets/common/assets/web/images/sunshine-pausing-45.png b/src_assets/common/assets/web/images/sunshine-pausing-45.png new file mode 100644 index 00000000000..03151cac274 Binary files /dev/null and b/src_assets/common/assets/web/images/sunshine-pausing-45.png differ diff --git a/src_assets/common/assets/web/images/sunshine-pausing.ico b/src_assets/common/assets/web/images/sunshine-pausing.ico new file mode 100644 index 00000000000..5b47c24c1f2 Binary files /dev/null and b/src_assets/common/assets/web/images/sunshine-pausing.ico differ diff --git a/src_assets/common/assets/web/images/sunshine-pausing.png b/src_assets/common/assets/web/images/sunshine-pausing.png new file mode 100644 index 00000000000..b5d8d46d2d0 Binary files /dev/null and b/src_assets/common/assets/web/images/sunshine-pausing.png differ diff --git a/src_assets/common/assets/web/images/sunshine-pausing.svg b/src_assets/common/assets/web/images/sunshine-pausing.svg new file mode 100644 index 00000000000..0826be1d2eb --- /dev/null +++ b/src_assets/common/assets/web/images/sunshine-pausing.svg @@ -0,0 +1,84 @@ + + + + diff --git a/src_assets/common/assets/web/images/sunshine-playing-16.png b/src_assets/common/assets/web/images/sunshine-playing-16.png new file mode 100644 index 00000000000..c4a90256a33 Binary files /dev/null and b/src_assets/common/assets/web/images/sunshine-playing-16.png differ diff --git a/src_assets/common/assets/web/images/sunshine-playing-45.png b/src_assets/common/assets/web/images/sunshine-playing-45.png new file mode 100644 index 00000000000..15fdd91e34f Binary files /dev/null and b/src_assets/common/assets/web/images/sunshine-playing-45.png differ diff --git a/src_assets/common/assets/web/images/sunshine-playing.ico b/src_assets/common/assets/web/images/sunshine-playing.ico new file mode 100644 index 00000000000..9d1e0cc73dd Binary files /dev/null and b/src_assets/common/assets/web/images/sunshine-playing.ico differ diff --git a/src_assets/common/assets/web/images/sunshine-playing.png b/src_assets/common/assets/web/images/sunshine-playing.png new file mode 100644 index 00000000000..8b8f2023506 Binary files /dev/null and b/src_assets/common/assets/web/images/sunshine-playing.png differ diff --git a/src_assets/common/assets/web/images/sunshine-playing.svg b/src_assets/common/assets/web/images/sunshine-playing.svg new file mode 100644 index 00000000000..518af24f409 --- /dev/null +++ b/src_assets/common/assets/web/images/sunshine-playing.svg @@ -0,0 +1,89 @@ + + + + diff --git a/src_assets/common/assets/web/images/sunshine.ico b/src_assets/common/assets/web/images/sunshine.ico new file mode 100644 index 00000000000..aa8a28b89e5 Binary files /dev/null and b/src_assets/common/assets/web/images/sunshine.ico differ diff --git a/src_assets/common/assets/web/index.html b/src_assets/common/assets/web/index.html index fb8dc940c9b..2fe713e63e8 100644 --- a/src_assets/common/assets/web/index.html +++ b/src_assets/common/assets/web/index.html @@ -1,6 +1,16 @@

Hello, Sunshine!

Sunshine is a self-hosted game stream host for Moonlight.

+
+
+ + Attention! Sunshine detected these errors during startup. These errors MUST be fixed before using + Sunshine.
+
+
    +
  • {{v.value}}
  • +
+
@@ -9,10 +19,10 @@

Version {{version}}

Loading Latest Release...
-
+
Thank you for helping to make Sunshine a better software! 🌇
-
+
You're running the latest version of Sunshine
@@ -21,7 +31,8 @@

Version {{version}}

A new Nightly Version is Available!
- Download + Download
{{nightlyData.head_sha}}
{{nightlyData.display_title}}
@@ -83,23 +94,26 @@

Legal

githubVersion: null, nightlyData: null, loading: true, + logs: null, } }, async created() { try { this.version = (await fetch("/api/config").then((r) => r.json())).version; this.githubVersion = (await fetch("https://api.github.com/repos/LizardByte/Sunshine/releases/latest").then((r) => r.json())); - if (this.buildVersionIsNightly()) { - this.nightlyData = (await fetch("https://api.github.com/repos/LizardByte/Sunshine/actions/workflows/CI.yml/runs?branch=nightly&status=success&per_page=1").then((r) => r.json())).workflow_runs[0]; + if (this.buildVersionIsNightly) { + this.nightlyData = (await fetch("https://api.github.com/repos/LizardByte/Sunshine/actions/workflows/CI.yml/runs?branch=nightly&event=push&exclude_pull_requests=true&per_page=1").then((r) => r.json())).workflow_runs[0]; } - } catch(e){ + } catch (e) { + } + try { + this.logs = (await fetch("/api/logs").then(r => r.text())) + } catch (e) { + console.error(e); } this.loading = false; }, computed: { - runningDirtyBuild() { - return this.buildVersionIsDirty() - }, stableBuildAvailable() { // If we can't get versions, return false if (!this.githubVersion || !this.version) return false; @@ -109,27 +123,25 @@

Legal

if (v.indexOf("v") === 0) v = v.substring(1); // if nightly or dirty, we do an additional check to make sure it's an actual upgrade. - if (this.buildVersionIsNightly() || this.buildVersionIsDirty()) { + if (this.buildVersionIsNightly || this.buildVersionIsDirty) { const stableVersion = this.version.split('.').slice(0, 3).join('.'); return this.githubVersion.tag_name.substring(1) > stableVersion; } // return true if the version is different, otherwise false - return v !== this.version.split(".")[0]; + return v !== this.version; }, nightlyBuildAvailable() { // Verify nightly data is available and the build version is not stable // This is important to ensure the UI does not try to load undefined values. - if (!this.nightlyData || this.buildVersionIsStable()) return false; + if (!this.nightlyData || this.buildVersionIsStable) return false; // If built with dirty git tree, return false - if (this.buildVersionIsDirty()) return false; + if (this.buildVersionIsDirty) return false; // Get the commit hash let commit = this.version?.split(".").pop(); // return true if the commit hash is different, otherwise false return this.nightlyData.head_sha.indexOf(commit) !== 0; - } - }, - methods: { + }, buildVersionIsDirty() { return this.version?.split(".").length === 5 && this.version.indexOf("dirty") !== -1 @@ -139,6 +151,17 @@

Legal

}, buildVersionIsStable() { return this.version?.split(".").length === 3 + }, + /** Parse the text errors, calculating the text, the timestamp and the level */ + fancyLogs() { + if (!this.logs) return []; + let regex = /(\[\d{4}:\d{2}:\d{2}:\d{2}:\d{2}:\d{2}\]):\s/g; + let rawLogLines = (this.logs.split(regex)).splice(1); + let logLines = [] + for (let i = 0; i < rawLogLines.length; i += 2) { + logLines.push({ timestamp: rawLogLines[i], level: rawLogLines[i + 1].split(":")[0], value: rawLogLines[i + 1] }); + } + return logLines; } } }); diff --git a/src_assets/common/assets/web/troubleshooting.html b/src_assets/common/assets/web/troubleshooting.html index 3fe0ada0d0a..0ada5a8ae33 100644 --- a/src_assets/common/assets/web/troubleshooting.html +++ b/src_assets/common/assets/web/troubleshooting.html @@ -13,7 +13,7 @@

Force Close

Application Closed Successfully!
- Error while closing Appplication + Error while closing Application