diff --git a/.docker_platforms b/.docker_platforms deleted file mode 100644 index f41242ad..00000000 --- a/.docker_platforms +++ /dev/null @@ -1 +0,0 @@ -linux/amd64,linux/arm64/v8 \ No newline at end of file diff --git a/.dockerignore b/.dockerignore index fb14701d..2de6c8e8 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,7 +6,6 @@ # ignore repo directories and files docs/ -packaging/ scripts/ tools/ crowdin.yml @@ -14,3 +13,6 @@ crowdin.yml # ignore dev directories build/ venv/ + +# ignore artifacts +artifacts/ diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 2b757b97..8c9df6e3 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,7 +9,7 @@ updates: directory: "/" schedule: interval: "daily" - time: "00:00" + time: "08:00" target-branch: "nightly" open-pull-requests-limit: 10 @@ -17,7 +17,7 @@ updates: directory: "/" schedule: interval: "daily" - time: "00:00" + time: "08:30" target-branch: "nightly" open-pull-requests-limit: 10 @@ -25,7 +25,7 @@ updates: directory: "/" schedule: interval: "daily" - time: "00:00" + time: "09:00" target-branch: "nightly" open-pull-requests-limit: 10 @@ -33,7 +33,7 @@ updates: directory: "/" schedule: interval: "daily" - time: "00:00" + time: "09:30" target-branch: "nightly" open-pull-requests-limit: 10 @@ -41,6 +41,6 @@ updates: directory: "/" schedule: interval: "daily" - time: "00:00" + time: "10:00" target-branch: "nightly" open-pull-requests-limit: 10 diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 970191bc..09d15d48 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -163,9 +163,10 @@ jobs: cmake - name: Configure PKGBUILD files + id: prepare run: | # variables for manifest - echo "aur_publish=false" >> $GITHUB_ENV + aur_publish=false aur_pkg=sunshine-dev sub_version="" conflicts="'sunshine'" @@ -174,22 +175,20 @@ jobs: branch=${GITHUB_HEAD_REF} # check the branch variable - if [ -z "$branch" ] - then + if [ -z "$branch" ]; then echo "This is a PUSH event" commit=${{ github.sha }} clone_url=${{ github.event.repository.clone_url }} - if [[ ${{ github.ref == 'refs/heads/master' }} ]]; then + if [[ ${{ github.ref == 'refs/heads/master' }} == true ]]; then + echo "This is a main release event" + aur_publish=true aur_pkg=sunshine conflicts="" provides="" - - echo "aur_publish=true" >> $GITHUB_ENV - elif [[ ${{ github.ref == 'refs/heads/nightly' }} ]]; then + elif [[ ${{ github.ref == 'refs/heads/nightly' }} == true ]]; then + echo "This is a nightly release event" sub_version=".r${commit}" - - echo "aur_publish=false" >> $GITHUB_ENV fi else echo "This is a PR event" @@ -201,7 +200,8 @@ jobs: echo "Commit: ${commit}" echo "Clone URL: ${clone_url}" - echo "aur_pkg=${aur_pkg}" >> $GITHUB_ENV + echo "aur_publish=${aur_publish}" >> $GITHUB_OUTPUT + echo "aur_pkg=${aur_pkg}" >> $GITHUB_OUTPUT mkdir -p artifacts mkdir -p build @@ -237,10 +237,10 @@ jobs: path: artifacts/ - name: Publish AUR package - if: ${{ env.aur_publish == 'true' }} + if: ${{ steps.prepare.outputs.aur_publish == 'true' }} uses: KSXGitHub/github-actions-deploy-aur@v2.6.0 with: - pkgname: ${{ env.aur_pkg }} + pkgname: ${{ steps.prepare.outputs.aur_pkg }} pkgbuild: ./artifacts/PKGBUILD assets: | ./artifacts/* @@ -363,12 +363,6 @@ jobs: fail-fast: false # false to test all, true to fail entire job if any fail matrix: include: # package these differently - - type: cpack - EXTRA_ARGS: '' - dist: 20.04 - - type: cpack - EXTRA_ARGS: '' - dist: 22.04 - type: appimage EXTRA_ARGS: '-DSUNSHINE_CONFIGURE_APPIMAGE=ON' dist: 20.04 @@ -430,6 +424,7 @@ jobs: libcurl4-openssl-dev \ libdrm-dev \ libevdev-dev \ + libmfx-dev \ libnuma-dev \ libopus-dev \ libpulse-dev \ @@ -459,9 +454,9 @@ jobs: --slave /usr/bin/gcc-ar gcc-ar /usr/bin/gcc-ar-10 \ --slave /usr/bin/gcc-ranlib gcc-ranlib /usr/bin/gcc-ranlib-10 - # Install CuDA + # Install CUDA sudo wget \ - https://developer.download.nvidia.com/compute/cuda/11.4.2/local_installers/cuda_11.4.2_470.57.02_linux.run \ + 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 @@ -886,6 +881,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-nsis mingw-w64-x86_64-openssl mingw-w64-x86_64-opus diff --git a/.github/workflows/autoupdate.yml b/.github/workflows/autoupdate.yml index 65d80dc3..83f4e161 100644 --- a/.github/workflows/autoupdate.yml +++ b/.github/workflows/autoupdate.yml @@ -9,7 +9,7 @@ # 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. +# Dependabot PRs are updated by an action that comments `@depdenabot rebase` on dependabot PRs. (disabled) name: autoupdate @@ -34,13 +34,18 @@ jobs: PR_READY_STATE: "all" MERGE_CONFLICT_ACTION: "fail" - dependabot-rebase: - name: Dependabot Rebase - if: >- - startsWith(github.repository, 'LizardByte/') - runs-on: ubuntu-latest - steps: - - name: rebase - uses: "bbeesley/gha-auto-dependabot-rebase@v1.2.0" - env: - GITHUB_TOKEN: ${{ secrets.GH_BOT_TOKEN }} +# 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 8ff84aaa..48f9c8cb 100644 --- a/.github/workflows/ci-docker.yml +++ b/.github/workflows/ci-docker.yml @@ -3,10 +3,21 @@ # 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 intended to work with all our organization Docker projects. Docker platforms/architectures should be -# listed in a file named `.docker_platforms`, comma separated list with no spaces. A readme named `DOCKER_README.md` +# This workflow is intended to work with all our organization Docker projects. A readme named `DOCKER_README.md` # will be used to update the description on Docker hub. +# custom comments in dockerfiles: + +# `# platforms: ` +# Comma separated list of platforms, i.e. `# platforms: linux/386,linux/amd64`. Docker platforms can alternatively +# be listed in a file named `.docker_platforms`. +# `# platforms_pr: ` +# Comma separated list of platforms to run for PR events, i.e. `# platforms_pr: linux/amd64`. This will take +# precedence over the `# platforms: ` directive. +# `# artifacts: ` +# `true` to build in two steps, stopping at `artifacts` build stage and extracting the image from there to the +# GitHub runner. + name: CI Docker on: @@ -22,56 +33,55 @@ concurrency: cancel-in-progress: true jobs: - check_dockerfile: - name: Check Dockerfile + check_dockerfiles: + name: Check Dockerfiles runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v3 - - name: Check - id: check + - name: Find dockerfiles + id: find run: | - if [ -f "./Dockerfile" ] - then - FOUND=true - else - FOUND=false - fi + dockerfiles=$(find . -type f -iname "Dockerfile" -o -iname "*.dockerfile") - echo "dockerfile=${FOUND}" >> $GITHUB_OUTPUT + echo "found dockerfiles: ${dockerfiles}" + + # do not quote to keep this as a single line + echo dockerfiles=${dockerfiles} >> $GITHUB_OUTPUT + + MATRIX_COMBINATIONS="" + for FILE in ${dockerfiles}; do + # extract tag from file name + tag=$(echo $FILE | sed -r -z -e 's/(\.\/)*.*\/(Dockerfile)/None/gm') + if [[ $tag == "None" ]]; then + MATRIX_COMBINATIONS="$MATRIX_COMBINATIONS {\"dockerfile\": \"$FILE\"}," + else + tag=$(echo $FILE | sed -r -z -e 's/(\.\/)*.*\/(.+)(\.dockerfile)/-\2/gm') + MATRIX_COMBINATIONS="$MATRIX_COMBINATIONS {\"dockerfile\": \"$FILE\", \"tag\": \"$tag\"}," + fi + done + + # removes the last character (i.e. comma) + MATRIX_COMBINATIONS=${MATRIX_COMBINATIONS::-1} + + # setup matrix for later jobs + matrix=$(( + echo "{ \"include\": [$MATRIX_COMBINATIONS] }" + ) | jq -c .) + + echo $matrix + echo $matrix | jq . + echo "matrix=$matrix" >> $GITHUB_OUTPUT outputs: - dockerfile: ${{ steps.check.outputs.dockerfile }} - - lint_dockerfile: - name: Lint Dockerfile - needs: [check_dockerfile] - if: ${{ needs.check_dockerfile.outputs.dockerfile == 'true' }} - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Hadolint - id: hadolint - uses: hadolint/hadolint-action@v3.0.0 - with: - dockerfile: ./Dockerfile - ignore: DL3008,DL3013,DL3016,DL3018,DL3028,DL3059 - output-file: ./hadolint.log - verbose: true - - - name: Log - if: failure() - run: | - echo "Hadolint outcome: ${{ steps.hadolint.outcome }}" >> $GITHUB_STEP_SUMMARY - cat "./hadolint.log" >> $GITHUB_STEP_SUMMARY + dockerfiles: ${{ steps.find.outputs.dockerfiles }} + matrix: ${{ steps.find.outputs.matrix }} check_changelog: name: Check Changelog - needs: [check_dockerfile] - if: ${{ needs.check_dockerfile.outputs.dockerfile == 'true' }} + needs: [check_dockerfiles] + if: ${{ needs.check_dockerfiles.outputs.dockerfiles }} runs-on: ubuntu-latest steps: - name: Checkout @@ -87,15 +97,99 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} outputs: next_version: ${{ steps.verify_changelog.outputs.changelog_parser_version }} + next_version_bare: ${{ steps.verify_changelog.outputs.changelog_parser_version_bare }} + last_version: ${{ steps.verify_changelog.outputs.latest_release_tag_name }} + release_body: ${{ steps.verify_changelog.outputs.changelog_parser_description }} + + setup_release: + name: Setup Release + needs: check_changelog + runs-on: ubuntu-latest + steps: + - name: Set release details + id: release_details + env: + RELEASE_BODY: ${{ needs.check_changelog.outputs.release_body }} + run: | + # determine to create a release or not + if [[ $GITHUB_EVENT_NAME == "push" ]]; then + RELEASE=true + else + RELEASE=false + fi + + # set the release tag + COMMIT=${{ github.sha }} + if [[ $GITHUB_REF == refs/heads/master ]]; then + TAG="${{ needs.check_changelog.outputs.next_version }}" + RELEASE_NAME="${{ needs.check_changelog.outputs.next_version }}" + RELEASE_BODY="$RELEASE_BODY" + PRE_RELEASE="false" + elif [[ $GITHUB_REF == refs/heads/nightly ]]; then + TAG="nightly-dev" + RELEASE_NAME="nightly" + RELEASE_BODY="automated nightly release - $(date -u +'%Y-%m-%dT%H:%M:%SZ') - ${COMMIT}" + PRE_RELEASE="true" + fi + + echo "create_release=${RELEASE}" >> $GITHUB_OUTPUT + echo "release_tag=${TAG}" >> $GITHUB_OUTPUT + echo "release_commit=${COMMIT}" >> $GITHUB_OUTPUT + echo "release_name=${RELEASE_NAME}" >> $GITHUB_OUTPUT + echo "pre_release=${PRE_RELEASE}" >> $GITHUB_OUTPUT + + # this is stupid but works for multiline strings + echo "RELEASE_BODY<> $GITHUB_ENV + echo "$RELEASE_BODY" >> $GITHUB_ENV + echo "EOF" >> $GITHUB_ENV + + outputs: + create_release: ${{ steps.release_details.outputs.create_release }} + release_tag: ${{ steps.release_details.outputs.release_tag }} + release_commit: ${{ steps.release_details.outputs.release_commit }} + release_name: ${{ steps.release_details.outputs.release_name }} + release_body: ${{ env.RELEASE_BODY }} + pre_release: ${{ steps.release_details.outputs.pre_release }} + + lint_dockerfile: + needs: [check_dockerfiles] + if: ${{ needs.check_dockerfiles.outputs.dockerfiles }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.check_dockerfiles.outputs.matrix) }} + name: Lint Dockerfile${{ matrix.tag }} + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Hadolint + id: hadolint + uses: hadolint/hadolint-action@v3.1.0 + with: + dockerfile: ${{ matrix.dockerfile }} + ignore: DL3008,DL3013,DL3016,DL3018,DL3028,DL3059 + output-file: ./hadolint.log + verbose: true + + - name: Log + if: failure() + run: | + echo "Hadolint outcome: ${{ steps.hadolint.outcome }}" >> $GITHUB_STEP_SUMMARY + cat "./hadolint.log" >> $GITHUB_STEP_SUMMARY docker: - name: Docker - needs: [check_dockerfile, check_changelog] - if: ${{ needs.check_dockerfile.outputs.dockerfile == 'true' }} + needs: [check_dockerfiles, check_changelog, setup_release] + if: ${{ needs.check_dockerfiles.outputs.dockerfiles }} runs-on: ubuntu-latest permissions: packages: write contents: write + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.check_dockerfiles.outputs.matrix) }} + name: Docker${{ matrix.tag }} steps: - name: Checkout @@ -106,13 +200,12 @@ jobs: - name: Prepare id: prepare env: - NEXT_VERSION: ${{ needs.check_changelog.outputs.next_version }} + NV: ${{ needs.check_changelog.outputs.next_version }} run: | # get branch name BRANCH=${GITHUB_HEAD_REF} - if [ -z "$BRANCH" ] - then + if [ -z "$BRANCH" ]; then echo "This is a PUSH event" BRANCH=${{ github.ref_name }} fi @@ -129,27 +222,62 @@ jobs: BASE_TAG=$(echo $REPOSITORY | tr '[:upper:]' '[:lower:]') COMMIT=${{ github.sha }} - TAGS="${BASE_TAG}:${COMMIT:0:7},ghcr.io/${BASE_TAG}:${COMMIT:0:7}" + TAGS="${BASE_TAG}:${COMMIT:0:7}${{ matrix.tag }},ghcr.io/${BASE_TAG}:${COMMIT:0:7}${{ matrix.tag }}" if [[ $GITHUB_REF == refs/heads/master ]]; then - TAGS="${TAGS},${BASE_TAG}:latest,ghcr.io/${BASE_TAG}:latest" - TAGS="${TAGS},${BASE_TAG}:master,ghcr.io/${BASE_TAG}:master" + TAGS="${TAGS},${BASE_TAG}:latest${{ matrix.tag }},ghcr.io/${BASE_TAG}:latest${{ matrix.tag }}" + TAGS="${TAGS},${BASE_TAG}:master${{ matrix.tag }},ghcr.io/${BASE_TAG}:master${{ matrix.tag }}" elif [[ $GITHUB_REF == refs/heads/nightly ]]; then - TAGS="${TAGS},${BASE_TAG}:nightly,ghcr.io/${BASE_TAG}:nightly" + TAGS="${TAGS},${BASE_TAG}:nightly${{ matrix.tag }},ghcr.io/${BASE_TAG}:nightly${{ matrix.tag }}" else - TAGS="${TAGS},${BASE_TAG}:test,ghcr.io/${BASE_TAG}:test" + TAGS="${TAGS},${BASE_TAG}:test${{ matrix.tag }},ghcr.io/${BASE_TAG}:test${{ matrix.tag }}" fi - if [[ ${NEXT_VERSION} != "" ]]; then - TAGS="${TAGS},${BASE_TAG}:${NEXT_VERSION},ghcr.io/${BASE_TAG}:${NEXT_VERSION}" + if [[ ${NV} != "" ]]; then + TAGS="${TAGS},${BASE_TAG}:${NV}${{ matrix.tag }},ghcr.io/${BASE_TAG}:${NV}${{ matrix.tag }}" fi - # read the platforms from `.docker_platforms` - PLATFORMS=$(<.docker_platforms) + # parse custom directives out of dockerfile + # try to get the platforms from the dockerfile custom directive, i.e. `# platforms: xxx,yyy` + # directives for PR event, i.e. not push event + if [[ ${PUSH} == "false" ]]; then + while read -r line; do + if [[ $line == "# platforms_pr: "* && $PLATFORMS == "" ]]; then + # echo the line and use `sed` to remove the custom directive + PLATFORMS=$(echo -e "$line" | sed 's/# platforms_pr: //') + elif [[ $PLATFORMS != "" ]]; then + # break while loop once all custom "PR" event directives are found + break + fi + done <"${{ matrix.dockerfile }}" + fi + # directives for all events... above directives will not be parsed if they were already found + while read -r line; do + if [[ $line == "# platforms: "* && $PLATFORMS == "" ]]; then + # echo the line and use `sed` to remove the custom directive + PLATFORMS=$(echo -e "$line" | sed 's/# platforms: //') + elif [[ $line == "# artifacts: "* && $ARTIFACTS == "" ]]; then + # echo the line and use `sed` to remove the custom directive + ARTIFACTS=$(echo -e "$line" | sed 's/# artifacts: //') + elif [[ $PLATFORMS != "" && $ARTIFACTS != "" ]]; then + # break while loop once all custom directives are found + break + fi + done <"${{ matrix.dockerfile }}" + # if PLATFORMS is blank, fall back to the legacy method of reading from the `.docker_platforms` file + if [[ $PLATFORMS == "" ]]; then + # read the platforms from `.docker_platforms` + PLATFORMS=$(<.docker_platforms) + fi + # if PLATFORMS is still blank, fall back to `linux/amd64` + if [[ $PLATFORMS == "" ]]; then + PLATFORMS="linux/amd64" + fi echo "branch=${BRANCH}" >> $GITHUB_OUTPUT echo "build_date=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT echo "commit=${COMMIT}" >> $GITHUB_OUTPUT + echo "artifacts=${ARTIFACTS}" >> $GITHUB_OUTPUT echo "platforms=${PLATFORMS}" >> $GITHUB_OUTPUT echo "push=${PUSH}" >> $GITHUB_OUTPUT echo "tags=${TAGS}" >> $GITHUB_OUTPUT @@ -165,9 +293,9 @@ jobs: uses: actions/cache@v3 with: path: /tmp/.buildx-cache - key: ${{ runner.os }}-buildx-${{ github.sha }} + key: Docker-buildx${{ matrix.tag }}-${{ github.sha }} restore-keys: | - ${{ runner.os }}-buildx- + Docker-buildx${{ matrix.tag }}- - name: Log in to Docker Hub if: ${{ steps.prepare.outputs.push == 'true' }} # PRs do not have access to secrets @@ -184,11 +312,32 @@ jobs: username: ${{ secrets.GH_BOT_NAME }} password: ${{ secrets.GH_BOT_TOKEN }} - - name: Build and push + - name: Build artifacts + if: ${{ steps.prepare.outputs.artifacts == 'true' }} + id: build_artifacts uses: docker/build-push-action@v3 with: context: ./ - file: ./Dockerfile + file: ${{ matrix.dockerfile }} + target: artifacts + outputs: type=local,dest=artifacts + push: false + platforms: ${{ steps.prepare.outputs.platforms }} + build-args: | + BRANCH=${{ steps.prepare.outputs.branch }} + BUILD_DATE=${{ steps.prepare.outputs.build_date }} + BUILD_VERSION=${{ needs.check_changelog.outputs.next_version }} + COMMIT=${{ steps.prepare.outputs.commit }} + tags: ${{ steps.prepare.outputs.tags }} + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,dest=/tmp/.buildx-cache + + - name: Build and push + id: build + uses: docker/build-push-action@v3 + with: + context: ./ + file: ${{ matrix.dockerfile }} push: ${{ steps.prepare.outputs.push }} platforms: ${{ steps.prepare.outputs.platforms }} build-args: | @@ -200,6 +349,36 @@ jobs: cache-from: type=local,src=/tmp/.buildx-cache cache-to: type=local,dest=/tmp/.buildx-cache + - name: Arrange Artifacts + if: ${{ steps.prepare.outputs.artifacts == 'true' }} + working-directory: artifacts + run: | + # artifacts will be in sub directories named after the docker target platform, e.g. `linux_amd64` + # so move files to the artifacts directory + # https://unix.stackexchange.com/a/52816 + find ./ -type f -exec mv -t ./ -n '{}' + + + - name: Upload Artifacts + if: ${{ steps.prepare.outputs.artifacts == 'true' }} + uses: actions/upload-artifact@v3 + with: + name: sunshine${{ matrix.tag }} + path: artifacts/ + + - name: Create/Update GitHub Release + if: ${{ needs.setup_release.outputs.create_release == 'true' && steps.prepare.outputs.artifacts == 'true' }} + uses: ncipollo/release-action@v1 + with: + name: ${{ needs.setup_release.outputs.release_name }} + tag: ${{ needs.setup_release.outputs.release_tag }} + commit: ${{ needs.setup_release.outputs.release_commit }} + artifacts: "*artifacts/*" + token: ${{ secrets.GH_BOT_TOKEN }} + allowUpdates: true + body: ${{ needs.setup_release.outputs.release_body }} + discussionCategory: announcements + prerelease: ${{ needs.setup_release.outputs.pre_release }} + - name: Update Docker Hub Description if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }} uses: peter-evans/dockerhub-description@v3 diff --git a/.github/workflows/issues-stale.yml b/.github/workflows/issues-stale.yml index cc0e3ae8..5fe1d773 100644 --- a/.github/workflows/issues-stale.yml +++ b/.github/workflows/issues-stale.yml @@ -9,7 +9,7 @@ name: Stale Issues / PRs on: schedule: - - cron: '00 00 * * *' + - cron: '00 10 * * *' jobs: stale: diff --git a/.gitmodules b/.gitmodules index b79222a2..c6b2af2c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -42,3 +42,7 @@ path = third-party/ffmpeg-macos-aarch64 url = https://github.com/LizardByte/build-deps branch = ffmpeg-macos-aarch64 +[submodule "third-party/nanors"] + path = third-party/nanors + url = https://github.com/sleepybishop/nanors.git + branch = master diff --git a/CHANGELOG.md b/CHANGELOG.md index d2521b68..9110aa67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,33 @@ # Changelog +## [0.18.0] - 2023-01-29 +Attention, this release contains critical security fixes. Please update as soon as possible. Additionally, we are +encouraging users to change your Sunshine password, especially if you expose the web UI (i.e. port 47790 by default) +to the internet, or have ever uploaded your logs with verbose output to a public resource. + +### Added +- (Windows) Add support for Intel QuickSync +- (Linux) Added aarch64 deb and rpm packages +- (Windows) Add support for hybrid graphics systems, such as laptops with both integrated and discrete GPUs +- (Linux) Add support for streaming from Steam Deck Gaming Mode +- (Windows) Add HDR support, see https://docs.lizardbyte.dev/projects/sunshine/en/latest/about/usage.html#hdr-support +### Fixed +- (Network) Refactor code for UPnP port forwarding +- (Video) Enforce 10 FPS encoding frame rate minimum to improve static image quality +- (Linux) deb and rpm packages are now specific to destination distro and version +- (Docs) Add nvidia/nvenc preset migration guide +- (Network) Performance optimizations +- (Video/Windows) Fix streaming to multiple clients from hardware encoder +- (Linux) Fix child process spawning +- (Security) Fix security vulnerability in implementation of SimpleWebServer +- (Misc) Rename "Steam BigPicture" to "Steam Big Picture" in default apps.json +- (Security) Scrub basic authorization header from logs +- (Linux) The systemd service will now restart in the event of a crash +- (Video/KMS/Linux) Fixed error: `couldn't import RGB Image: 00003002 and 00003004` +- (Video/Windows) Fix stream freezing triggered by the resolution changed +- (Installer/Windows) Fixes silent installation and other miscellaneous improvements +- (CPU) Significantly improved CPU usage + ## [0.17.0] - 2023-01-08 If you are running Sunshine as a service on Windows, we are strongly urging you to update to v0.17.0 as soon as possible. Older Windows versions of Sunshine had a security flaw in which the binary was located in a user-writable @@ -272,3 +300,4 @@ settings. In v0.17.0, games now run under your user account without elevated pri [0.15.0]: https://github.com/LizardByte/Sunshine/releases/tag/v0.15.0 [0.16.0]: https://github.com/LizardByte/Sunshine/releases/tag/v0.16.0 [0.17.0]: https://github.com/LizardByte/Sunshine/releases/tag/v0.17.0 +[0.18.0]: https://github.com/LizardByte/Sunshine/releases/tag/v0.18.0 diff --git a/CMakeLists.txt b/CMakeLists.txt index b1970b1d..62b72fcc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,7 @@ -cmake_minimum_required(VERSION 3.0) +cmake_minimum_required(VERSION 3.18) +# `CMAKE_CUDA_ARCHITECTURES` requires 3.18 -project(Sunshine VERSION 0.17.0 +project(Sunshine VERSION 0.18.0 DESCRIPTION "Sunshine is a self-hosted game stream host for Moonlight." HOMEPAGE_URL "https://app.lizardbyte.dev") @@ -16,6 +17,11 @@ 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}) @@ -64,7 +70,7 @@ include_directories(third-party/miniupnp/miniupnpc/include) find_package(Threads REQUIRED) find_package(OpenSSL REQUIRED) find_package(PkgConfig REQUIRED) -pkg_check_modules (CURL REQUIRED libcurl) +pkg_check_modules(CURL REQUIRED libcurl) if(NOT APPLE) set(Boost_USE_STATIC_LIBS ON) # cmake-lint: disable=C0103 @@ -197,12 +203,70 @@ else() check_language(CUDA) if(CMAKE_CUDA_COMPILER) - if(NOT DEFINED CMAKE_CUDA_ARCHITECTURES) - set(CMAKE_CUDA_ARCHITECTURES 35) - endif() - 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}) @@ -337,8 +401,8 @@ configure_file(version.h.in version.h @ONLY) include_directories(${CMAKE_CURRENT_BINARY_DIR}) set(SUNSHINE_TARGET_FILES - third-party/moonlight-common-c/reedsolomon/rs.c - third-party/moonlight-common-c/reedsolomon/rs.h + 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 @@ -385,10 +449,13 @@ set(SUNSHINE_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") + # 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) + 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") @@ -396,12 +463,13 @@ elseif(APPLE) 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) endif() - set(FFMPEG_PLATFORM_LIBRARIES va va-drm va-x11 vdpau X11) endif() set(FFMPEG_INCLUDE_DIRS ${FFMPEG_PREPARED_BINARIES}/include) @@ -424,20 +492,19 @@ include_directories( ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/third-party ${CMAKE_CURRENT_SOURCE_DIR}/third-party/moonlight-common-c/enet/include - ${CMAKE_CURRENT_SOURCE_DIR}/third-party/moonlight-common-c/reedsolomon + ${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") - list(APPEND SUNSHINE_COMPILE_OPTIONS -O0 -ggdb3) if(WIN32) set_source_files_properties(src/nvhttp.cpp PROPERTIES COMPILE_FLAGS -O2) endif() else() add_definitions(-DNDEBUG) - list(APPEND SUNSHINE_COMPILE_OPTIONS -O3) endif() # setup assets directory @@ -475,6 +542,8 @@ 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) endif() target_link_libraries(sunshine ${SUNSHINE_EXTERNAL_LIBRARIES} ${EXTRA_LIBS}) @@ -521,11 +590,17 @@ install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/node_modules" 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) install(TARGETS sunshinesvc RUNTIME DESTINATION "tools" COMPONENT sunshinesvc) + # Mandatory tools + install(TARGETS ddprobe RUNTIME DESTINATION "tools" COMPONENT application) + # scripts install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/windows/misc/firewall/" DESTINATION "scripts" @@ -558,15 +633,17 @@ if(WIN32) # see options at: https://cmake.org/cmake/help/latest/cpack_gen/nsis.h # Install service SET(CPACK_NSIS_EXTRA_INSTALL_COMMANDS "${CPACK_NSIS_EXTRA_INSTALL_COMMANDS} - ExecWait '\\\"$SYSDIR\\\\cmd.exe\\\" /c \\\"start https://sunshinestream.readthedocs.io/\\\"' - ExecWait 'icacls \\\"$INSTDIR\\\" /reset' - ExecWait '\\\"$INSTDIR\\\\scripts\\\\migrate-config.bat\\\"' - ExecWait 'icacls \\\"$INSTDIR\\\\config\\\" /grant:r Users:\\\(OI\\\)\\\(CI\\\)\\\(F\\\)' - ExecWait '\\\"$INSTDIR\\\\scripts\\\\add-firewall-rule.bat\\\"' - ExecWait '\\\"$INSTDIR\\\\scripts\\\\install-service.bat\\\"' - MessageBox MB_YESNO|MB_ICONQUESTION 'Do you want to add/update ViGEmBus (virtual controller support)?' \ - IDNO NoController - ExecWait '\\\"$SYSDIR\\\\cmd.exe\\\" /c \\\"start https://github.com/ViGEm/ViGEmBus/releases/latest\\\"'; \ + IfSilent +2 0 + ExecShell 'open' 'https://sunshinestream.readthedocs.io/' + nsExec::ExecToLog 'icacls \\\"$INSTDIR\\\" /reset' + nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\migrate-config.bat\\\"' + nsExec::ExecToLog 'icacls \\\"$INSTDIR\\\\config\\\" /grant:r Users:\\\(OI\\\)\\\(CI\\\)\\\(F\\\)' + nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\add-firewall-rule.bat\\\"' + nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\install-service.bat\\\"' + MessageBox MB_YESNO|MB_ICONQUESTION \ + 'Do you want to install ViGEmBus? This is REQUIRED for gamepad support while streaming.' \ + /SD IDNO IDNO NoController + ExecShell 'open' 'https://github.com/ViGEm/ViGEmBus/releases/latest'; \ skipped if no NoController: ") @@ -575,8 +652,8 @@ if(WIN32) # see options at: https://cmake.org/cmake/help/latest/cpack_gen/nsis.h # Uninstall service set(CPACK_NSIS_EXTRA_UNINSTALL_COMMANDS "${CPACK_NSIS_EXTRA_UNINSTALL_COMMANDS} - ExecWait '\\\"$INSTDIR\\\\scripts\\\\delete-firewall-rule.bat\\\"' - ExecWait '\\\"$INSTDIR\\\\scripts\\\\uninstall-service.bat\\\"' + 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 $INSTDIR (this includes the configuration, cover images, and settings)?' \ /SD IDNO IDNO NoDelete @@ -592,12 +669,12 @@ if(WIN32) # see options at: https://cmake.org/cmake/help/latest/cpack_gen/nsis.h 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' \ + CreateShortCut '\$SMPROGRAMS\\\\$STARTMENU_FOLDER\\\\${CMAKE_PROJECT_NAME} (Foreground Mode).lnk' \ '\$INSTDIR\\\\${CMAKE_PROJECT_NAME}.exe' ") set(CPACK_NSIS_DELETE_ICONS_EXTRA "${CPACK_NSIS_DELETE_ICONS_EXTRA} - Delete '\$SMPROGRAMS\\\\$MUI_TEMP\\\\${CMAKE_PROJECT_NAME}.lnk' + Delete '\$SMPROGRAMS\\\\$MUI_TEMP\\\\${CMAKE_PROJECT_NAME} (Foreground Mode).lnk' ") # Checking for previous installed versions diff --git a/DOCKER_README.md b/DOCKER_README.md index 71338e0a..63639527 100644 --- a/DOCKER_README.md +++ b/DOCKER_README.md @@ -1,17 +1,43 @@ # Docker +## Important note +Starting with v0.18.0, tag names have changed. You may no longer use `latest`, `master`, `vX.X.X`. + ## Build your own containers This image provides a method for you to easily use the latest Sunshine release in your own docker projects. It is not intended to use as a standalone container at this point, and should be considered experimental. ```dockerfile -FROM lizardbyte/sunshine +ARG SUNSHINE_VERSION=latest +ARG SUNSHINE_OS=ubuntu-22.04 +FROM lizardbyte/sunshine:${SUNSHINE_VERSION}-${SUNSHINE_OS} # install Steam, Wayland, etc. ENTRYPOINT steam && sunshine ``` +### SUNSHINE_VERSION +- `latest`, `master`, `vX.X.X` +- `nightly` +- commit hash + +### SUNSHINE_OS +Sunshine images are available, based on the following base images. + +- `debian-bullseye` +- `fedora-36` +- `fedora-37` +- `ubuntu-20.04` +- `ubuntu-22.04` + +### Tags +You must combine the `SUNSHINE_VERSION` and `SUNSHINE_OS` to determine the tag to pull. The format should be +`-`. For example, `latest-ubuntu-22.04`. + +See all our available tags on [docker hub](https://hub.docker.com/r/lizardbyte/sunshine/tags) or +[ghcr](https://github.com/LizardByte/Sunshine/pkgs/container/sunshine/versions) for more info. + ## Where used This is a list of docker projects using Sunshine. Something missing? Let us know about it! @@ -97,12 +123,12 @@ If you want to change the PUID or PGID after the image has been built, it will r ## Supported Architectures -Specifying `lizardbyte/sunshine:latest` or `ghcr.io/lizardbyte/sunshine:latest` should retrieve the correct -image for your architecture. +Specifying `lizardbyte/sunshine:latest-` or `ghcr.io/lizardbyte/sunshine:latest-` should +retrieve the correct image for your architecture. -The architectures supported by this image are: +The architectures supported by these images are: -| Architecture | Available | -|:------------:|:---------:| -| x86-64 | ✅ | -| arm64 | ✅ | +| Architecture | Available | +|:---------------:|:---------:| +| amd64 / x86_64 | ✅ | +| arm64 / aarch64 | ✅ | diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 9ff9d0d9..00000000 --- a/Dockerfile +++ /dev/null @@ -1,102 +0,0 @@ -FROM ubuntu:22.04 AS sunshine-base - -ARG DEBIAN_FRONTEND=noninteractive - -FROM sunshine-base as sunshine-build - -SHELL ["/bin/bash", "-o", "pipefail", "-c"] -RUN apt-get update -y \ - && apt-get install -y --no-install-recommends \ - build-essential=12.9* \ - cmake=3.22.1* \ - libavdevice-dev=7:4.4.* \ - libboost-filesystem-dev=1.74.0* \ - libboost-log-dev=1.74.0* \ - libboost-thread-dev=1.74.0* \ - libboost-program-options-dev=1.74.0* \ - libcap-dev=1:2.44* \ - libcurl4-openssl-dev=7.81.0* \ - libdrm-dev=2.4.110* \ - libevdev-dev=1.12.1* \ - libnuma-dev=2.0.14* \ - libopus-dev=1.3.1* \ - libpulse-dev=1:15.99.1* \ - libssl-dev=3.0.2* \ - libva-dev=2.14.0* \ - libvdpau-dev=1.4* \ - libwayland-dev=1.20.0* \ - libx11-dev=2:1.7.5* \ - libxcb-shm0-dev=1.14* \ - libxcb-xfixes0-dev=1.14* \ - libxcb1-dev=1.14* \ - libxfixes-dev=1:6.0.0* \ - libxrandr-dev=2:1.5.2* \ - libxtst-dev=2:1.2.3* \ - nodejs=12.22.9* \ - npm=8.5.1* \ - nvidia-cuda-dev=11.5.1* \ - nvidia-cuda-toolkit=11.5.1* \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* - -# copy repository -WORKDIR /root/sunshine-build/ -COPY . . - -# setup npm and dependencies -RUN npm install - -# setup build directory -WORKDIR /root/sunshine-build/build - -# cmake and cpack -RUN cmake -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 \ - /root/sunshine-build \ - && make -j "$(nproc)" \ - && cpack -G DEB - -FROM sunshine-base as sunshine - -# copy deb from builder -COPY --from=sunshine-build /root/sunshine-build/build/cpack_artifacts/Sunshine.deb /sunshine.deb - -# install sunshine -RUN apt-get update -y \ - && apt-get install -y --no-install-recommends /sunshine.deb \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* - -# 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 - -RUN groupadd -f -g "${PGID}" "${UNAME}" && \ - useradd -lm -d ${HOME} -s /bin/bash -g "${PGID}" -G input -u "${PUID}" "${UNAME}" && \ - mkdir -p ${HOME}/.config/sunshine && \ - ln -s ${HOME}/.config/sunshine /config && \ - chown -R ${UNAME} ${HOME} - -USER ${UNAME} -WORKDIR ${HOME} - -# entrypoint -ENTRYPOINT ["/usr/bin/sunshine"] diff --git a/docker/debian-bullseye.dockerfile b/docker/debian-bullseye.dockerfile new file mode 100644 index 00000000..893409f8 --- /dev/null +++ b/docker/debian-bullseye.dockerfile @@ -0,0 +1,156 @@ +# syntax=docker/dockerfile:1.4 +# artifacts: true +# platforms: linux/amd64,linux/arm64/v8 +# platforms_pr: linux/amd64 +ARG BASE=debian +ARG TAG=bullseye +FROM ${BASE}:${TAG} AS sunshine-base + +ENV DEBIAN_FRONTEND=noninteractive + +FROM sunshine-base as sunshine-build + +ARG TARGETPLATFORM +RUN echo "target_platform: ${TARGETPLATFORM}" + +SHELL ["/bin/bash", "-o", "pipefail", "-c"] +# install dependencies +RUN <<_DEPS +#!/bin/bash +apt-get update -y +apt-get install -y --no-install-recommends \ + build-essential=12.9* \ + cmake=3.18.4* \ + libavdevice-dev=7:4.3.* \ + libboost-filesystem-dev=1.74.0* \ + libboost-log-dev=1.74.0* \ + libboost-program-options-dev=1.74.0* \ + libboost-thread-dev=1.74.0* \ + libcap-dev=1:2.44* \ + libcurl4-openssl-dev=7.74.0* \ + libdrm-dev=2.4.104* \ + libevdev-dev=1.11.0* \ + libnuma-dev=2.0.12* \ + libopus-dev=1.3.1* \ + libpulse-dev=14.2* \ + libssl-dev=1.1.1* \ + libva-dev=2.10.0* \ + libvdpau-dev=1.4* \ + libwayland-dev=1.18.0* \ + libx11-dev=2:1.7.2* \ + libxcb-shm0-dev=1.14* \ + libxcb-xfixes0-dev=1.14* \ + libxcb1-dev=1.14* \ + libxfixes-dev=1:5.0.3* \ + libxrandr-dev=2:1.5.1* \ + libxtst-dev=2:1.2.3* \ + nodejs=12.22* \ + npm=7.5.2* \ + wget=1.21* +if [[ "${TARGETPLATFORM}" == 'linux/amd64' ]]; then + apt-get install -y --no-install-recommends \ + libmfx-dev=21.1.0* +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="11.8.0" +ENV CUDA_BUILD="520.61.05" +# hadolint ignore=SC3010 +RUN <<_INSTALL_CUDA +#!/bin/bash +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 .. . + +# setup npm dependencies +RUN npm install + +# setup build directory +WORKDIR /build/sunshine/build + +# cmake and cpack +RUN <<_MAKE +#!/bin/bash +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 --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 --from=artifacts /sunshine*.deb /sunshine.deb + +# install sunshine +RUN <<_INSTALL_SUNSHINE +#!/bin/bash +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 +groupadd -f -g "${PGID}" "${UNAME}" +useradd -lm -d ${HOME} -s /bin/bash -g "${PGID}" -G input -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/fedora-36.dockerfile b/docker/fedora-36.dockerfile new file mode 100644 index 00000000..30c215f8 --- /dev/null +++ b/docker/fedora-36.dockerfile @@ -0,0 +1,155 @@ +# syntax=docker/dockerfile:1.4 +# artifacts: true +# platforms: linux/amd64,linux/arm64/v8 +# platforms_pr: linux/amd64 +ARG BASE=fedora +ARG TAG=36 +FROM ${BASE}:${TAG} AS sunshine-base + +FROM sunshine-base as sunshine-build + +ARG TARGETPLATFORM +RUN echo "target_platform: ${TARGETPLATFORM}" + +SHELL ["/bin/bash", "-o", "pipefail", "-c"] +# install dependencies +# hadolint ignore=DL3041 +RUN <<_DEPS +#!/bin/bash +dnf -y update +dnf -y group install "Development Tools" +dnf -y install \ + boost-devel-1.76.0* \ + boost-static-1.76.0* \ + cmake-3.22.2* \ + gcc-12.0.1* \ + gcc-c++-12.0.1* \ + libcap-devel-2.48* \ + libcurl-devel-7.82.0* \ + libdrm-devel-2.4.110* \ + libevdev-devel-1.12.0* \ + libva-devel-2.14.0* \ + libvdpau-devel-1.5* \ + libX11-devel-1.7.3* \ + libxcb-devel-1.13.1* \ + libXcursor-devel-1.2.0* \ + libXfixes-devel-6.0.0* \ + libXi-devel-1.8* \ + libXinerama-devel-1.1.4* \ + libXrandr-devel-1.5.2* \ + libXtst-devel-1.2.3* \ + mesa-libGL-devel-22.0.1* \ + npm-8.3.1* \ + numactl-devel-2.0.14* \ + openssl-devel-3.0.2* \ + opus-devel-1.3.1* \ + pulseaudio-libs-devel-15.0* \ + rpm-build-4.17.0* \ + wget-1.21.3* \ + which-2.21* +if [[ "${TARGETPLATFORM}" == 'linux/amd64' ]]; then + dnf -y install intel-mediasdk-devel-22.3.0* +fi +dnf clean all +rm -rf /var/cache/yum +_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 +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 .. . + +# setup npm dependencies +RUN npm install + +# setup build directory +WORKDIR /build/sunshine/build + +# cmake and cpack +RUN <<_MAKE +#!/bin/bash +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 RPM +_MAKE + +FROM scratch AS artifacts +ARG BASE +ARG TAG +ARG TARGETARCH +COPY --from=sunshine-build /build/sunshine/build/cpack_artifacts/Sunshine.rpm /sunshine-${BASE}-${TAG}-${TARGETARCH}.rpm + +FROM sunshine-base as sunshine + +# copy deb from builder +COPY --from=artifacts /sunshine*.rpm /sunshine.rpm + +# install sunshine +RUN <<_INSTALL_SUNSHINE +#!/bin/bash +dnf -y update +dnf -y install /sunshine.rpm +dnf clean all +rm -rf /var/cache/yum +_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 +groupadd -f -g "${PGID}" "${UNAME}" +useradd -lm -d ${HOME} -s /bin/bash -g "${PGID}" -G input -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/fedora-37.dockerfile b/docker/fedora-37.dockerfile new file mode 100644 index 00000000..f71837a1 --- /dev/null +++ b/docker/fedora-37.dockerfile @@ -0,0 +1,155 @@ +# syntax=docker/dockerfile:1.4 +# artifacts: true +# platforms: linux/amd64,linux/arm64/v8 +# platforms_pr: linux/amd64 +ARG BASE=fedora +ARG TAG=37 +FROM ${BASE}:${TAG} AS sunshine-base + +FROM sunshine-base as sunshine-build + +ARG TARGETPLATFORM +RUN echo "target_platform: ${TARGETPLATFORM}" + +SHELL ["/bin/bash", "-o", "pipefail", "-c"] +# install dependencies +# hadolint ignore=DL3041 +RUN <<_DEPS +#!/bin/bash +dnf -y update +dnf -y group install "Development Tools" +dnf -y install \ + boost-devel-1.78.0* \ + boost-static-1.78.0* \ + cmake-3.24.1* \ + gcc-12.2.1* \ + gcc-c++-12.2.1* \ + libcap-devel-2.48* \ + libcurl-devel-7.85.0* \ + libdrm-devel-2.4.112* \ + libevdev-devel-1.13.0* \ + libva-devel-2.15.0* \ + libvdpau-devel-1.5* \ + libX11-devel-1.8.1* \ + libxcb-devel-1.13.1* \ + libXcursor-devel-1.2.1* \ + libXfixes-devel-6.0.0* \ + libXi-devel-1.8* \ + libXinerama-devel-1.1.4* \ + libXrandr-devel-1.5.2* \ + libXtst-devel-1.2.3* \ + mesa-libGL-devel-22.2.2* \ + npm-8.15.0* \ + numactl-devel-2.0.14* \ + openssl-devel-3.0.5* \ + opus-devel-1.3.1* \ + pulseaudio-libs-devel-16.1* \ + rpm-build-4.18.0* \ + wget-1.21.3* \ + which-2.21* +if [[ "${TARGETPLATFORM}" == 'linux/amd64' ]]; then + dnf -y install intel-mediasdk-devel-22.4.4* +fi +dnf clean all +rm -rf /var/cache/yum +_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 +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 .. . + +# setup npm dependencies +RUN npm install + +# setup build directory +WORKDIR /build/sunshine/build + +# cmake and cpack +RUN <<_MAKE +#!/bin/bash +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 RPM +_MAKE + +FROM scratch AS artifacts +ARG BASE +ARG TAG +ARG TARGETARCH +COPY --from=sunshine-build /build/sunshine/build/cpack_artifacts/Sunshine.rpm /sunshine-${BASE}-${TAG}-${TARGETARCH}.rpm + +FROM sunshine-base as sunshine + +# copy deb from builder +COPY --from=artifacts /sunshine*.rpm /sunshine.rpm + +# install sunshine +RUN <<_INSTALL_SUNSHINE +#!/bin/bash +dnf -y update +dnf -y install /sunshine.rpm +dnf clean all +rm -rf /var/cache/yum +_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 +groupadd -f -g "${PGID}" "${UNAME}" +useradd -lm -d ${HOME} -s /bin/bash -g "${PGID}" -G input -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/ubuntu-18.04.dockerfile-todo b/docker/ubuntu-18.04.dockerfile-todo new file mode 100644 index 00000000..5e905fbd --- /dev/null +++ b/docker/ubuntu-18.04.dockerfile-todo @@ -0,0 +1,210 @@ +# syntax=docker/dockerfile:1.4 +# artifacts: true +# platforms: linux/amd64,linux/arm64/v8 +# platforms_pr: linux/amd64 +ARG BASE=ubuntu +ARG TAG=18.04 +FROM ${BASE}:${TAG} AS sunshine-base + +ENV DEBIAN_FRONTEND=noninteractive + +FROM sunshine-base as sunshine-build + +ARG TARGETPLATFORM +RUN echo "target_platform: ${TARGETPLATFORM}" + +SHELL ["/bin/bash", "-o", "pipefail", "-c"] +# install dependencies +RUN <<_DEPS +#!/bin/bash +apt-get update -y +apt-get install -y --no-install-recommends \ + software-properties-common=0.96.24.32.18 +add-apt-repository ppa:ubuntu-toolchain-r/test +apt-get install -y --no-install-recommends \ + bison=2:3.0.4* \ + build-essential=12.4* \ + gcc-10=10.3.0* \ + g++-10=10.3.0* \ + libavdevice-dev=7:3.4.* \ + libcap-dev=1:2.25* \ + libcurl-openssl1.0-dev=7.58.0* \ + libdrm-dev=2.4.101* \ + libevdev-dev=1.5.8* \ + libnuma-dev=2.0.11* \ + libopus-dev=1.1.2* \ + libpulse-dev=1:11.1* \ + libssl1.0-dev=1.0.2* \ + libva-dev=2.1.0* \ + libvdpau-dev=1.1.1* \ + libwayland-dev=1.16.0* \ + libx11-dev=2:1.6.4* \ + libxcb-shm0-dev=1.13* \ + libxcb-xfixes0-dev=1.13* \ + libxcb1-dev=1.13* \ + libxfixes-dev=1:5.0.3* \ + libxrandr-dev=2:1.5.1* \ + libxtst-dev=2:1.2.3* \ + npm=3.5.2* \ + node-gyp=3.6.2* \ + nodejs-dev=8.10.0* \ + wget=1.19.4* +apt-get clean +rm -rf /var/lib/apt/lists/* +_DEPS + +# Update gcc alias +# https://stackoverflow.com/a/70653945/11214013 +RUN <<_GCC_ALIAS +#!/bin/bash +update-alternatives --install \ + /usr/bin/gcc gcc /usr/bin/gcc-10 100 \ + --slave /usr/bin/g++ g++ /usr/bin/g++-10 \ + --slave /usr/bin/gcov gcov /usr/bin/gcov-10 \ + --slave /usr/bin/gcc-ar gcc-ar /usr/bin/gcc-ar-10 \ + --slave /usr/bin/gcc-ranlib gcc-ranlib /usr/bin/gcc-ranlib-10 +_GCC_ALIAS + +# install boost +# cannot install boost for aarch64 using ppa:savoury1/boost-defaults-1.71 +# otherwise add the repository and the following packages +# libboost-filesystem1.71-dev=1.71.0* \ +# libboost-log1.71-dev=1.71.0* \ +# libboost-program-options1.71-dev=1.71.0* \ +# libboost-regex1.71-dev=1.71.0* \ +# libboost-thread1.71-dev=1.71.0* \ +WORKDIR /build/tmp +RUN <<_INSTALL_BOOST +url="https://boostorg.jfrog.io/artifactory/main/release/1.74.0/source/boost_1_74_0.tar.bz2" +wget "${url}" --progress=bar:force:noscroll -q --show-progress -O ./boost.tar.bz2 +tar --bzip2 -xf boost.tar.bz2 --directory /build +mv /build/boost_*/ /build/boost +ls -a /build/boost +cd /build/boost +./bootstrap.sh --with-libraries=system,thread,log,program_options && \ +./b2 install variant=release link=static,shared runtime-link=shared -j "$(nproc)" +_INSTALL_BOOST + +# install cmake +# sunshine requires cmake >= 3.18 +WORKDIR /build/cmake +# https://cmake.org/download/ +ENV CMAKE_VERSION="3.25.1" +# hadolint ignore=SC3010 +RUN <<_INSTALL_CMAKE +#!/bin/bash +cmake_prefix="https://github.com/Kitware/CMake/releases/download/v" +if [[ "${TARGETPLATFORM}" == 'linux/amd64' ]]; then + cmake_arch="x86_64" +elif [[ "${TARGETPLATFORM}" == 'linux/arm64' ]]; then + cmake_arch="aarch64" +fi +url="${cmake_prefix}${CMAKE_VERSION}/cmake-${CMAKE_VERSION}-linux-${cmake_arch}.sh" +echo "cmake url: ${url}" +wget "$url" --progress=bar:force:noscroll -q --show-progress -O ./cmake.sh +sh ./cmake.sh --prefix=/usr/local --skip-license +cmake --version +_INSTALL_CMAKE + +# install cuda +WORKDIR /build/cuda +# versions: https://developer.nvidia.com/cuda-toolkit-archive +ENV CUDA_VERSION="11.8.0" +ENV CUDA_BUILD="520.61.05" +# hadolint ignore=SC3010 +RUN <<_INSTALL_CUDA +#!/bin/bash +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 + +# todo - install libmfx +# https://github.com/Intel-Media-SDK/MediaSDK + +# copy repository +WORKDIR /build/sunshine/ +COPY .. . + +# setup npm dependencies +RUN npm install + +# setup build directory +WORKDIR /build/sunshine/build + +# cmake and cpack +RUN <<_MAKE +#!/bin/bash +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 --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 --from=artifacts /sunshine*.deb /sunshine.deb + +# install sunshine +RUN <<_INSTALL_SUNSHINE +#!/bin/bash +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 +groupadd -f -g "${PGID}" "${UNAME}" +useradd -lm -d ${HOME} -s /bin/bash -g "${PGID}" -G input -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/ubuntu-20.04.dockerfile b/docker/ubuntu-20.04.dockerfile new file mode 100644 index 00000000..0a5d0907 --- /dev/null +++ b/docker/ubuntu-20.04.dockerfile @@ -0,0 +1,190 @@ +# syntax=docker/dockerfile:1.4 +# artifacts: true +# platforms: linux/amd64,linux/arm64/v8 +# platforms_pr: linux/amd64 +ARG BASE=ubuntu +ARG TAG=20.04 +FROM ${BASE}:${TAG} AS sunshine-base + +ENV DEBIAN_FRONTEND=noninteractive + +FROM sunshine-base as sunshine-build + +ARG TARGETPLATFORM +RUN echo "target_platform: ${TARGETPLATFORM}" + +SHELL ["/bin/bash", "-o", "pipefail", "-c"] +# install dependencies +RUN <<_DEPS +#!/bin/bash +apt-get update -y +apt-get install -y --no-install-recommends \ + build-essential=12.8* \ + gcc-10=10.3.0* \ + g++-10=10.3.0* \ + libavdevice-dev=7:4.2.* \ + libboost-filesystem-dev=1.71.0* \ + libboost-log-dev=1.71.0* \ + libboost-program-options-dev=1.71.0* \ + libboost-thread-dev=1.71.0* \ + libcap-dev=1:2.32* \ + libcurl4-openssl-dev=7.68.0* \ + libdrm-dev=2.4.107* \ + libevdev-dev=1.9.0* \ + libnuma-dev=2.0.12* \ + libopus-dev=1.3.1* \ + libpulse-dev=1:13.99.1* \ + libssl-dev=1.1.1* \ + libva-dev=2.7.0* \ + libvdpau-dev=1.3* \ + libwayland-dev=1.18.0* \ + libx11-dev=2:1.6.9* \ + libxcb-shm0-dev=1.14* \ + libxcb-xfixes0-dev=1.14* \ + libxcb1-dev=1.14* \ + libxfixes-dev=1:5.0.3* \ + libxrandr-dev=2:1.5.2* \ + libxtst-dev=2:1.2.3* \ + nodejs=10.19.0* \ + npm=6.14.4* \ + wget=1.20.3* +if [[ "${TARGETPLATFORM}" == 'linux/amd64' ]]; then + apt-get install -y --no-install-recommends \ + libmfx-dev=20.1.0* +fi +apt-get clean +rm -rf /var/lib/apt/lists/* +_DEPS + +# Update gcc alias +# https://stackoverflow.com/a/70653945/11214013 +RUN <<_GCC_ALIAS +#!/bin/bash +update-alternatives --install \ + /usr/bin/gcc gcc /usr/bin/gcc-10 100 \ + --slave /usr/bin/g++ g++ /usr/bin/g++-10 \ + --slave /usr/bin/gcov gcov /usr/bin/gcov-10 \ + --slave /usr/bin/gcc-ar gcc-ar /usr/bin/gcc-ar-10 \ + --slave /usr/bin/gcc-ranlib gcc-ranlib /usr/bin/gcc-ranlib-10 +_GCC_ALIAS + +# install cmake +# sunshine requires cmake >= 3.18 +WORKDIR /build/cmake +# https://cmake.org/download/ +ENV CMAKE_VERSION="3.25.1" +# hadolint ignore=SC3010 +RUN <<_INSTALL_CMAKE +#!/bin/bash +cmake_prefix="https://github.com/Kitware/CMake/releases/download/v" +if [[ "${TARGETPLATFORM}" == 'linux/amd64' ]]; then + cmake_arch="x86_64" +elif [[ "${TARGETPLATFORM}" == 'linux/arm64' ]]; then + cmake_arch="aarch64" +fi +url="${cmake_prefix}${CMAKE_VERSION}/cmake-${CMAKE_VERSION}-linux-${cmake_arch}.sh" +echo "cmake url: ${url}" +wget "$url" --progress=bar:force:noscroll -q --show-progress -O ./cmake.sh +sh ./cmake.sh --prefix=/usr/local --skip-license +cmake --version +_INSTALL_CMAKE + +# install cuda +WORKDIR /build/cuda +# versions: https://developer.nvidia.com/cuda-toolkit-archive +ENV CUDA_VERSION="11.8.0" +ENV CUDA_BUILD="520.61.05" +# hadolint ignore=SC3010 +RUN <<_INSTALL_CUDA +#!/bin/bash +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 .. . + +# setup npm dependencies +RUN npm install + +# setup build directory +WORKDIR /build/sunshine/build + +# cmake and cpack +RUN <<_MAKE +#!/bin/bash +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 --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 --from=artifacts /sunshine*.deb /sunshine.deb + +# install sunshine +RUN <<_INSTALL_SUNSHINE +#!/bin/bash +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 +groupadd -f -g "${PGID}" "${UNAME}" +useradd -lm -d ${HOME} -s /bin/bash -g "${PGID}" -G input -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/ubuntu-22.04.dockerfile b/docker/ubuntu-22.04.dockerfile new file mode 100644 index 00000000..d2a85be4 --- /dev/null +++ b/docker/ubuntu-22.04.dockerfile @@ -0,0 +1,156 @@ +# syntax=docker/dockerfile:1.4 +# artifacts: true +# platforms: linux/amd64,linux/arm64/v8 +# platforms_pr: linux/amd64 +ARG BASE=ubuntu +ARG TAG=22.04 +FROM ${BASE}:${TAG} AS sunshine-base + +ENV DEBIAN_FRONTEND=noninteractive + +FROM sunshine-base as sunshine-build + +ARG TARGETPLATFORM +RUN echo "target_platform: ${TARGETPLATFORM}" + +SHELL ["/bin/bash", "-o", "pipefail", "-c"] +# install dependencies +RUN <<_DEPS +#!/bin/bash +apt-get update -y +apt-get install -y --no-install-recommends \ + build-essential=12.9* \ + cmake=3.22.1* \ + libavdevice-dev=7:4.4.* \ + libboost-filesystem-dev=1.74.0* \ + libboost-log-dev=1.74.0* \ + libboost-program-options-dev=1.74.0* \ + libboost-thread-dev=1.74.0* \ + libcap-dev=1:2.44* \ + libcurl4-openssl-dev=7.81.0* \ + libdrm-dev=2.4.110* \ + libevdev-dev=1.12.1* \ + libnuma-dev=2.0.14* \ + libopus-dev=1.3.1* \ + libpulse-dev=1:15.99.1* \ + libssl-dev=3.0.2* \ + libva-dev=2.14.0* \ + libvdpau-dev=1.4* \ + libwayland-dev=1.20.0* \ + libx11-dev=2:1.7.5* \ + libxcb-shm0-dev=1.14* \ + libxcb-xfixes0-dev=1.14* \ + libxcb1-dev=1.14* \ + libxfixes-dev=1:6.0.0* \ + libxrandr-dev=2:1.5.2* \ + libxtst-dev=2:1.2.3* \ + nodejs=12.22.9* \ + npm=8.5.1* \ + wget=1.21.2* +if [[ "${TARGETPLATFORM}" == 'linux/amd64' ]]; then + apt-get install -y --no-install-recommends \ + libmfx-dev=22.3.0* +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="11.8.0" +ENV CUDA_BUILD="520.61.05" +# hadolint ignore=SC3010 +RUN <<_INSTALL_CUDA +#!/bin/bash +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 .. . + +# setup npm dependencies +RUN npm install + +# setup build directory +WORKDIR /build/sunshine/build + +# cmake and cpack +RUN <<_MAKE +#!/bin/bash +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 --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 --from=artifacts /sunshine*.deb /sunshine.deb + +# install sunshine +RUN <<_INSTALL_SUNSHINE +#!/bin/bash +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 +groupadd -f -g "${PGID}" "${UNAME}" +useradd -lm -d ${HOME} -s /bin/bash -g "${PGID}" -G input -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/docs/requirements.txt b/docs/requirements.txt index 82012203..24cbabce 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,4 @@ furo==2022.12.7 m2r2==0.3.3 -Sphinx==6.1.1 +Sphinx==6.1.3 sphinx-copybutton==0.5.1 diff --git a/docs/source/about/advanced_usage.rst b/docs/source/about/advanced_usage.rst index 03e46983..ee1776c6 100644 --- a/docs/source/about/advanced_usage.rst +++ b/docs/source/about/advanced_usage.rst @@ -739,13 +739,14 @@ encoder .. table:: :widths: auto - ======== =========== - Value Description - ======== =========== - nvenc For Nvidia graphics cards - amdvce For AMD graphics cards - software Encoding occurs on the CPU - ======== =========== + ========= =========== + Value Description + ========= =========== + nvenc For NVIDIA graphics cards + quicksync For Intel graphics cards + amdvce For AMD graphics cards + software Encoding occurs on the CPU + ========= =========== **Default** Sunshine will use the first encoder that is available. @@ -843,7 +844,8 @@ nv_preset **Description** The encoder preset to use. - .. Note:: This option only applies when using nvenc `encoder`_. + .. Note:: This option only applies when using nvenc `encoder`_. For more information on the presets, see + `nvenc preset migration guide `_. **Choices** @@ -958,6 +960,68 @@ nv_coder nv_coder = auto +qsv_preset +^^^^^^^^^^ + +**Description** + The encoder preset to use. + + .. Note:: This option only applies when using quicksync `encoder`_. + +**Choices** + +.. table:: + :widths: auto + + ========== =========== + Value Description + ========== =========== + veryfast fastest (lowest quality) + faster faster (lower quality) + fast fast (low quality) + medium medium (default) + slow slow (good quality) + slower slower (better quality) + veryslow slowest (best quality) + ========== =========== + +**Default** + ``medium`` + +**Example** + .. code-block:: text + + qsv_preset = medium + +qsv_coder +^^^^^^^^^ + +**Description** + The entropy encoding to use. + + .. Note:: This option only applies when using H264 with quicksync `encoder`_. + +**Choices** + +.. table:: + :widths: auto + + ========== =========== + Value Description + ========== =========== + auto let ffmpeg decide + cabac context adaptive binary arithmetic coding - higher quality + cavlc context adaptive variable-length coding - faster decode + ========== =========== + +**Default** + ``auto`` + +**Example** + .. code-block:: text + + qsv_coder = auto + amd_quality ^^^^^^^^^^^ diff --git a/docs/source/about/installation.rst b/docs/source/about/installation.rst index aa64a189..e195c490 100644 --- a/docs/source/about/installation.rst +++ b/docs/source/about/installation.rst @@ -23,6 +23,28 @@ Linux ----- Follow the instructions for your preferred package type below. +**CUDA Compatibility** + +CUDA is used for NVFBC capture. + +.. Tip:: See `CUDA GPUS `_ to cross reference Compute Capability to your GPU. + +.. table:: + :widths: auto + + =========================================== ============== ============== ================================ + Package CUDA Version Min Driver CUDA Compute Capabilities + =========================================== ============== ============== ================================ + https://aur.archlinux.org/packages/sunshine 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_{arch}.flatpak 11.8.0 450.80.02 50;52;60;61;62;70;75;80;86;90;35 + sunshine-debian-bullseye-{arch}.deb 11.8.0 450.80.02 50;52;60;61;62;70;75;80;86;90;35 + sunshine-fedora-36-{arch}.rpm 12.0.0 525.60.13 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-ubuntu-20.04-{arch}.deb 11.8.0 450.80.02 50;52;60;61;62;70;75;80;86;90;35 + sunshine-ubuntu-22.04-{arch}.deb 11.8.0 450.80.02 50;52;60;61;62;70;75;80;86;90;35 + =========================================== ============== ============== ================================ + AppImage ^^^^^^^^ According to AppImageLint the supported distro matrix of the AppImage is below. diff --git a/docs/source/about/usage.rst b/docs/source/about/usage.rst index f6c574f1..7861e523 100644 --- a/docs/source/about/usage.rst +++ b/docs/source/about/usage.rst @@ -87,9 +87,13 @@ Sunshine needs access to `uinput` to create mouse and gamepad events. [Unit] Description=Sunshine self-hosted game stream host for Moonlight. + StartLimitIntervalSec=500 + StartLimitBurst=5 [Service] ExecStart= + Restart=on-failure + RestartSec=5s #Flatpak Only #ExecStop=flatpak kill dev.lizardbyte.sunshine @@ -190,7 +194,7 @@ Shortcuts All shortcuts start with ``CTRL + ALT + SHIFT``, just like Moonlight - ``CTRL + ALT + SHIFT + N`` - Hide/Unhide the cursor (This may be useful for Remote Desktop Mode for Moonlight) -- ``CTRL + ALT + SHIFT + F1/F13`` - Switch to different monitor for Streaming +- ``CTRL + ALT + SHIFT + F1/F12`` - Switch to different monitor for Streaming Application List ---------------- @@ -257,6 +261,18 @@ Considerations instead it simply starts a stream. - For the Linux flatpak you must prepend commands with ``flatpak-spawn --host``. +HDR Support +----------- +Streaming HDR content is supported for Windows hosts with NVIDIA, AMD, or Intel GPUs that support encoding HEVC Main 10. +You must have an HDR-capable display or EDID emulator dongle connected to your host PC to activate HDR in Windows. + +- Ensure you enable the HDR option in your Moonlight client settings, otherwise the stream will be SDR. +- A good HDR experience relies on proper HDR display calibration both in Windows and in game. HDR calibration can differ significantly between client and host displays. +- We recommend calibrating the display by streaming the Windows HDR Calibration app to your client device and saving an HDR calibration profile to use while streaming. +- You may also need to tune the brightness slider or HDR calibration options in game to the different HDR brightness capabilities of your client's display. +- Older games that use NVIDIA-specific NVAPI HDR rather than native Windows 10 OS HDR support may not display in HDR. +- Some GPUs can produce lower image quality or encoding performance when streaming in HDR compared to SDR. + Tutorials --------- Tutorial videos are available `here `_. diff --git a/docs/source/building/linux.rst b/docs/source/building/linux.rst index 2ccdd7c0..36f48d27 100644 --- a/docs/source/building/linux.rst +++ b/docs/source/building/linux.rst @@ -17,11 +17,13 @@ Install Requirements libavdevice-dev \ libboost-filesystem-dev \ libboost-log-dev \ - libboost-thread-dev \ libboost-program-options-dev \ + libboost-thread-dev \ libcap-dev \ # KMS + libcurl4-openssl-dev \ libdrm-dev \ # KMS libevdev-dev \ + libmfx-dev \ # x86_64 only libnuma-dev \ libopus-dev \ libpulse-dev \ @@ -41,25 +43,24 @@ Install Requirements nvidia-cuda-dev \ # Cuda, NvFBC nvidia-cuda-toolkit # Cuda, NvFBC -Fedora 36 -^^^^^^^^^ +Fedora 36, 37 +^^^^^^^^^^^^^ End of Life: TBD -Install Repositories - .. code-block:: bash - - sudo dnf update && \ - sudo dnf group install "Development Tools" && \ - sudo dnf install https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-$(rpm -E %fedora).noarch.rpm - Install Requirements .. code-block:: bash + sudo dnf update && \ + sudo dnf group install "Development Tools" && \ sudo dnf install \ boost-devel \ - boost-static.x86_64 \ + boost-static \ cmake \ + gcc \ gcc-c++ \ + libcap-devel \ + libcurl-devel \ + libdrm-devel \ libevdev-devel \ libva-devel \ libvdpau-devel \ @@ -67,18 +68,21 @@ Install Requirements libxcb-devel \ # X11 libXcursor-devel \ # X11 libXfixes-devel \ # X11 - libXinerama-devel \ # X11 libXi-devel \ # X11 + libXinerama-devel \ # X11 libXrandr-devel \ # X11 libXtst-devel \ # X11 mesa-libGL-devel \ - nodejs \ npm \ numactl-devel \ openssl-devel \ opus-devel \ pulseaudio-libs-devel \ - rpm-build # if you want to build an RPM binary package + rpm-build \ # if you want to build an RPM binary package + wget \ # necessary for cuda install with `run` file + which \ # necessary for cuda install with `run` file + # libmfx-devel is not listed for fedora, this is for x86_64 only + https://kojipkgs.fedoraproject.org//packages/libmfx/1.25/4.el8/x86_64/libmfx-devel-1.25-4.el8.x86_64.rpm Ubuntu 20.04 ^^^^^^^^^^^^ @@ -99,6 +103,7 @@ Install Requirements libcap-dev \ # KMS libdrm-dev \ # KMS libevdev-dev \ + libmfx-dev \ # x86_64 only libnuma-dev \ libopus-dev \ libpulse-dev \ @@ -115,18 +120,17 @@ Install Requirements libxtst-dev \ # X11 nodejs \ npm \ - wget + wget # necessary for cuda install with `run` file Update gcc alias .. code-block:: bash - update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-10 100 --slave /usr/bin/g++ g++ /usr/bin/g++-10 - -Install CuDA - .. 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 --progress=bar:force:noscroll -q --show-progress -O ./cuda.run && chmod a+x ./cuda.run - ./cuda.run --silent --toolkit --toolkitpath=/usr --no-opengl-libs --no-man-page --no-drm && rm ./cuda.run + 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 Ubuntu 22.04 ^^^^^^^^^^^^ @@ -146,6 +150,7 @@ Install Requirements libcap-dev \ # KMS libdrm-dev \ # KMS libevdev-dev \ + libmfx-dev \ # x86_64 only libnuma-dev \ libopus-dev \ libpulse-dev \ @@ -160,8 +165,26 @@ Install Requirements libxtst-dev \ # X11 nodejs \ npm \ - nvidia-cuda-dev \ # Cuda, NvFBC - nvidia-cuda-toolkit # Cuda, NvFBC + nvidia-cuda-dev \ # CUDA, NvFBC + nvidia-cuda-toolkit # CUDA, NvFBC + +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. + + Select the appropriate run file based on your desired CUDA version and architecture according to + `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 \ + --progress=bar:force:noscroll -q --show-progress -O ./cuda.run + chmod a+x ./cuda.run + ./cuda.run --silent --toolkit --toolkitpath=/usr --no-opengl-libs --no-man-page --no-drm + rm ./cuda.run npm dependencies ---------------- diff --git a/docs/source/building/windows.rst b/docs/source/building/windows.rst index 82b0dfe6..0cfb53a4 100644 --- a/docs/source/building/windows.rst +++ b/docs/source/building/windows.rst @@ -16,7 +16,8 @@ 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-openssl mingw-w64-x86_64-opus mingw-w64-x86_64-toolchain + mingw-w64-x86_64-libmfx mingw-w64-x86_64-openssl mingw-w64-x86_64-opus \ + mingw-w64-x86_64-toolchain npm dependencies ---------------- diff --git a/docs/source/gamestream/gamestream.rst b/docs/source/gamestream/gamestream.rst index c5c8d68d..59ba6e1d 100644 --- a/docs/source/gamestream/gamestream.rst +++ b/docs/source/gamestream/gamestream.rst @@ -16,6 +16,5 @@ Limitations ----------- Sunshine does have some limitations, as compared to Nvidia GameStream. -- HDR support is limited and currently HDR is converted to SDR. - Automatic game/application list. - Changing game settings automatically, to optimize streaming. diff --git a/packaging/linux/aur/PKGBUILD b/packaging/linux/aur/PKGBUILD index f8dfbc19..60ac7fed 100644 --- a/packaging/linux/aur/PKGBUILD +++ b/packaging/linux/aur/PKGBUILD @@ -9,9 +9,11 @@ arch=('x86_64' 'i686') url=@PROJECT_HOMEPAGE_URL@ license=('GPL3') -depends=('avahi' 'boost-libs' 'curl' 'libevdev' 'libpulse' 'libva' 'libvdpau' 'libx11' 'libxcb' 'libxfixes' 'libxrandr' 'libxtst' 'numactl' 'openssl' 'opus' 'udev') +depends=('avahi' 'boost-libs' 'curl' 'libevdev' 'libmfx' 'libpulse' 'libva' 'libvdpau' 'libx11' 'libxcb' 'libxfixes' 'libxrandr' 'libxtst' 'numactl' 'openssl' 'opus' 'udev') makedepends=('boost' 'cmake' 'git' 'make' 'nodejs' 'npm') -optdepends=('cuda' 'libcap' 'libdrm') +optdepends=('cuda: NvFBC capture support' + 'libcap' + 'libdrm') provides=(@SUNSHINE_AUR_PROVIDES@) conflicts=(@SUNSHINE_AUR_CONFLICTS@) diff --git a/packaging/linux/flatpak/dev.lizardbyte.sunshine.yml b/packaging/linux/flatpak/dev.lizardbyte.sunshine.yml index a27e4e7a..1ad23b3e 100644 --- a/packaging/linux/flatpak/dev.lizardbyte.sunshine.yml +++ b/packaging/linux/flatpak/dev.lizardbyte.sunshine.yml @@ -40,6 +40,12 @@ modules: - type: archive 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 + - type: shell + commands: + - for n in $(cat patches/series); do if [[ $n != "#"* ]]; then patch -Np1 -i "patches/$n" -d .; fi; done - name: avahi disabled: false @@ -74,6 +80,13 @@ modules: - type: archive url: http://archive.ubuntu.com/ubuntu/pool/main/a/avahi/avahi_0.8.orig.tar.gz sha256: 060309d7a333d38d951bc27598c677af1796934dbd98e1024e7ad8de798fedda + - type: archive + url: http://archive.ubuntu.com/ubuntu/pool/main/a/avahi/avahi_0.8-6ubuntu1.debian.tar.xz + sha256: ebf1dfe5e853b6bc6843e3bd784cb6af632041f305abd0e5415114f80c1dcea4 + - type: shell + commands: + - for n in $(cat patches/series); do if [[ $n != "#"* ]]; then patch -Np1 -i "patches/$n" -d .; fi; done + - autoreconf -ivf - name: libevdev disabled: false @@ -87,34 +100,56 @@ modules: - type: archive url: http://archive.ubuntu.com/ubuntu/pool/main/libe/libevdev/libevdev_1.13.0+dfsg.orig.tar.xz sha256: a882e13ef1dd6bd227318080cabf60fe5af3c06471259d3acfc9dbfb202351a7 + - type: archive + url: http://archive.ubuntu.com/ubuntu/pool/main/libe/libevdev/libevdev_1.13.0+dfsg-1.debian.tar.xz + sha256: d33c56acbbfff2dc540e45c57a38d92210b5e7fd0947ac47fbe48183468aad74 + - type: shell + commands: + - for n in $(cat patches/series); do if [[ $n != "#"* ]]; then patch -Np1 -i "patches/$n" -d .; fi; done - - name: cuda + - name: intel-mediasdk disabled: false - buildsystem: simple + buildsystem: cmake + config-opts: + - -DENABLE_OPENCL=ON + - -DENABLE_X11_DRI3=ON + - -DENABLE_WAYLAND=ON + - -DENABLE_ITT=OFF + - -DENABLE_TEXTLOG=OFF + - -DENABLE_STAT=OFF + - -DBUILD_ALL=OFF + - -DBUILD_RUNTIME=ON + - -DBUILD_SAMPLES=OFF + - -DBUILD_TESTS=OFF + - -DBUILD_TOOLS=OFF + - -DUSE_SYSTEM_GTEST=OFF + - -DMFX_ENABLE_KERNELS=ON only-arches: - x86_64 - - aarch64 - cleanup: - - '*' - build-commands: - - chmod u+x ./cuda.run - - ./cuda.run --silent --toolkit --toolkitpath=$FLATPAK_DEST/cuda --no-opengl-libs --no-man-page --no-drm - --tmpdir=$FLATPAK_BUILDER_BUILDDIR - - rm -r $FLATPAK_DEST/cuda/nsight-systems-2021.3.2 - - rm ./cuda.run sources: - - type: file - only-arches: - - x86_64 - url: https://developer.download.nvidia.com/compute/cuda/11.4.2/local_installers/cuda_11.4.2_470.57.02_linux.run - sha256: bbd87ca0e913f837454a796367473513cddef555082e4d86ed9a38659cc81f0a - dest-filename: cuda.run - - type: file - only-arches: - - aarch64 - url: https://developer.download.nvidia.com/compute/cuda/11.4.2/local_installers/cuda_11.4.2_470.57.02_linux_sbsa.run # yamllint disable-line rule:line-length - sha256: f2c4a52e06329606c8dfb7c5ea3f4cb4c0b28f9d3fdffeeb734fcc98daf580d8 - dest-filename: cuda.run + - type: archive + url: http://archive.ubuntu.com/ubuntu/pool/universe/i/intel-mediasdk/intel-mediasdk_22.3.0.orig.tar.gz + sha256: e1e74229f409e969b70c2b35b1955068de3d40db85ecc42bd6ff501468bc76d7 + - type: archive + url: http://archive.ubuntu.com/ubuntu/pool/universe/i/intel-mediasdk/intel-mediasdk_22.3.0-1.debian.tar.xz + sha256: 024d98d2f63443d2765a90cfe997d104e7b897694889f199ca8fb4d9ffdcf1dc + - type: shell + commands: + - for n in $(cat patches/series); do if [[ $n != "#"* ]]; then patch -Np1 -i "patches/$n" -d .; fi; done + modules: + - name: libdrm + disabled: false + buildsystem: meson + sources: + - type: archive + url: http://archive.ubuntu.com/ubuntu/pool/main/libd/libdrm/libdrm_2.4.110.orig.tar.xz + sha256: eecee4c4b47ed6d6ce1a9be3d6d92102548ea35e442282216d47d05293cf9737 + - type: archive + url: http://archive.ubuntu.com/ubuntu/pool/main/libd/libdrm/libdrm_2.4.110-1ubuntu1.debian.tar.xz + sha256: 464b9553861f39beddfaee6b8924734b02a0febfae3968e4ca1360f2972bba8b + - type: shell + commands: + - for n in $(cat patches/series); do if [[ $n != "#"* ]]; then patch -Np1 -i "patches/$n" -d .; fi; done - name: numactl buildsystem: autotools @@ -124,9 +159,44 @@ modules: - type: archive url: http://archive.ubuntu.com/ubuntu/pool/main/n/numactl/numactl_2.0.14.orig.tar.gz sha256: 1ee27abd07ff6ba140aaf9bc6379b37825e54496e01d6f7343330cf1a4487035 + - type: archive + url: http://archive.ubuntu.com/ubuntu/pool/main/n/numactl/numactl_2.0.14-3ubuntu2.debian.tar.xz + sha256: 49089e5be5367f6367f8b0389d1d523944432607783b53f0605705792e1015ee + - type: shell + commands: + - for n in $(cat patches/series); do if [[ $n != "#"* ]]; then patch -Np1 -i "patches/$n" -d .; fi; done cleanup: - "/bin" + # Caching is configured until here, not including CUDA + - name: cuda + disabled: false + buildsystem: simple + only-arches: + - x86_64 + - aarch64 + cleanup: + - "*" + build-commands: + - chmod u+x ./cuda.run + - ./cuda.run --silent --toolkit --toolkitpath=$FLATPAK_DEST/cuda --no-opengl-libs --no-man-page --no-drm + --tmpdir=$FLATPAK_BUILDER_BUILDDIR + - rm -r $FLATPAK_DEST/cuda/nsight-systems-* + - rm ./cuda.run + sources: + - type: file + only-arches: + - x86_64 + url: https://developer.download.nvidia.com/compute/cuda/11.8.0/local_installers/cuda_11.8.0_520.61.05_linux.run + sha256: 9223c4af3aebe4a7bbed9abd9b163b03a1b34b855fbc2b4a0d1b706ac09a5a16 + dest-filename: cuda.run + - type: file + only-arches: + - aarch64 + url: https://developer.download.nvidia.com/compute/cuda/11.8.0/local_installers/cuda_11.8.0_520.61.05_linux_sbsa.run # yamllint disable-line rule:line-length + sha256: e6e9a8d31163c9776b5e313fd7590877c5684e1ecddee741154f95704d4ed27c + dest-filename: cuda.run + - name: sunshine disabled: false buildsystem: cmake @@ -156,9 +226,9 @@ modules: - -DSUNSHINE_CONFIGURE_FLATPAK=ON sources: - type: git - url: '@GITHUB_CLONE_URL@' - branch: '@GITHUB_BRANCH@' - commit: '@GITHUB_COMMIT@' + url: "@GITHUB_CLONE_URL@" + branch: "@GITHUB_BRANCH@" + commit: "@GITHUB_COMMIT@" post-install: # use `sed` to update apps.json with prefixes required for flatpak # -r (regex) diff --git a/src/config.cpp b/src/config.cpp index 5bc04159..11c393a8 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -200,6 +200,46 @@ int coder_from_view(const std::string_view &coder) { } } // namespace amd +namespace qsv { +enum preset_e : int { + veryslow = 1, + slower = 2, + slow = 3, + medium = 4, + fast = 5, + faster = 6, + veryfast = 7 +}; + +enum cavlc_e : int { + _auto = false, + enabled = true, + disabled = false +}; + +std::optional preset_from_view(const std::string_view &preset) { +#define _CONVERT_(x) \ + if(preset == #x##sv) return x + _CONVERT_(veryslow); + _CONVERT_(slower); + _CONVERT_(slow); + _CONVERT_(medium); + _CONVERT_(fast); + _CONVERT_(faster); + _CONVERT_(veryfast); +#undef _CONVERT_ + return std::nullopt; +} + +std::optional coder_from_view(const std::string_view &coder) { + if(coder == "auto"sv) return _auto; + if(coder == "cabac"sv || coder == "ac"sv) return disabled; + if(coder == "cavlc"sv || coder == "vlc"sv) return enabled; + return std::nullopt; +} + +} // namespace qsv + namespace vt { enum coder_e : int { @@ -254,6 +294,11 @@ video_t video { nv::_auto // coder }, // nv + { + qsv::medium, // preset + qsv::_auto, // cavlc + }, // qsv + { (int)amd::quality_h264_e::balanced, // quality (h264) (int)amd::quality_hevc_e::balanced, // quality (hevc) @@ -261,11 +306,13 @@ video_t video { (int)amd::rc_hevc_e::vbr_latency, // rate control (hevc) (int)amd::coder_e::_auto, // coder }, // amd + { 0, 0, 1, - -1 }, // vt + -1, + }, // vt {}, // encoder {}, // adapter_name @@ -761,6 +808,9 @@ void apply_config(std::unordered_map &&vars) { int_f(vars, "nv_rc", video.nv.rc, nv::rc_from_view); int_f(vars, "nv_coder", video.nv.coder, nv::coder_from_view); + int_f(vars, "qsv_preset", video.qsv.preset, qsv::preset_from_view); + int_f(vars, "qsv_coder", video.qsv.cavlc, qsv::coder_from_view); + std::string quality; string_f(vars, "amd_quality", quality); if(!quality.empty()) { diff --git a/src/config.h b/src/config.h index 7b1c705e..a95e0f85 100644 --- a/src/config.h +++ b/src/config.h @@ -28,6 +28,11 @@ struct video_t { int coder; } nv; + struct { + std::optional preset; + std::optional cavlc; + } qsv; + struct { std::optional quality_h264; std::optional quality_hevc; diff --git a/src/confighttp.cpp b/src/confighttp.cpp index 1276c597..e86ca9c1 100644 --- a/src/confighttp.cpp +++ b/src/confighttp.cpp @@ -56,7 +56,7 @@ void print_req(const req_https_t &request) { BOOST_LOG(debug) << "DESTINATION :: "sv << request->path; for(auto &[name, val] : request->header) { - BOOST_LOG(debug) << name << " -- " << val; + BOOST_LOG(debug) << name << " -- " << (name == "Authorization" ? "CREDENTIALS REDACTED" : val); } BOOST_LOG(debug) << " [--] "sv; @@ -246,23 +246,40 @@ void getSunshineLogoImage(resp_https_t response, req_https_t request) { response->write(SimpleWeb::StatusCode::success_ok, in, headers); } +bool isChildPath(fs::path const &base, fs::path const &query) { + auto relPath = fs::relative(base, query); + return *(relPath.begin()) != fs::path(".."); +} + void getNodeModules(resp_https_t response, req_https_t request) { print_req(request); + fs::path webDirPath(WEB_DIR); + fs::path nodeModulesPath(webDirPath / "node_modules"); - SimpleWeb::CaseInsensitiveMultimap headers; - if(boost::algorithm::iends_with(request->path, ".ttf") == 1) { - std::ifstream in((WEB_DIR + request->path).c_str(), std::ios::binary); - headers.emplace("Content-Type", "font/ttf"); - response->write(SimpleWeb::StatusCode::success_ok, in, headers); + // .relative_path is needed to shed any leading slash that might exist in the request path + auto filePath = fs::weakly_canonical(webDirPath / fs::path(request->path).relative_path()); + + // Don't do anything if file does not exist or is outside the node_modules directory + if(!isChildPath(filePath, nodeModulesPath)) { + BOOST_LOG(warning) << "Someone requested a path " << filePath << " that is outside the node_modules folder"; + response->write(SimpleWeb::StatusCode::client_error_bad_request, "Bad Request"); } - else if(boost::algorithm::iends_with(request->path, ".woff2") == 1) { - std::ifstream in((WEB_DIR + request->path).c_str(), std::ios::binary); - headers.emplace("Content-Type", "font/woff2"); - response->write(SimpleWeb::StatusCode::success_ok, in, headers); + else if(!fs::exists(filePath)) { + response->write(SimpleWeb::StatusCode::client_error_not_found); } else { - std::string content = read_file((WEB_DIR + request->path).c_str()); - response->write(content); + auto relPath = fs::relative(filePath, webDirPath); + if(relPath.extension() == ".ttf" or relPath.extension() == ".woff2") { + // Fonts are read differntly + SimpleWeb::CaseInsensitiveMultimap headers; + std::ifstream in((filePath).c_str(), std::ios::binary); + headers.emplace("Content-Type", "font/" + filePath.extension().string().substr(1)); + response->write(SimpleWeb::StatusCode::success_ok, in, headers); + } + else { + std::string content = read_file((filePath.string()).c_str()); + response->write(content); + } } } diff --git a/src/input.cpp b/src/input.cpp index 2238c316..6cdd6e6f 100644 --- a/src/input.cpp +++ b/src/input.cpp @@ -188,12 +188,20 @@ void print(PNV_SCROLL_PACKET packet) { << "--end mouse scroll packet--"sv; } +void print(PSS_HSCROLL_PACKET packet) { + BOOST_LOG(debug) + << "--begin mouse hscroll packet--"sv << std::endl + << "scrollAmount ["sv << util::endian::big(packet->scrollAmount) << ']' << std::endl + << "--end mouse hscroll packet--"sv; +} + void print(PNV_KEYBOARD_PACKET packet) { BOOST_LOG(debug) << "--begin keyboard packet--"sv << std::endl << "keyAction ["sv << util::hex(packet->header.magic).to_string_view() << ']' << std::endl << "keyCode ["sv << util::hex(packet->keyCode).to_string_view() << ']' << std::endl << "modifiers ["sv << util::hex(packet->modifiers).to_string_view() << ']' << std::endl + << "flags ["sv << util::hex(packet->flags).to_string_view() << ']' << std::endl << "--end keyboard packet--"sv; } @@ -238,6 +246,9 @@ void print(void *payload) { case SCROLL_MAGIC_GEN5: print((PNV_SCROLL_PACKET)payload); break; + case SS_HSCROLL_MAGIC: + print((PSS_HSCROLL_PACKET)payload); + break; case KEY_DOWN_EVENT_MAGIC: case KEY_UP_EVENT_MAGIC: print((PNV_KEYBOARD_PACKET)payload); @@ -459,6 +470,10 @@ void passthrough(PNV_SCROLL_PACKET packet) { platf::scroll(platf_input, util::endian::big(packet->scrollAmt1)); } +void passthrough(PSS_HSCROLL_PACKET packet) { + platf::hscroll(platf_input, util::endian::big(packet->scrollAmount)); +} + void passthrough(PNV_UNICODE_PACKET packet) { auto size = util::endian::big(packet->header.size) - sizeof(packet->header.magic); platf::unicode(platf_input, packet->text, size); @@ -621,6 +636,9 @@ void passthrough_helper(std::shared_ptr input, std::vector +} + struct sockaddr; struct AVFrame; +struct AVBufferRef; +struct AVHWFramesContext; // Forward declarations of boost classes to avoid having to include boost headers // here, which results in issues with Windows.h and WinSock2.h include order. namespace boost { +namespace asio { +namespace ip { +class address; +} // namespace ip +} // namespace asio namespace filesystem { class path; } namespace process { class child; +class group; template class basic_environment; typedef basic_environment environment; } // namespace process } // namespace boost +namespace video { +struct config_t; +} namespace platf { constexpr auto MAX_GAMEPADS = 32; @@ -196,13 +211,25 @@ struct hwdevice_t { /** * implementations must take ownership of 'frame' */ - virtual int set_frame(AVFrame *frame) { + virtual int set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) { BOOST_LOG(error) << "Illegal call to hwdevice_t::set_frame(). Did you forget to override it?"; 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 + */ + virtual void init_hwframes(AVHWFramesContext *frames) {}; + + /** + * Implementations may make modifications required before context derivation + */ + virtual int prepare_to_derive_context(int hw_device_type) { + return 0; + }; + virtual ~hwdevice_t() = default; }; @@ -250,6 +277,15 @@ public: return std::make_shared(); } + virtual bool is_hdr() { + return false; + } + + virtual bool get_hdr_metadata(SS_HDR_METADATA &metadata) { + std::memset(&metadata, 0, sizeof(metadata)); + return false; + } + virtual ~display_t() = default; // Offsets for when streaming a specific monitor. By default, they are 0. @@ -295,16 +331,16 @@ std::unique_ptr audio_control(); * 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. * - * framerate --> The peak number of images per second + * config --> Stream configuration * * Returns display_t based on hwdevice_type */ -std::shared_ptr display(mem_type_e hwdevice_type, const std::string &display_name, int framerate); +std::shared_ptr display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config); // A list of names of displays accepted as display_name with the mem_type_e std::vector display_names(mem_type_e hwdevice_type); -boost::process::child run_unprivileged(const std::string &cmd, boost::filesystem::path &working_dir, boost::process::environment &env, FILE *file, std::error_code &ec); +boost::process::child run_unprivileged(const std::string &cmd, boost::filesystem::path &working_dir, boost::process::environment &env, FILE *file, std::error_code &ec, boost::process::group *group); enum class thread_priority_e : int { low, @@ -321,11 +357,29 @@ void streaming_will_stop(); bool restart_supported(); bool restart(); +struct batched_send_info_t { + const char *buffer; + size_t block_size; + size_t block_count; + + std::uintptr_t native_socket; + boost::asio::ip::address &target_address; + uint16_t target_port; +}; +bool send_batch(batched_send_info_t &send_info); + +enum class qos_data_type_e : int { + audio, + video +}; +std::unique_ptr enable_socket_qos(uintptr_t native_socket, boost::asio::ip::address &address, uint16_t port, qos_data_type_e data_type); + input_t input(); void move_mouse(input_t &input, int deltaX, int deltaY); void abs_mouse(input_t &input, const touch_port_t &touch_port, float x, float y); void button_mouse(input_t &input, int button, bool release); void scroll(input_t &input, int distance); +void hscroll(input_t &input, int distance); void keyboard(input_t &input, uint16_t modcode, bool release); void gamepad(input_t &input, int nr, const gamepad_state_t &gamepad_state); void unicode(input_t &input, char *utf8, int size); diff --git a/src/platform/linux/cuda.cpp b/src/platform/linux/cuda.cpp index 963d7c35..845046d7 100644 --- a/src/platform/linux/cuda.cpp +++ b/src/platform/linux/cuda.cpp @@ -13,6 +13,7 @@ extern "C" { #include "graphics.h" #include "src/main.h" #include "src/utility.h" +#include "src/video.h" #include "wayland.h" #define SUNSHINE_STRINGVIEW_HELPER(x) x##sv @@ -94,20 +95,21 @@ public: return 0; } - int set_frame(AVFrame *frame) override { + int set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) override { this->hwframe.reset(frame); this->frame = frame; - auto hwframe_ctx = (AVHWFramesContext *)frame->hw_frames_ctx->data; + auto hwframe_ctx = (AVHWFramesContext *)hw_frames_ctx->data; if(hwframe_ctx->sw_format != AV_PIX_FMT_NV12) { BOOST_LOG(error) << "cuda::cuda_t doesn't support any format other than AV_PIX_FMT_NV12"sv; return -1; } - if(av_hwframe_get_buffer(frame->hw_frames_ctx, frame, 0)) { - BOOST_LOG(error) << "Couldn't get hwframe for NVENC"sv; - - return -1; + if(!frame->buf[0]) { + if(av_hwframe_get_buffer(hw_frames_ctx, frame, 0)) { + BOOST_LOG(error) << "Couldn't get hwframe for NVENC"sv; + return -1; + } } auto cuda_ctx = (AVCUDADeviceContext *)hwframe_ctx->device_ctx->hwctx; @@ -180,8 +182,8 @@ public: return sws.load_ram(img, tex.array) || sws.convert(frame->data[0], frame->data[1], frame->linesize[0], frame->linesize[1], tex_obj(tex), stream.get()); } - int set_frame(AVFrame *frame) { - if(cuda_t::set_frame(frame)) { + int set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) { + if(cuda_t::set_frame(frame, hw_frames_ctx)) { return -1; } @@ -413,7 +415,7 @@ public: class display_t : public platf::display_t { public: - int init(const std::string_view &display_name, int framerate) { + int init(const std::string_view &display_name, const ::video::config_t &config) { auto handle = handle_t::make(); if(!handle) { return -1; @@ -443,14 +445,14 @@ public: } } - delay = std::chrono::nanoseconds { 1s } / framerate; + delay = std::chrono::nanoseconds { 1s } / config.framerate; capture_params = NVFBC_CREATE_CAPTURE_SESSION_PARAMS { NVFBC_CREATE_CAPTURE_SESSION_PARAMS_VER }; capture_params.eCaptureType = NVFBC_CAPTURE_SHARED_CUDA; capture_params.bDisableAutoModesetRecovery = nv_bool(true); - capture_params.dwSamplingRateMs = 1000 /* ms */ / framerate; + capture_params.dwSamplingRateMs = 1000 /* ms */ / config.framerate; if(streamedMonitor != -1) { auto &output = status_params->outputs[streamedMonitor]; @@ -662,7 +664,7 @@ public: } // namespace cuda namespace platf { -std::shared_ptr nvfbc_display(mem_type_e hwdevice_type, const std::string &display_name, int framerate) { +std::shared_ptr nvfbc_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config) { if(hwdevice_type != mem_type_e::cuda) { BOOST_LOG(error) << "Could not initialize nvfbc display with the given hw device type"sv; return nullptr; @@ -670,7 +672,7 @@ std::shared_ptr nvfbc_display(mem_type_e hwdevice_type, const std::st auto display = std::make_shared(); - if(display->init(display_name, framerate)) { + if(display->init(display_name, config)) { return nullptr; } diff --git a/src/platform/linux/input.cpp b/src/platform/linux/input.cpp index 23acb33e..c4585e1c 100644 --- a/src/platform/linux/input.cpp +++ b/src/platform/linux/input.cpp @@ -982,6 +982,19 @@ void scroll(input_t &input, int high_res_distance) { libevdev_uinput_write_event(mouse, EV_SYN, SYN_REPORT, 0); } +void hscroll(input_t &input, int high_res_distance) { + int distance = high_res_distance / 120; + + auto mouse = ((input_raw_t *)input.get())->mouse_input.get(); + if(!mouse) { + return; + } + + libevdev_uinput_write_event(mouse, EV_REL, REL_HWHEEL, distance); + libevdev_uinput_write_event(mouse, EV_REL, REL_HWHEEL_HI_RES, high_res_distance); + libevdev_uinput_write_event(mouse, EV_SYN, SYN_REPORT, 0); +} + static keycode_t keysym(std::uint16_t modcode) { if(modcode <= keycodes.size()) { return keycodes[modcode]; diff --git a/src/platform/linux/kmsgrab.cpp b/src/platform/linux/kmsgrab.cpp index 2c84baea..240ce696 100644 --- a/src/platform/linux/kmsgrab.cpp +++ b/src/platform/linux/kmsgrab.cpp @@ -12,6 +12,7 @@ #include "src/platform/common.h" #include "src/round_robin.h" #include "src/utility.h" +#include "src/video.h" // Cursor rendering support through x11 #include "graphics.h" @@ -293,6 +294,18 @@ public: return false; } + std::uint32_t get_panel_orientation(std::uint32_t plane_id) { + auto props = plane_props(plane_id); + for(auto &[prop, val] : props) { + if(prop->name == "rotation"sv) { + return val; + } + } + + BOOST_LOG(error) << "Failed to determine panel orientation, defaulting to landscape."; + return DRM_MODE_ROTATE_0; + } + connector_interal_t connector(std::uint32_t id) { return drmModeGetConnector(fd.el, id); } @@ -444,8 +457,8 @@ class display_t : public platf::display_t { public: display_t(mem_type_e mem_type) : platf::display_t(), mem_type { mem_type } {} - int init(const std::string &display_name, int framerate) { - delay = std::chrono::nanoseconds { 1s } / framerate; + int init(const std::string &display_name, const ::video::config_t &config) { + delay = std::chrono::nanoseconds { 1s } / config.framerate; int monitor_index = util::from_view(display_name); int monitor = 0; @@ -530,11 +543,25 @@ public: if(monitor != std::end(pos->crtc_to_monitor)) { auto &viewport = monitor->second.viewport; - width = viewport.width; - height = viewport.height; + width = viewport.width; + height = viewport.height; + + switch(card.get_panel_orientation(plane->plane_id)) { + case DRM_MODE_ROTATE_270: + BOOST_LOG(debug) << "Detected panel orientation at 90, swapping width and height."; + width = viewport.height; + height = viewport.width; + break; + case DRM_MODE_ROTATE_90: + case DRM_MODE_ROTATE_180: + BOOST_LOG(warning) << "Panel orientation is unsupported, screen capture may not work correctly."; + break; + } + offset_x = viewport.offset_x; offset_y = viewport.offset_y; } + // This code path shouldn't happend, but it's there just in case. // crtc_to_monitor is part of the guesswork after all. else { @@ -583,9 +610,12 @@ public: for(int y = 0; y < 4; ++y) { if(!fb->handles[y]) { - // It's not clear wheter there could still be valid handles left. + // setting sd->fds[y] to a negative value indicates that sd->offsets[y] and sd->pitches[y] + // are uninitialized and contain invalid values. + sd->fds[y] = -1; + // It's not clear whether there could still be valid handles left. // So, continue anyway. - // TODO: Is this redundent? + // TODO: Is this redundant? continue; } @@ -632,13 +662,13 @@ class display_ram_t : public display_t { public: display_ram_t(mem_type_e mem_type) : display_t(mem_type) {} - int init(const std::string &display_name, int framerate) { + int init(const std::string &display_name, const ::video::config_t &config) { if(!gbm::create_device) { BOOST_LOG(warning) << "libgbm not initialized"sv; return -1; } - if(display_t::init(display_name, framerate)) { + if(display_t::init(display_name, config)) { return -1; } @@ -852,8 +882,8 @@ public: return capture_e::ok; } - int init(const std::string &display_name, int framerate) { - if(display_t::init(display_name, framerate)) { + int init(const std::string &display_name, const ::video::config_t &config) { + if(display_t::init(display_name, config)) { return -1; } @@ -872,11 +902,11 @@ public: } // namespace kms -std::shared_ptr kms_display(mem_type_e hwdevice_type, const std::string &display_name, int framerate) { +std::shared_ptr kms_display(mem_type_e hwdevice_type, const std::string &display_name, const ::video::config_t &config) { if(hwdevice_type == mem_type_e::vaapi) { auto disp = std::make_shared(hwdevice_type); - if(!disp->init(display_name, framerate)) { + if(!disp->init(display_name, config)) { return disp; } @@ -885,7 +915,7 @@ std::shared_ptr kms_display(mem_type_e hwdevice_type, const std::stri auto disp = std::make_shared(hwdevice_type); - if(disp->init(display_name, framerate)) { + if(disp->init(display_name, config)) { return nullptr; } diff --git a/src/platform/linux/misc.cpp b/src/platform/linux/misc.cpp index 35e68b96..081fb534 100644 --- a/src/platform/linux/misc.cpp +++ b/src/platform/linux/misc.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include @@ -14,6 +15,7 @@ #include "src/main.h" #include "src/platform/common.h" +#include #include #ifdef __GNUC__ @@ -143,13 +145,23 @@ std::string get_mac_address(const std::string_view &address) { return "00:00:00:00:00:00"s; } -bp::child run_unprivileged(const std::string &cmd, boost::filesystem::path &working_dir, bp::environment &env, FILE *file, std::error_code &ec) { +bp::child run_unprivileged(const std::string &cmd, boost::filesystem::path &working_dir, bp::environment &env, FILE *file, std::error_code &ec, bp::group *group) { BOOST_LOG(warning) << "run_unprivileged() is not yet implemented for this platform. The new process will run with Sunshine's permissions."sv; - if(!file) { - return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_out > bp::null, bp::std_err > bp::null, ec); + 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); + } + else { + return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_out > file, bp::std_err > file, ec); + } } else { - return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_out > file, bp::std_err > file, ec); + if(!file) { + return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_out > bp::null, bp::std_err > bp::null, ec, *group); + } + else { + return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_out > file, bp::std_err > file, ec, *group); + } } } @@ -175,6 +187,215 @@ bool restart() { return false; } +bool send_batch(batched_send_info_t &send_info) { + auto sockfd = (int)send_info.native_socket; + + // Convert the target address into a sockaddr + struct sockaddr_in saddr_v4 = {}; + struct sockaddr_in6 saddr_v6 = {}; + struct sockaddr *addr; + socklen_t addr_len; + if(send_info.target_address.is_v6()) { + auto address_v6 = send_info.target_address.to_v6(); + + saddr_v6.sin6_family = AF_INET6; + saddr_v6.sin6_port = htons(send_info.target_port); + saddr_v6.sin6_scope_id = address_v6.scope_id(); + + auto addr_bytes = address_v6.to_bytes(); + memcpy(&saddr_v6.sin6_addr, addr_bytes.data(), sizeof(saddr_v6.sin6_addr)); + + addr = (struct sockaddr *)&saddr_v6; + addr_len = sizeof(saddr_v6); + } + else { + auto address_v4 = send_info.target_address.to_v4(); + + saddr_v4.sin_family = AF_INET; + saddr_v4.sin_port = htons(send_info.target_port); + + auto addr_bytes = address_v4.to_bytes(); + memcpy(&saddr_v4.sin_addr, addr_bytes.data(), sizeof(saddr_v4.sin_addr)); + + addr = (struct sockaddr *)&saddr_v4; + addr_len = sizeof(saddr_v4); + } + +#ifdef UDP_SEGMENT + { + struct msghdr msg = {}; + struct iovec iov = {}; + union { + char buf[CMSG_SPACE(sizeof(uint16_t))]; + struct cmsghdr alignment; + } cmbuf; + + // UDP GSO on Linux currently only supports sending 64K or 64 segments at a time + size_t seg_index = 0; + const size_t seg_max = 65536 / 1500; + while(seg_index < send_info.block_count) { + 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)); + + // Enable GSO to perform segmentation of our buffer for us + auto cm = CMSG_FIRSTHDR(&msg); + 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; + } + + // This will fail if GSO is not available, so we will fall back to non-GSO if + // it's the first sendmsg() call. On subsequent calls, we will treat errors as + // actual failures and return to the caller. + auto bytes_sent = sendmsg(sockfd, &msg, 0); + if(bytes_sent < 0) { + // If there's no send buffer space, wait for some to be available + if(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 + continue; + } + + break; + } + + seg_index += bytes_sent / send_info.block_size; + } + + // If we sent something, return the status and don't fall back to the non-GSO path. + if(seg_index != 0) { + return seg_index >= send_info.block_count; + } + } +#endif + + { + // If GSO is not supported, use sendmmsg() instead. + struct mmsghdr msgs[send_info.block_count]; + struct iovec iovs[send_info.block_count]; + for(size_t i = 0; i < send_info.block_count; i++) { + iovs[i] = {}; + iovs[i].iov_base = (void *)&send_info.buffer[i * send_info.block_size]; + 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_iov = &iovs[i]; + msgs[i].msg_hdr.msg_iovlen = 1; + } + + // Call sendmmsg() until all messages are sent + size_t blocks_sent = 0; + while(blocks_sent < send_info.block_count) { + int msgs_sent = sendmmsg(sockfd, &msgs[blocks_sent], send_info.block_count - blocks_sent, 0); + if(msgs_sent < 0) { + // If there's no send buffer space, wait for some to be available + if(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 + continue; + } + + BOOST_LOG(warning) << "sendmmsg() failed: "sv << errno; + return false; + } + + blocks_sent += msgs_sent; + } + + return true; + } +} + +class qos_t : public deinit_t { +public: + qos_t(int sockfd, int level, int option) : sockfd(sockfd), level(level), option(option) {} + + virtual ~qos_t() { + int reset_val = -1; + if(setsockopt(sockfd, level, option, &reset_val, sizeof(reset_val)) < 0) { + BOOST_LOG(warning) << "Failed to reset IP TOS: "sv << errno; + } + } + +private: + int sockfd; + int level; + int option; +}; + +std::unique_ptr enable_socket_qos(uintptr_t native_socket, boost::asio::ip::address &address, uint16_t port, qos_data_type_e data_type) { + int sockfd = (int)native_socket; + + int level; + int option; + if(address.is_v6()) { + level = SOL_IPV6; + option = IPV6_TCLASS; + } + else { + level = SOL_IP; + option = IP_TOS; + } + + // The specific DSCP values here are chosen to be consistent with Windows + int dscp; + switch(data_type) { + case qos_data_type_e::video: + dscp = 40; + break; + case qos_data_type_e::audio: + dscp = 56; + break; + default: + BOOST_LOG(error) << "Unknown traffic type: "sv << (int)data_type; + return nullptr; + } + + // Shift to put the DSCP value in the correct position in the TOS field + dscp <<= 2; + + if(setsockopt(sockfd, level, option, &dscp, sizeof(dscp)) < 0) { + return nullptr; + } + + return std::make_unique(sockfd, level, option); +} + namespace source { enum source_e : std::size_t { #ifdef SUNSHINE_BUILD_CUDA @@ -197,7 +418,7 @@ static std::bitset sources; #ifdef SUNSHINE_BUILD_CUDA std::vector nvfbc_display_names(); -std::shared_ptr nvfbc_display(mem_type_e hwdevice_type, const std::string &display_name, int framerate); +std::shared_ptr nvfbc_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config); bool verify_nvfbc() { return !nvfbc_display_names().empty(); @@ -206,7 +427,7 @@ bool verify_nvfbc() { #ifdef SUNSHINE_BUILD_WAYLAND std::vector wl_display_names(); -std::shared_ptr wl_display(mem_type_e hwdevice_type, const std::string &display_name, int framerate); +std::shared_ptr wl_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config); bool verify_wl() { return window_system == window_system_e::WAYLAND && !wl_display_names().empty(); @@ -215,7 +436,7 @@ bool verify_wl() { #ifdef SUNSHINE_BUILD_DRM std::vector kms_display_names(); -std::shared_ptr kms_display(mem_type_e hwdevice_type, const std::string &display_name, int framerate); +std::shared_ptr kms_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config); bool verify_kms() { return !kms_display_names().empty(); @@ -224,7 +445,7 @@ bool verify_kms() { #ifdef SUNSHINE_BUILD_X11 std::vector x11_display_names(); -std::shared_ptr x11_display(mem_type_e hwdevice_type, const std::string &display_name, int framerate); +std::shared_ptr x11_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config); bool verify_x11() { return window_system == window_system_e::X11 && !x11_display_names().empty(); @@ -248,29 +469,29 @@ std::vector display_names(mem_type_e hwdevice_type) { return {}; } -std::shared_ptr display(mem_type_e hwdevice_type, const std::string &display_name, int framerate) { +std::shared_ptr display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config) { #ifdef SUNSHINE_BUILD_CUDA if(sources[source::NVFBC] && hwdevice_type == mem_type_e::cuda) { BOOST_LOG(info) << "Screencasting with NvFBC"sv; - return nvfbc_display(hwdevice_type, display_name, framerate); + return nvfbc_display(hwdevice_type, display_name, config); } #endif #ifdef SUNSHINE_BUILD_WAYLAND if(sources[source::WAYLAND]) { BOOST_LOG(info) << "Screencasting with Wayland's protocol"sv; - return wl_display(hwdevice_type, display_name, framerate); + return wl_display(hwdevice_type, display_name, config); } #endif #ifdef SUNSHINE_BUILD_DRM if(sources[source::KMS]) { BOOST_LOG(info) << "Screencasting with KMS"sv; - return kms_display(hwdevice_type, display_name, framerate); + return kms_display(hwdevice_type, display_name, config); } #endif #ifdef SUNSHINE_BUILD_X11 if(sources[source::X11]) { BOOST_LOG(info) << "Screencasting with X11"sv; - return x11_display(hwdevice_type, display_name, framerate); + return x11_display(hwdevice_type, display_name, config); } #endif diff --git a/src/platform/linux/vaapi.cpp b/src/platform/linux/vaapi.cpp index 9dcb2dfe..8f97db27 100644 --- a/src/platform/linux/vaapi.cpp +++ b/src/platform/linux/vaapi.cpp @@ -313,14 +313,15 @@ public: return 0; } - int set_frame(AVFrame *frame) override { + int set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) override { this->hwframe.reset(frame); this->frame = frame; - if(av_hwframe_get_buffer(frame->hw_frames_ctx, frame, 0)) { - BOOST_LOG(error) << "Couldn't get hwframe for VAAPI"sv; - - return -1; + if(!frame->buf[0]) { + if(av_hwframe_get_buffer(hw_frames_ctx, frame, 0)) { + BOOST_LOG(error) << "Couldn't get hwframe for VAAPI"sv; + return -1; + } } va::DRMPRIMESurfaceDescriptor prime; @@ -385,12 +386,14 @@ public: va::display_t::pointer va_display; file_t file; - frame_t hwframe; - gbm::gbm_t gbm; egl::display_t display; egl::ctx_t ctx; + // This must be destroyed before display_t to ensure the GPU + // driver is still loaded when vaDestroySurfaces() is called. + frame_t hwframe; + egl::sws_t sws; egl::nv12_t nv12; diff --git a/src/platform/linux/wlgrab.cpp b/src/platform/linux/wlgrab.cpp index 20d89572..ea4adc8d 100644 --- a/src/platform/linux/wlgrab.cpp +++ b/src/platform/linux/wlgrab.cpp @@ -1,6 +1,7 @@ #include "src/platform/common.h" #include "src/main.h" +#include "src/video.h" #include "vaapi.h" #include "wayland.h" @@ -18,8 +19,8 @@ struct img_t : public platf::img_t { class wlr_t : public platf::display_t { public: - int init(platf::mem_type_e hwdevice_type, const std::string &display_name, int framerate) { - delay = std::chrono::nanoseconds { 1s } / framerate; + int init(platf::mem_type_e hwdevice_type, const std::string &display_name, const ::video::config_t &config) { + delay = std::chrono::nanoseconds { 1s } / config.framerate; mem_type = hwdevice_type; if(display.init()) { @@ -167,7 +168,7 @@ public: int w, h; gl::ctx.GetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &w); gl::ctx.GetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &h); - BOOST_LOG(debug) << "width and height: w "sv << w << ' h ' << h; + BOOST_LOG(debug) << "width and height: w "sv << w << " h "sv << h; gl::ctx.GetTextureSubImage((*rgb_opt)->tex[0], 0, 0, 0, 0, width, height, 1, GL_BGRA, GL_UNSIGNED_BYTE, img_out_base->height * img_out_base->row_pitch, img_out_base->data); gl::ctx.BindTexture(GL_TEXTURE_2D, 0); @@ -175,8 +176,8 @@ public: return platf::capture_e::ok; } - int init(platf::mem_type_e hwdevice_type, const std::string &display_name, int framerate) { - if(wlr_t::init(hwdevice_type, display_name, framerate)) { + int init(platf::mem_type_e hwdevice_type, const std::string &display_name, const ::video::config_t &config) { + if(wlr_t::init(hwdevice_type, display_name, config)) { return -1; } @@ -307,7 +308,7 @@ public: } // namespace wl namespace platf { -std::shared_ptr wl_display(mem_type_e hwdevice_type, const std::string &display_name, int framerate) { +std::shared_ptr wl_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config) { if(hwdevice_type != platf::mem_type_e::system && hwdevice_type != platf::mem_type_e::vaapi && hwdevice_type != platf::mem_type_e::cuda) { BOOST_LOG(error) << "Could not initialize display with the given hw device type."sv; return nullptr; @@ -315,7 +316,7 @@ std::shared_ptr wl_display(mem_type_e hwdevice_type, const std::strin if(hwdevice_type == platf::mem_type_e::vaapi) { auto wlr = std::make_shared(); - if(wlr->init(hwdevice_type, display_name, framerate)) { + if(wlr->init(hwdevice_type, display_name, config)) { return nullptr; } @@ -323,7 +324,7 @@ std::shared_ptr wl_display(mem_type_e hwdevice_type, const std::strin } auto wlr = std::make_shared(); - if(wlr->init(hwdevice_type, display_name, framerate)) { + if(wlr->init(hwdevice_type, display_name, config)) { return nullptr; } diff --git a/src/platform/linux/x11grab.cpp b/src/platform/linux/x11grab.cpp index 1f8f5b73..e18de69d 100644 --- a/src/platform/linux/x11grab.cpp +++ b/src/platform/linux/x11grab.cpp @@ -19,6 +19,7 @@ #include "src/config.h" #include "src/main.h" #include "src/task_pool.h" +#include "src/video.h" #include "cuda.h" #include "graphics.h" @@ -382,13 +383,13 @@ struct x11_attr_t : public display_t { x11::InitThreads(); } - int init(const std::string &display_name, int framerate) { + int init(const std::string &display_name, const ::video::config_t &config) { if(!xdisplay) { BOOST_LOG(error) << "Could not open X11 display"sv; return -1; } - delay = std::chrono::nanoseconds { 1s } / framerate; + delay = std::chrono::nanoseconds { 1s } / config.framerate; xwindow = DefaultRootWindow(xdisplay.get()); @@ -641,8 +642,8 @@ struct shm_attr_t : public x11_attr_t { return 0; } - int init(const std::string &display_name, int framerate) { - if(x11_attr_t::init(display_name, framerate)) { + int init(const std::string &display_name, const ::video::config_t &config) { + if(x11_attr_t::init(display_name, config)) { return 1; } @@ -685,7 +686,7 @@ struct shm_attr_t : public x11_attr_t { } }; -std::shared_ptr x11_display(platf::mem_type_e hwdevice_type, const std::string &display_name, int framerate) { +std::shared_ptr x11_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 && hwdevice_type != platf::mem_type_e::vaapi && hwdevice_type != platf::mem_type_e::cuda) { BOOST_LOG(error) << "Could not initialize x11 display with the given hw device type"sv; return nullptr; @@ -700,7 +701,7 @@ std::shared_ptr x11_display(platf::mem_type_e hwdevice_type, const st // Attempt to use shared memory X11 to avoid copying the frame auto shm_disp = std::make_shared(hwdevice_type); - auto status = shm_disp->init(display_name, framerate); + auto status = shm_disp->init(display_name, config); if(status > 0) { // x11_attr_t::init() failed, don't bother trying again. return nullptr; @@ -712,7 +713,7 @@ std::shared_ptr x11_display(platf::mem_type_e hwdevice_type, const st // Fallback auto x11_disp = std::make_shared(hwdevice_type); - if(x11_disp->init(display_name, framerate)) { + if(x11_disp->init(display_name, config)) { return nullptr; } diff --git a/src/platform/macos/display.mm b/src/platform/macos/display.mm index a53ac0e6..c818235e 100644 --- a/src/platform/macos/display.mm +++ b/src/platform/macos/display.mm @@ -6,6 +6,11 @@ #include "src/config.h" #include "src/main.h" +// Avoid conflict between AVFoundation and libavutil both defining AVMediaType +#define AVMediaType AVMediaType_FFmpeg +#include "src/video.h" +#undef AVMediaType + namespace fs = std::filesystem; namespace platf { @@ -147,7 +152,7 @@ struct av_display_t : public display_t { } }; -std::shared_ptr display(platf::mem_type_e hwdevice_type, const std::string &display_name, int framerate) { +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) { BOOST_LOG(error) << "Could not initialize display with the given hw device type."sv; return nullptr; @@ -168,7 +173,7 @@ std::shared_ptr display(platf::mem_type_e hwdevice_type, const std::s } } - display->av_capture = [[AVVideo alloc] initWithDisplay:display->display_id frameRate:framerate]; + display->av_capture = [[AVVideo alloc] initWithDisplay:display->display_id frameRate:config.framerate]; if(!display->av_capture) { BOOST_LOG(error) << "Video setup failed."sv; diff --git a/src/platform/macos/input.cpp b/src/platform/macos/input.cpp index f1970944..ff59f4fb 100644 --- a/src/platform/macos/input.cpp +++ b/src/platform/macos/input.cpp @@ -417,6 +417,10 @@ void scroll(input_t &input, int high_res_distance) { CFRelease(upEvent); } +void hscroll(input_t &input, int high_res_distance) { + // Unimplemented +} + input_t input() { input_t result { new macos_input_t() }; diff --git a/src/platform/macos/misc.cpp b/src/platform/macos/misc.cpp index eb99de78..cb9c0c52 100644 --- a/src/platform/macos/misc.cpp +++ b/src/platform/macos/misc.cpp @@ -121,13 +121,23 @@ std::string get_mac_address(const std::string_view &address) { return "00:00:00:00:00:00"s; } -bp::child run_unprivileged(const std::string &cmd, boost::filesystem::path &working_dir, bp::environment &env, FILE *file, std::error_code &ec) { +bp::child run_unprivileged(const std::string &cmd, boost::filesystem::path &working_dir, bp::environment &env, FILE *file, std::error_code &ec, bp::group *group) { BOOST_LOG(warning) << "run_unprivileged() is not yet implemented for this platform. The new process will run with Sunshine's permissions."sv; - if(!file) { - return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_out > bp::null, bp::std_err > bp::null, ec); + 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); + } + else { + return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_out > file, bp::std_err > file, ec); + } } else { - return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_out > file, bp::std_err > file, ec); + if(!file) { + return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_out > bp::null, bp::std_err > bp::null, ec, *group); + } + else { + return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_out > file, bp::std_err > file, ec, *group); + } } } @@ -153,6 +163,18 @@ bool restart() { return false; } +bool send_batch(batched_send_info_t &send_info) { + // Fall back to unbatched send calls + return false; +} + +std::unique_ptr enable_socket_qos(uintptr_t native_socket, boost::asio::ip::address &address, uint16_t port, qos_data_type_e data_type) { + // Unimplemented + // + // NB: When implementing, remember to consider that some routes can drop DSCP-tagged packets completely! + return nullptr; +} + } // namespace platf namespace dyn { diff --git a/src/platform/macos/nv12_zero_device.cpp b/src/platform/macos/nv12_zero_device.cpp index 1af0e058..71e58307 100644 --- a/src/platform/macos/nv12_zero_device.cpp +++ b/src/platform/macos/nv12_zero_device.cpp @@ -53,7 +53,7 @@ int nv12_zero_device::convert(platf::img_t &img) { return result > 0 ? 0 : -1; } -int nv12_zero_device::set_frame(AVFrame *frame) { +int nv12_zero_device::set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) { this->frame = frame; av_frame.reset(frame); diff --git a/src/platform/macos/nv12_zero_device.h b/src/platform/macos/nv12_zero_device.h index 3b74ebcc..1863fb0f 100644 --- a/src/platform/macos/nv12_zero_device.h +++ b/src/platform/macos/nv12_zero_device.h @@ -20,7 +20,7 @@ public: int init(void *display, resolution_fn_t resolution_fn, pixel_format_fn_t pixel_format_fn); int convert(img_t &img); - int set_frame(AVFrame *frame); + int set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx); void set_colorspace(std::uint32_t colorspace, std::uint32_t color_range); }; diff --git a/src/platform/windows/display.h b/src/platform/windows/display.h index a788df51..dac853fc 100644 --- a/src/platform/windows/display.h +++ b/src/platform/windows/display.h @@ -10,7 +10,7 @@ #include #include #include -#include +#include #include "src/platform/common.h" #include "src/utility.h" @@ -37,6 +37,7 @@ using adapter_t = util::safe_ptr>; using output1_t = util::safe_ptr>; using output5_t = util::safe_ptr>; +using output6_t = util::safe_ptr>; using dup_t = util::safe_ptr>; using texture2d_t = util::safe_ptr>; using texture1d_t = util::safe_ptr>; @@ -115,7 +116,8 @@ public: class display_base_t : public display_t { public: - int init(int framerate, const std::string &display_name); + int init(const ::video::config_t &config, const std::string &display_name); + capture_e capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr img, bool *cursor) override; std::chrono::nanoseconds delay; @@ -140,29 +142,34 @@ public: typedef NTSTATUS WINAPI (*PD3DKMTSetProcessSchedulingPriorityClass)(HANDLE, D3DKMT_SCHEDULINGPRIORITYCLASS); + virtual bool is_hdr() override; + virtual bool get_hdr_metadata(SS_HDR_METADATA &metadata) override; + protected: int get_pixel_pitch() { return (capture_format == DXGI_FORMAT_R16G16B16A16_FLOAT) ? 8 : 4; } const char *dxgi_format_to_string(DXGI_FORMAT format); + const char *colorspace_to_string(DXGI_COLOR_SPACE_TYPE type); - virtual int complete_img(img_t *img, bool dummy) = 0; - virtual std::vector get_supported_sdr_capture_formats() = 0; + virtual capture_e snapshot(img_t *img, std::chrono::milliseconds timeout, bool cursor_visible) = 0; + virtual int complete_img(img_t *img, bool dummy) = 0; + virtual std::vector get_supported_sdr_capture_formats() = 0; + virtual std::vector get_supported_hdr_capture_formats() = 0; }; class display_ram_t : public display_base_t { public: - capture_e capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr img, bool *cursor) override; - capture_e snapshot(img_t *img, std::chrono::milliseconds timeout, bool cursor_visible); - + virtual capture_e snapshot(img_t *img, std::chrono::milliseconds timeout, bool cursor_visible) override; std::shared_ptr alloc_img() override; int dummy_img(img_t *img) override; int complete_img(img_t *img, bool dummy) override; std::vector get_supported_sdr_capture_formats() override; + std::vector get_supported_hdr_capture_formats() override; - int init(int framerate, const std::string &display_name); + int init(const ::video::config_t &config, const std::string &display_name); cursor_t cursor; D3D11_MAPPED_SUBRESOURCE img_info; @@ -171,15 +178,15 @@ public: class display_vram_t : public display_base_t, public std::enable_shared_from_this { public: - capture_e capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr img, bool *cursor) override; - capture_e snapshot(img_t *img, std::chrono::milliseconds timeout, bool cursor_visible); + virtual capture_e snapshot(img_t *img, std::chrono::milliseconds timeout, bool cursor_visible) override; std::shared_ptr alloc_img() override; int dummy_img(img_t *img_base) override; int complete_img(img_t *img_base, bool dummy) override; std::vector get_supported_sdr_capture_formats() override; + std::vector get_supported_hdr_capture_formats() override; - int init(int framerate, const std::string &display_name); + int init(const ::video::config_t &config, const std::string &display_name); std::shared_ptr make_hwdevice(pix_fmt_e pix_fmt) override; @@ -196,6 +203,8 @@ public: gpu_cursor_t cursor_xor; texture2d_t last_frame_copy; + + std::atomic next_image_id; }; } // namespace platf::dxgi diff --git a/src/platform/windows/display_base.cpp b/src/platform/windows/display_base.cpp index 9f8c2705..39be1112 100644 --- a/src/platform/windows/display_base.cpp +++ b/src/platform/windows/display_base.cpp @@ -6,16 +6,25 @@ #include #include +#include + +// We have to include boost/process.hpp before display.h due to WinSock.h, +// but that prevents the definition of NTSTATUS so we must define it ourself. +typedef long NTSTATUS; + #include "display.h" #include "misc.h" #include "src/config.h" #include "src/main.h" #include "src/platform/common.h" +#include "src/video.h" namespace platf { using namespace std::literals; } namespace platf::dxgi { +namespace bp = boost::process; + capture_e duplication_t::next_frame(DXGI_OUTDUPL_FRAME_INFO &frame_info, std::chrono::milliseconds timeout, resource_t::pointer *res_p) { auto capture_status = release_frame(); if(capture_status != capture_e::ok) { @@ -79,7 +88,202 @@ duplication_t::~duplication_t() { release_frame(); } -int display_base_t::init(int framerate, const std::string &display_name) { +capture_e display_base_t::capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr<::platf::img_t> img, 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 close_timer = util::fail_guard([timer]() { + CloseHandle(timer); + }); + + while(img) { + // 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 + // the display subsystem. It is recommended to call this once per frame. + if(!factory->IsCurrent()) { + 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; + } + 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; + } + + auto status = snapshot(img.get(), 1000ms, *cursor); + switch(status) { + case platf::capture_e::reinit: + case platf::capture_e::error: + return status; + case platf::capture_e::timeout: + img = snapshot_cb(img, false); + break; + case platf::capture_e::ok: + img = snapshot_cb(img, true); + break; + default: + BOOST_LOG(error) << "Unrecognized capture status ["sv << (int)status << ']'; + return status; + } + } + + return capture_e::ok; +} + +bool set_gpu_preference_on_self(int preference) { + // The GPU preferences key uses app path as the value name. + WCHAR sunshine_path[MAX_PATH]; + GetModuleFileNameW(NULL, sunshine_path, ARRAYSIZE(sunshine_path)); + + WCHAR value_data[128]; + swprintf_s(value_data, L"GpuPreference=%d;", preference); + + auto status = RegSetKeyValueW(HKEY_CURRENT_USER, + L"Software\\Microsoft\\DirectX\\UserGpuPreferences", + sunshine_path, + REG_SZ, + value_data, + (wcslen(value_data) + 1) * sizeof(WCHAR)); + if(status != ERROR_SUCCESS) { + BOOST_LOG(error) << "Failed to set GPU preference: "sv << status; + return false; + } + + BOOST_LOG(info) << "Set GPU preference: "sv << preference; + return true; +} + +// On hybrid graphics systems, Windows will change the order of GPUs reported by +// DXGI in accordance with the user's GPU preference. If the selected GPU is a +// render-only device with no displays, DXGI will add virtual outputs to the +// that device to avoid confusing applications. While this works properly for most +// applications, it breaks the Desktop Duplication API because DXGI doesn't proxy +// the virtual DXGIOutput to the real GPU it is attached to. When trying to call +// DuplicateOutput() on one of these virtual outputs, it fails with DXGI_ERROR_UNSUPPORTED +// (even if you try sneaky stuff like passing the ID3D11Device for the iGPU and the +// virtual DXGIOutput from the dGPU). Because the GPU preference is once-per-process, +// we spawn a helper tool to probe for us before we set our own GPU preference. +bool probe_for_gpu_preference(const std::string &display_name) { + // If we've already been through here, there's nothing to do this time. + static bool set_gpu_preference = false; + if(set_gpu_preference) { + return true; + } + + std::string cmd = "tools\\ddprobe.exe"; + + // We start at 1 because 0 is automatic selection which can be overridden by + // the GPU driver control panel options. Since ddprobe.exe can have different + // GPU driver overrides than Sunshine.exe, we want to avoid a scenario where + // autoselection might work for ddprobe.exe but not for us. + for(int i = 1; i < 5; i++) { + // Run the probe tool. It returns the status of DuplicateOutput(). + // + // Arg format: [GPU preference] [Display name] + HRESULT result; + try { + result = bp::system(cmd, std::to_string(i), display_name, bp::std_out > bp::null, bp::std_err > bp::null); + } + catch(bp::process_error &e) { + BOOST_LOG(error) << "Failed to start ddprobe.exe: "sv << e.what(); + return false; + } + + BOOST_LOG(info) << "ddprobe.exe ["sv << i << "] ["sv << display_name << "] returned: 0x"sv << util::hex(result).to_string_view(); + + // E_ACCESSDENIED can happen at the login screen. If we get this error, + // we know capture would have been supported, because DXGI_ERROR_UNSUPPORTED + // would have been raised first if it wasn't. + if(result == S_OK || result == E_ACCESSDENIED) { + // We found a working GPU preference, so set ourselves to use that. + if(set_gpu_preference_on_self(i)) { + set_gpu_preference = true; + return true; + } + else { + return false; + } + } + else { + // This configuration didn't work, so continue testing others + continue; + } + } + + // If none of the manual options worked, leave the GPU preference alone + return false; +} + +bool test_dxgi_duplication(adapter_t &adapter, output_t &output) { + D3D_FEATURE_LEVEL featureLevels[] { + D3D_FEATURE_LEVEL_11_1, + D3D_FEATURE_LEVEL_11_0, + D3D_FEATURE_LEVEL_10_1, + D3D_FEATURE_LEVEL_10_0, + D3D_FEATURE_LEVEL_9_3, + D3D_FEATURE_LEVEL_9_2, + D3D_FEATURE_LEVEL_9_1 + }; + + device_t device; + auto status = D3D11CreateDevice( + adapter.get(), + D3D_DRIVER_TYPE_UNKNOWN, + nullptr, + D3D11_CREATE_DEVICE_FLAGS, + featureLevels, sizeof(featureLevels) / sizeof(D3D_FEATURE_LEVEL), + D3D11_SDK_VERSION, + &device, + nullptr, + nullptr); + if(FAILED(status)) { + BOOST_LOG(error) << "Failed to create D3D11 device for DD test [0x"sv << util::hex(status).to_string_view() << ']'; + return false; + } + + output1_t output1; + status = output->QueryInterface(IID_IDXGIOutput1, (void **)&output1); + if(FAILED(status)) { + BOOST_LOG(error) << "Failed to query IDXGIOutput1 from the output"sv; + return false; + } + + // Check if we can use the Desktop Duplication API on this output + for(int x = 0; x < 2; ++x) { + dup_t dup; + status = output1->DuplicateOutput((IUnknown *)device.get(), &dup); + if(SUCCEEDED(status)) { + return true; + } + Sleep(200); + } + + BOOST_LOG(error) << "DuplicateOutput() test failed [0x"sv << util::hex(status).to_string_view() << ']'; + return false; +} + +int display_base_t::init(const ::video::config_t &config, const std::string &display_name) { std::once_flag windows_cpp_once_flag; std::call_once(windows_cpp_once_flag, []() { @@ -99,7 +303,7 @@ int display_base_t::init(int framerate, const std::string &display_name) { // Ensure we can duplicate the current display syncThreadDesktop(); - delay = std::chrono::nanoseconds { 1s } / framerate; + delay = std::chrono::nanoseconds { 1s } / config.framerate; // Get rectangle of full desktop for absolute mouse coordinates env_width = GetSystemMetrics(SM_CXVIRTUALSCREEN); @@ -107,6 +311,11 @@ int display_base_t::init(int framerate, const std::string &display_name) { HRESULT status; + // We must set the GPU preference before calling any DXGI APIs! + if(!probe_for_gpu_preference(display_name)) { + BOOST_LOG(warning) << "Failed to set GPU preference. Capture may not work!"sv; + } + status = CreateDXGIFactory1(IID_IDXGIFactory1, (void **)&factory); if(FAILED(status)) { BOOST_LOG(error) << "Failed to create DXGIFactory1 [0x"sv << util::hex(status).to_string_view() << ']'; @@ -140,7 +349,7 @@ int display_base_t::init(int framerate, const std::string &display_name) { continue; } - if(desc.AttachedToDesktop) { + if(desc.AttachedToDesktop && test_dxgi_duplication(adapter_tmp, output_tmp)) { output = std::move(output_tmp); offset_x = desc.DesktopCoordinates.left; @@ -219,7 +428,7 @@ int display_base_t::init(int framerate, const std::string &display_name) { << "Virtual Desktop : "sv << env_width << 'x' << env_height; // Enable DwmFlush() only if the current refresh rate can match the client framerate. - auto refresh_rate = framerate; + auto refresh_rate = config.framerate; DWM_TIMING_INFO timing_info; timing_info.cbSize = sizeof(timing_info); @@ -231,7 +440,7 @@ int display_base_t::init(int framerate, const std::string &display_name) { refresh_rate = std::round((double)timing_info.rateRefresh.uiNumerator / (double)timing_info.rateRefresh.uiDenominator); } - dup.use_dwmflush = config::video.dwmflush && !(framerate > refresh_rate) ? true : false; + dup.use_dwmflush = config::video.dwmflush && !(config.framerate > refresh_rate) ? true : false; // Bump up thread priority { @@ -300,7 +509,7 @@ int display_base_t::init(int framerate, const std::string &display_name) { status = output->QueryInterface(IID_IDXGIOutput5, (void **)&output5); if(SUCCEEDED(status)) { // Ask the display implementation which formats it supports - auto supported_formats = get_supported_sdr_capture_formats(); + auto supported_formats = config.dynamicRange ? get_supported_hdr_capture_formats() : get_supported_sdr_capture_formats(); if(supported_formats.empty()) { BOOST_LOG(warning) << "No compatible capture formats for this encoder"sv; return -1; @@ -354,12 +563,99 @@ int display_base_t::init(int framerate, const std::string &display_name) { 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) << ']'; + dxgi::output6_t output6 {}; + status = output->QueryInterface(IID_IDXGIOutput6, (void **)&output6); + if(SUCCEEDED(status)) { + DXGI_OUTPUT_DESC1 desc1; + output6->GetDesc1(&desc1); + + BOOST_LOG(info) + << std::endl + << "Colorspace : "sv << colorspace_to_string(desc1.ColorSpace) << std::endl + << "Bits Per Color : "sv << desc1.BitsPerColor << std::endl + << "Red Primary : ["sv << desc1.RedPrimary[0] << ',' << desc1.RedPrimary[1] << ']' << std::endl + << "Green Primary : ["sv << desc1.GreenPrimary[0] << ',' << desc1.GreenPrimary[1] << ']' << std::endl + << "Blue Primary : ["sv << desc1.BluePrimary[0] << ',' << desc1.BluePrimary[1] << ']' << std::endl + << "White Point : ["sv << desc1.WhitePoint[0] << ',' << desc1.WhitePoint[1] << ']' << std::endl + << "Min Luminance : "sv << desc1.MinLuminance << " nits"sv << std::endl + << "Max Luminance : "sv << desc1.MaxLuminance << " nits"sv << std::endl + << "Max Full Luminance : "sv << desc1.MaxFullFrameLuminance << " nits"sv; + } + // Capture format will be determined from the first call to AcquireNextFrame() capture_format = DXGI_FORMAT_UNKNOWN; return 0; } +bool display_base_t::is_hdr() { + dxgi::output6_t output6 {}; + + auto status = output->QueryInterface(IID_IDXGIOutput6, (void **)&output6); + if(FAILED(status)) { + BOOST_LOG(warning) << "Failed to query IDXGIOutput6 from the output"sv; + return false; + } + + DXGI_OUTPUT_DESC1 desc1; + output6->GetDesc1(&desc1); + + return desc1.ColorSpace == DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020; +} + +bool display_base_t::get_hdr_metadata(SS_HDR_METADATA &metadata) { + dxgi::output6_t output6 {}; + + std::memset(&metadata, 0, sizeof(metadata)); + + auto status = output->QueryInterface(IID_IDXGIOutput6, (void **)&output6); + if(FAILED(status)) { + BOOST_LOG(warning) << "Failed to query IDXGIOutput6 from the output"sv; + return false; + } + + DXGI_OUTPUT_DESC1 desc1; + output6->GetDesc1(&desc1); + + // The primaries reported here seem to correspond to scRGB (Rec. 709) + // which we then convert to Rec 2020 in our scRGB FP16 -> PQ shader + // prior to encoding. It's not clear to me if we're supposed to report + // the primaries of the original colorspace or the one we've converted + // it to, but let's just report Rec 2020 primaries and D65 white level + // to avoid confusing clients by reporting Rec 709 primaries with a + // Rec 2020 colorspace. It seems like most clients ignore the primaries + // in the metadata anyway (luminance range is most important). + desc1.RedPrimary[0] = 0.708f; + desc1.RedPrimary[1] = 0.292f; + desc1.GreenPrimary[0] = 0.170f; + desc1.GreenPrimary[1] = 0.797f; + desc1.BluePrimary[0] = 0.131f; + desc1.BluePrimary[1] = 0.046f; + desc1.WhitePoint[0] = 0.3127f; + desc1.WhitePoint[1] = 0.3290f; + + metadata.displayPrimaries[0].x = desc1.RedPrimary[0] * 50000; + metadata.displayPrimaries[0].y = desc1.RedPrimary[1] * 50000; + metadata.displayPrimaries[1].x = desc1.GreenPrimary[0] * 50000; + metadata.displayPrimaries[1].y = desc1.GreenPrimary[1] * 50000; + metadata.displayPrimaries[2].x = desc1.BluePrimary[0] * 50000; + metadata.displayPrimaries[2].y = desc1.BluePrimary[1] * 50000; + + metadata.whitePoint.x = desc1.WhitePoint[0] * 50000; + metadata.whitePoint.y = desc1.WhitePoint[1] * 50000; + + metadata.maxDisplayLuminance = desc1.MaxLuminance; + metadata.minDisplayLuminance = desc1.MinLuminance * 10000; + + // These are content-specific metadata parameters that this interface doesn't give us + metadata.maxContentLightLevel = 0; + metadata.maxFrameAverageLightLevel = 0; + + metadata.maxFullFrameLuminance = desc1.MaxFullFrameLuminance; + + return true; +} + const char *format_str[] = { "DXGI_FORMAT_UNKNOWN", "DXGI_FORMAT_R32G32B32A32_TYPELESS", @@ -489,21 +785,58 @@ const char *display_base_t::dxgi_format_to_string(DXGI_FORMAT format) { return format_str[format]; } +const char *display_base_t::colorspace_to_string(DXGI_COLOR_SPACE_TYPE type) { + const char *type_str[] = { + "DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709", + "DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709", + "DXGI_COLOR_SPACE_RGB_STUDIO_G22_NONE_P709", + "DXGI_COLOR_SPACE_RGB_STUDIO_G22_NONE_P2020", + "DXGI_COLOR_SPACE_RESERVED", + "DXGI_COLOR_SPACE_YCBCR_FULL_G22_NONE_P709_X601", + "DXGI_COLOR_SPACE_YCBCR_STUDIO_G22_LEFT_P601", + "DXGI_COLOR_SPACE_YCBCR_FULL_G22_LEFT_P601", + "DXGI_COLOR_SPACE_YCBCR_STUDIO_G22_LEFT_P709", + "DXGI_COLOR_SPACE_YCBCR_FULL_G22_LEFT_P709", + "DXGI_COLOR_SPACE_YCBCR_STUDIO_G22_LEFT_P2020", + "DXGI_COLOR_SPACE_YCBCR_FULL_G22_LEFT_P2020", + "DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020", + "DXGI_COLOR_SPACE_YCBCR_STUDIO_G2084_LEFT_P2020", + "DXGI_COLOR_SPACE_RGB_STUDIO_G2084_NONE_P2020", + "DXGI_COLOR_SPACE_YCBCR_STUDIO_G22_TOPLEFT_P2020", + "DXGI_COLOR_SPACE_YCBCR_STUDIO_G2084_TOPLEFT_P2020", + "DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P2020", + "DXGI_COLOR_SPACE_YCBCR_STUDIO_GHLG_TOPLEFT_P2020", + "DXGI_COLOR_SPACE_YCBCR_FULL_GHLG_TOPLEFT_P2020", + "DXGI_COLOR_SPACE_RGB_STUDIO_G24_NONE_P709", + "DXGI_COLOR_SPACE_RGB_STUDIO_G24_NONE_P2020", + "DXGI_COLOR_SPACE_YCBCR_STUDIO_G24_LEFT_P709", + "DXGI_COLOR_SPACE_YCBCR_STUDIO_G24_LEFT_P2020", + "DXGI_COLOR_SPACE_YCBCR_STUDIO_G24_TOPLEFT_P2020", + }; + + if(type < ARRAYSIZE(type_str)) { + return type_str[type]; + } + else { + return "UNKNOWN"; + } +} + } // namespace platf::dxgi namespace platf { -std::shared_ptr display(mem_type_e hwdevice_type, const std::string &display_name, int framerate) { +std::shared_ptr display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config) { if(hwdevice_type == mem_type_e::dxgi) { auto disp = std::make_shared(); - if(!disp->init(framerate, display_name)) { + if(!disp->init(config, display_name)) { return disp; } } else if(hwdevice_type == mem_type_e::system) { auto disp = std::make_shared(); - if(!disp->init(framerate, display_name)) { + if(!disp->init(config, display_name)) { return disp; } } @@ -520,10 +853,15 @@ std::vector display_names(mem_type_e) { std::wstring_convert, wchar_t> converter; + // We must set the GPU preference before calling any DXGI APIs! + if(!dxgi::probe_for_gpu_preference(config::video.output_name)) { + BOOST_LOG(warning) << "Failed to set GPU preference. Capture may not work!"sv; + } + dxgi::factory1_t factory; status = CreateDXGIFactory1(IID_IDXGIFactory1, (void **)&factory); if(FAILED(status)) { - BOOST_LOG(error) << "Failed to create DXGIFactory1 [0x"sv << util::hex(status).to_string_view() << ']' << std::endl; + BOOST_LOG(error) << "Failed to create DXGIFactory1 [0x"sv << util::hex(status).to_string_view() << ']'; return {}; } @@ -562,7 +900,10 @@ std::vector display_names(mem_type_e) { << " Resolution : "sv << width << 'x' << height << std::endl << std::endl; - display_names.emplace_back(std::move(device_name)); + // Don't include the display in the list if we can't actually capture it + if(desc.AttachedToDesktop && dxgi::test_dxgi_duplication(adapter, output)) { + display_names.emplace_back(std::move(device_name)); + } } } diff --git a/src/platform/windows/display_ram.cpp b/src/platform/windows/display_ram.cpp index c5ce6dc3..c3f64bdb 100644 --- a/src/platform/windows/display_ram.cpp +++ b/src/platform/windows/display_ram.cpp @@ -165,37 +165,6 @@ void blend_cursor(const cursor_t &cursor, img_t &img) { } } -capture_e display_ram_t::capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr<::platf::img_t> img, bool *cursor) { - auto next_frame = std::chrono::steady_clock::now(); - - while(img) { - auto now = std::chrono::steady_clock::now(); - while(next_frame > now) { - now = std::chrono::steady_clock::now(); - } - next_frame = now + delay; - - auto status = snapshot(img.get(), 1000ms, *cursor); - switch(status) { - case platf::capture_e::reinit: - case platf::capture_e::error: - return status; - case platf::capture_e::timeout: - img = snapshot_cb(img, false); - std::this_thread::sleep_for(1ms); - break; - case platf::capture_e::ok: - img = snapshot_cb(img, true); - break; - default: - BOOST_LOG(error) << "Unrecognized capture status ["sv << (int)status << ']'; - return status; - } - } - - return capture_e::ok; -} - capture_e display_ram_t::snapshot(::platf::img_t *img_base, std::chrono::milliseconds timeout, bool cursor_visible) { auto img = (img_t *)img_base; @@ -380,11 +349,16 @@ int display_ram_t::dummy_img(platf::img_t *img) { } std::vector display_ram_t::get_supported_sdr_capture_formats() { - return std::vector { DXGI_FORMAT_B8G8R8A8_UNORM }; + return { DXGI_FORMAT_B8G8R8A8_UNORM }; } -int display_ram_t::init(int framerate, const std::string &display_name) { - if(display_base_t::init(framerate, display_name)) { +std::vector display_ram_t::get_supported_hdr_capture_formats() { + // HDR is unsupported + return {}; +} + +int display_ram_t::init(const ::video::config_t &config, const std::string &display_name) { + if(display_base_t::init(config, display_name)) { return -1; } diff --git a/src/platform/windows/display_vram.cpp b/src/platform/windows/display_vram.cpp index 4c0e8fa4..ab63974b 100644 --- a/src/platform/windows/display_vram.cpp +++ b/src/platform/windows/display_vram.cpp @@ -89,9 +89,12 @@ blend_t make_blend(device_t::pointer device, bool enable, bool invert) { blob_t convert_UV_vs_hlsl; blob_t convert_UV_ps_hlsl; +blob_t convert_UV_PQ_ps_hlsl; blob_t scene_vs_hlsl; blob_t convert_Y_ps_hlsl; +blob_t convert_Y_PQ_ps_hlsl; blob_t scene_ps_hlsl; +blob_t scene_NW_ps_hlsl; struct img_d3d_t : public platf::img_t { std::shared_ptr display; @@ -101,16 +104,16 @@ struct img_d3d_t : public platf::img_t { render_target_t capture_rt; keyed_mutex_t capture_mutex; - // These objects are owned by the hwdevice_t's ID3D11Device - texture2d_t encoder_texture; - shader_res_t encoder_input_res; - keyed_mutex_t encoder_mutex; - // This is the shared handle used by hwdevice_t to open capture_texture HANDLE encoder_texture_handle = {}; + // Set to true if the image corresponds to a dummy texture used prior to + // the first successful capture of a desktop frame bool dummy = false; + // Unique identifier for this image + uint32_t id = 0; + virtual ~img_d3d_t() override { if(encoder_texture_handle) { CloseHandle(encoder_texture_handle); @@ -300,62 +303,40 @@ blob_t compile_vertex_shader(LPCSTR file) { class hwdevice_t : public platf::hwdevice_t { public: int convert(platf::img_t &img_base) override { - auto &img = (img_d3d_t &)img_base; - auto back_d3d_img = (img_d3d_t *)back_img.get(); + auto &img = (img_d3d_t &)img_base; + auto &img_ctx = img_ctx_map[img.id]; // Open the shared capture texture with our ID3D11Device - if(share_img(&img_base)) { + if(initialize_image_context(img, img_ctx)) { return -1; } // Acquire encoder mutex to synchronize with capture code - auto status = img.encoder_mutex->AcquireSync(0, INFINITE); + auto status = img_ctx.encoder_mutex->AcquireSync(0, INFINITE); if(status != S_OK) { BOOST_LOG(error) << "Failed to acquire encoder mutex [0x"sv << util::hex(status).to_string_view() << ']'; return -1; } - // Even though this image will never have racing updates, we must acquire the - // keyed mutex for PSSetShaderResources() to succeed. - status = back_d3d_img->encoder_mutex->AcquireSync(0, INFINITE); - if(status != S_OK) { - img.encoder_mutex->ReleaseSync(0); - BOOST_LOG(error) << "Failed to acquire back_d3d_img mutex [0x"sv << util::hex(status).to_string_view() << ']'; - return -1; - } - - device_ctx->IASetInputLayout(input_layout.get()); - - _init_view_port(this->img.width, this->img.height); device_ctx->OMSetRenderTargets(1, &nv12_Y_rt, nullptr); device_ctx->VSSetShader(scene_vs.get(), nullptr, 0); device_ctx->PSSetShader(convert_Y_ps.get(), nullptr, 0); - device_ctx->PSSetShaderResources(0, 1, &back_d3d_img->encoder_input_res); - device_ctx->Draw(3, 0); - device_ctx->RSSetViewports(1, &outY_view); - device_ctx->PSSetShaderResources(0, 1, &img.encoder_input_res); + device_ctx->PSSetShaderResources(0, 1, &img_ctx.encoder_input_res); device_ctx->Draw(3, 0); // Artifacts start appearing on the rendered image if Sunshine doesn't flush // before rendering on the UV part of the image. device_ctx->Flush(); - _init_view_port(this->img.width / 2, this->img.height / 2); device_ctx->OMSetRenderTargets(1, &nv12_UV_rt, nullptr); device_ctx->VSSetShader(convert_UV_vs.get(), nullptr, 0); device_ctx->PSSetShader(convert_UV_ps.get(), nullptr, 0); - device_ctx->PSSetShaderResources(0, 1, &back_d3d_img->encoder_input_res); - device_ctx->Draw(3, 0); - device_ctx->RSSetViewports(1, &outUV_view); - device_ctx->PSSetShaderResources(0, 1, &img.encoder_input_res); device_ctx->Draw(3, 0); - device_ctx->Flush(); - // Release encoder mutexes to allow capture code to reuse this image - back_d3d_img->encoder_mutex->ReleaseSync(0); - img.encoder_mutex->ReleaseSync(0); + // Release encoder mutex to allow capture code to reuse this image + img_ctx.encoder_mutex->ReleaseSync(0); return 0; } @@ -392,17 +373,80 @@ public: this->color_matrix = std::move(color_matrix); } - int set_frame(AVFrame *frame) { + 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]; + } + auto out_width = frame->width; auto out_height = frame->height; - float in_width = img.display->width; - float in_height = img.display->height; + float in_width = display->width; + float in_height = display->height; - // // Ensure aspect ratio is maintained + // Ensure aspect ratio is maintained auto scalar = std::fminf(out_width / in_width, out_height / in_height); auto out_width_f = in_width * scalar; auto out_height_f = in_height * scalar; @@ -414,27 +458,9 @@ public: 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 }; - D3D11_TEXTURE2D_DESC t {}; - t.Width = out_width; - t.Height = out_height; - t.MipLevels = 1; - t.ArraySize = 1; - t.SampleDesc.Count = 1; - t.Usage = D3D11_USAGE_DEFAULT; - t.Format = format; - t.BindFlags = D3D11_BIND_RENDER_TARGET; - - auto status = device->CreateTexture2D(&t, nullptr, &img.encoder_texture); - if(FAILED(status)) { - BOOST_LOG(error) << "Failed to create render target texture [0x"sv << util::hex(status).to_string_view() << ']'; - return -1; - } - - img.width = out_width; - img.height = out_height; - img.data = (std::uint8_t *)img.encoder_texture.get(); - img.row_pitch = out_width * 4; - img.pixel_pitch = 4; + // The underlying frame pool owns the texture, so we must reference it for ourselves + frame_texture->AddRef(); + hwframe_texture.reset(frame_texture); float info_in[16 / sizeof(float)] { 1.0f / (float)out_width_f }; //aligned to 16-byte info_scene = make_buffer(device.get(), info_in); @@ -449,7 +475,7 @@ public: D3D11_RTV_DIMENSION_TEXTURE2D }; - status = device->CreateRenderTargetView(img.encoder_texture.get(), &nv12_rt_desc, &nv12_Y_rt); + auto status = device->CreateRenderTargetView(hwframe_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; @@ -457,28 +483,17 @@ public: nv12_rt_desc.Format = (format == DXGI_FORMAT_P010) ? DXGI_FORMAT_R16G16_UNORM : DXGI_FORMAT_R8G8_UNORM; - status = device->CreateRenderTargetView(img.encoder_texture.get(), &nv12_rt_desc, &nv12_UV_rt); + status = device->CreateRenderTargetView(hwframe_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; } - // Need to have something refcounted - if(!frame->buf[0]) { - frame->buf[0] = av_buffer_allocz(sizeof(AVD3D11FrameDescriptor)); - } - - auto desc = (AVD3D11FrameDescriptor *)frame->buf[0]->data; - desc->texture = (ID3D11Texture2D *)img.data; - desc->index = 0; - - frame->data[0] = img.data; - frame->data[1] = 0; - - frame->linesize[0] = img.row_pitch; - - frame->height = img.height; - frame->width = img.width; + // Clear the RTVs to ensure the aspect ratio padding is black + const float y_black[] = { 0.0f, 0.0f, 0.0f, 0.0f }; + device_ctx->ClearRenderTargetView(nv12_Y_rt.get(), y_black); + const float uv_black[] = { 0.5f, 0.5f, 0.5f, 0.5f }; + device_ctx->ClearRenderTargetView(nv12_UV_rt.get(), uv_black); return 0; } @@ -534,28 +549,39 @@ public: return -1; } - status = device->CreatePixelShader(convert_Y_ps_hlsl->GetBufferPointer(), convert_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); - if(status) { - BOOST_LOG(error) << "Failed to create convertUV pixel 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); if(status) { BOOST_LOG(error) << "Failed to create convertUV vertex shader [0x"sv << util::hex(status).to_string_view() << ']'; return -1; } - status = device->CreatePixelShader(scene_ps_hlsl->GetBufferPointer(), scene_ps_hlsl->GetBufferSize(), nullptr, &scene_ps); - if(status) { - BOOST_LOG(error) << "Failed to create scene pixel shader [0x"sv << util::hex(status).to_string_view() << ']'; - return -1; + // If the display is in HDR and we're streaming HDR, we'll be converting scRGB to SMPTE 2084 PQ. + // NB: We can consume scRGB in SDR with our regular shaders because it behaves like UNORM input. + 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_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_ps); + if(status) { + BOOST_LOG(error) << "Failed to create convertUV pixel shader [0x"sv << util::hex(status).to_string_view() << ']'; + return -1; + } + } + else { + status = device->CreatePixelShader(convert_Y_ps_hlsl->GetBufferPointer(), convert_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); + 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]); @@ -573,15 +599,7 @@ public: convert_UV_vs_hlsl->GetBufferPointer(), convert_UV_vs_hlsl->GetBufferSize(), &input_layout); - img.display = std::move(display); - - // Color the background black, so that the padding for keeping the aspect ratio - // is black - back_img = img.display->alloc_img(); - if(img.display->dummy_img(back_img.get()) || share_img(back_img.get())) { - BOOST_LOG(warning) << "Couldn't create an image to set background color to black"sv; - return -1; - } + this->display = std::move(display); blend_disable = make_blend(device.get(), false, false); if(!blend_disable) { @@ -615,28 +633,33 @@ public: } private: - void _init_view_port(float x, float y, float width, float height) { - D3D11_VIEWPORT view { - x, y, - width, height, - 0.0f, 1.0f - }; + struct encoder_img_ctx_t { + // Used to determine if the underlying texture changes. + // Not safe for actual use by the encoder! + texture2d_t::pointer capture_texture_p; - device_ctx->RSSetViewports(1, &view); - } + texture2d_t encoder_texture; + shader_res_t encoder_input_res; + keyed_mutex_t encoder_mutex; - void _init_view_port(float width, float height) { - _init_view_port(0.0f, 0.0f, width, height); - } - - int share_img(platf::img_t *img_base) { - auto img = (img_d3d_t *)img_base; + void reset() { + capture_texture_p = nullptr; + encoder_texture.reset(); + encoder_input_res.reset(); + encoder_mutex.reset(); + } + }; + int initialize_image_context(const img_d3d_t &img, encoder_img_ctx_t &img_ctx) { // If we've already opened the shared texture, we're done - if(img->encoder_texture) { + if(img_ctx.encoder_texture && img.capture_texture.get() == img_ctx.capture_texture_p) { return 0; } + // Reset this image context in case it was used before with a different texture. + // Textures can change when transitioning from a dummy image to a real image. + img_ctx.reset(); + device1_t device1; auto status = device->QueryInterface(__uuidof(ID3D11Device1), (void **)&device1); if(FAILED(status)) { @@ -645,26 +668,27 @@ private: } // Open a handle to the shared texture - status = device1->OpenSharedResource1(img->encoder_texture_handle, __uuidof(ID3D11Texture2D), (void **)&img->encoder_texture); + status = device1->OpenSharedResource1(img.encoder_texture_handle, __uuidof(ID3D11Texture2D), (void **)&img_ctx.encoder_texture); if(FAILED(status)) { BOOST_LOG(error) << "Failed to open shared image texture [0x"sv << util::hex(status).to_string_view() << ']'; return -1; } // Get the keyed mutex to synchronize with the capture code - status = img->encoder_texture->QueryInterface(__uuidof(IDXGIKeyedMutex), (void **)&img->encoder_mutex); + status = img_ctx.encoder_texture->QueryInterface(__uuidof(IDXGIKeyedMutex), (void **)&img_ctx.encoder_mutex); if(FAILED(status)) { BOOST_LOG(error) << "Failed to query IDXGIKeyedMutex [0x"sv << util::hex(status).to_string_view() << ']'; return -1; } // Create the SRV for the encoder texture - status = device->CreateShaderResourceView(img->encoder_texture.get(), nullptr, &img->encoder_input_res); + status = device->CreateShaderResourceView(img_ctx.encoder_texture.get(), nullptr, &img_ctx.encoder_input_res); if(FAILED(status)) { BOOST_LOG(error) << "Failed to create shader resource view for encoding [0x"sv << util::hex(status).to_string_view() << ']'; return -1; } + img_ctx.capture_texture_p = img.capture_texture.get(); return 0; } @@ -685,16 +709,19 @@ public: render_target_t nv12_UV_rt; // The image referenced by hwframe - // The resulting image is stored here. - img_d3d_t img; + texture2d_t hwframe_texture; - // Clear nv12 render target to black - std::shared_ptr back_img; + // 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; vs_t convert_UV_vs; ps_t convert_UV_ps; ps_t convert_Y_ps; - ps_t scene_ps; vs_t scene_vs; D3D11_VIEWPORT outY_view; @@ -706,37 +733,6 @@ public: device_ctx_t device_ctx; }; -capture_e display_vram_t::capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr<::platf::img_t> img, bool *cursor) { - auto next_frame = std::chrono::steady_clock::now(); - - while(img) { - auto now = std::chrono::steady_clock::now(); - while(next_frame > now) { - now = std::chrono::steady_clock::now(); - } - next_frame = now + delay; - - auto status = snapshot(img.get(), 1000ms, *cursor); - switch(status) { - case platf::capture_e::reinit: - case platf::capture_e::error: - return status; - case platf::capture_e::timeout: - img = snapshot_cb(img, false); - std::this_thread::sleep_for(1ms); - break; - case platf::capture_e::ok: - img = snapshot_cb(img, true); - break; - default: - BOOST_LOG(error) << "Unrecognized capture status ["sv << (int)status << ']'; - return status; - } - } - - return capture_e::ok; -} - bool set_cursor_texture(device_t::pointer device, gpu_cursor_t &cursor, util::buffer_t &&cursor_img, DXGI_OUTDUPL_POINTER_SHAPE_INFO &shape_info) { // This cursor image may not be used if(cursor_img.size() == 0) { @@ -969,8 +965,8 @@ capture_e display_vram_t::snapshot(platf::img_t *img_base, std::chrono::millisec return capture_e::ok; } -int display_vram_t::init(int framerate, const std::string &display_name) { - if(display_base_t::init(framerate, display_name)) { +int display_vram_t::init(const ::video::config_t &config, const std::string &display_name) { + if(display_base_t::init(config, display_name)) { return -1; } @@ -995,10 +991,32 @@ int display_vram_t::init(int framerate, const std::string &display_name) { return -1; } - status = device->CreatePixelShader(scene_ps_hlsl->GetBufferPointer(), scene_ps_hlsl->GetBufferSize(), nullptr, &scene_ps); - if(status) { - BOOST_LOG(error) << "Failed to create scene pixel shader [0x"sv << util::hex(status).to_string_view() << ']'; - return -1; + 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); + if(status) { + BOOST_LOG(error) << "Failed to create scene 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; + return -1; + } + + device_ctx->PSSetConstantBuffers(0, 1, &sdr_multiplier); + } + else { + status = device->CreatePixelShader(scene_ps_hlsl->GetBufferPointer(), scene_ps_hlsl->GetBufferSize(), nullptr, &scene_ps); + if(status) { + BOOST_LOG(error) << "Failed to create scene pixel shader [0x"sv << util::hex(status).to_string_view() << ']'; + return -1; + } } blend_alpha = make_blend(device.get(), true, false); @@ -1023,6 +1041,7 @@ std::shared_ptr display_vram_t::alloc_img() { img->width = width; img->height = height; img->display = shared_from_this(); + img->id = next_image_id++; return img; } @@ -1046,9 +1065,6 @@ int display_vram_t::complete_img(platf::img_t *img_base, bool dummy) { img->capture_texture.reset(); img->capture_rt.reset(); img->capture_mutex.reset(); - img->encoder_texture.reset(); - img->encoder_input_res.reset(); - img->encoder_mutex.reset(); img->data = nullptr; if(img->encoder_texture_handle) { CloseHandle(img->encoder_texture_handle); @@ -1123,7 +1139,29 @@ int display_vram_t::dummy_img(platf::img_t *img_base) { } std::vector display_vram_t::get_supported_sdr_capture_formats() { - return std::vector { DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_R8G8B8A8_UNORM }; + return { DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_R8G8B8A8_UNORM }; +} + +std::vector display_vram_t::get_supported_hdr_capture_formats() { + return { + // scRGB FP16 is the desired format for HDR content. This will also handle + // 10-bit SDR displays with the increased precision of FP16 vs 8-bit UNORMs. + DXGI_FORMAT_R16G16B16A16_FLOAT, + + // DXGI_FORMAT_R10G10B10A2_UNORM seems like it might give us frames already + // converted to SMPTE 2084 PQ, however it seems to actually just clamp the + // scRGB FP16 values that DWM is using when the desktop format is scRGB FP16. + // + // If there is a case where the desktop format is really SMPTE 2084 PQ, it + // might make sense to support capturing it without conversion to scRGB, + // but we avoid it for now. + + // We include the 8-bit modes too for when the display is in SDR mode, + // while the client stream is HDR-capable. These UNORM formats behave + // like a degenerate case of scRGB FP16 with values between 0.0f-1.0f. + DXGI_FORMAT_B8G8R8A8_UNORM, + DXGI_FORMAT_R8G8B8A8_UNORM, + }; } std::shared_ptr display_vram_t::make_hwdevice(pix_fmt_e pix_fmt) { @@ -1159,11 +1197,21 @@ int init() { return -1; } + convert_Y_PQ_ps_hlsl = compile_pixel_shader(SUNSHINE_SHADERS_DIR "/ConvertYPS_PQ.hlsl"); + if(!convert_Y_PQ_ps_hlsl) { + return -1; + } + convert_UV_ps_hlsl = compile_pixel_shader(SUNSHINE_SHADERS_DIR "/ConvertUVPS.hlsl"); if(!convert_UV_ps_hlsl) { return -1; } + convert_UV_PQ_ps_hlsl = compile_pixel_shader(SUNSHINE_SHADERS_DIR "/ConvertUVPS_PQ.hlsl"); + if(!convert_UV_PQ_ps_hlsl) { + return -1; + } + convert_UV_vs_hlsl = compile_vertex_shader(SUNSHINE_SHADERS_DIR "/ConvertUVVS.hlsl"); if(!convert_UV_vs_hlsl) { return -1; @@ -1173,6 +1221,11 @@ int init() { if(!scene_ps_hlsl) { return -1; } + + 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; return 0; diff --git a/src/platform/windows/input.cpp b/src/platform/windows/input.cpp index 6b737b0f..483bfd81 100644 --- a/src/platform/windows/input.cpp +++ b/src/platform/windows/input.cpp @@ -303,6 +303,18 @@ void scroll(input_t &input, int distance) { send_input(i); } +void hscroll(input_t &input, int distance) { + INPUT i {}; + + i.type = INPUT_MOUSE; + auto &mi = i.mi; + + mi.dwFlags = MOUSEEVENTF_HWHEEL; + mi.mouseData = distance; + + send_input(i); +} + void keyboard(input_t &input, uint16_t modcode, bool release) { auto raw = (input_raw_t *)input.get(); diff --git a/src/platform/windows/misc.cpp b/src/platform/windows/misc.cpp index 9b6867da..c89b0f2c 100644 --- a/src/platform/windows/misc.cpp +++ b/src/platform/windows/misc.cpp @@ -4,6 +4,7 @@ #include #include +#include #include // prevent clang format from "optimizing" the header include order @@ -16,12 +17,27 @@ #include #include #include +#include // clang-format on #include "src/main.h" #include "src/platform/common.h" #include "src/utility.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 +#include + +#ifndef WLAN_API_MAKE_VERSION +#define WLAN_API_MAKE_VERSION(_major, _minor) (((DWORD)(_minor)) << 16 | (_major)) +#endif + namespace bp = boost::process; using namespace std::literals; @@ -31,6 +47,20 @@ using adapteraddrs_t = util::c_ptr; bool enabled_mouse_keys = false; MOUSEKEYS previous_mouse_keys_state; +HANDLE qos_handle = nullptr; + +decltype(QOSCreateHandle) *fn_QOSCreateHandle = nullptr; +decltype(QOSAddSocketToFlow) *fn_QOSAddSocketToFlow = nullptr; +decltype(QOSRemoveSocketFromFlow) *fn_QOSRemoveSocketFromFlow = nullptr; + +HANDLE wlan_handle = nullptr; + +decltype(WlanOpenHandle) *fn_WlanOpenHandle = nullptr; +decltype(WlanCloseHandle) *fn_WlanCloseHandle = nullptr; +decltype(WlanFreeMemory) *fn_WlanFreeMemory = nullptr; +decltype(WlanEnumInterfaces) *fn_WlanEnumInterfaces = nullptr; +decltype(WlanSetInterface) *fn_WlanSetInterface = nullptr; + std::filesystem::path appdata() { WCHAR sunshine_path[MAX_PATH]; GetModuleFileNameW(NULL, sunshine_path, _countof(sunshine_path)); @@ -345,7 +375,7 @@ void free_proc_thread_attr_list(LPPROC_THREAD_ATTRIBUTE_LIST list) { HeapFree(GetProcessHeap(), 0, list); } -bp::child run_unprivileged(const std::string &cmd, boost::filesystem::path &working_dir, bp::environment &env, FILE *file, std::error_code &ec) { +bp::child run_unprivileged(const std::string &cmd, boost::filesystem::path &working_dir, bp::environment &env, FILE *file, std::error_code &ec, bp::group *group) { HANDLE shell_token = duplicate_shell_token(); if(!shell_token) { // This can happen if the shell has crashed. Fail the launch rather than risking launching with @@ -460,6 +490,9 @@ bp::child run_unprivileged(const std::string &cmd, boost::filesystem::path &work // Since we are always spawning a process with a less privileged token than ourselves, // bp::child() should have no problem opening it with any access rights it wants. auto child = bp::child((bp::pid_t)process_info.dwProcessId); + if(group) { + group->add(child); + } // Only close handles after bp::child() has opened the process. If the process terminates // quickly, the PID could be reused if we close the process handle. @@ -507,6 +540,35 @@ void adjust_thread_priority(thread_priority_e priority) { } void streaming_will_start() { + static std::once_flag load_wlanapi_once_flag; + std::call_once(load_wlanapi_once_flag, []() { + // wlanapi.dll is not installed by default on Windows Server, so we load it dynamically + HMODULE wlanapi = LoadLibraryExA("wlanapi.dll", NULL, LOAD_LIBRARY_SEARCH_SYSTEM32); + if(!wlanapi) { + BOOST_LOG(debug) << "wlanapi.dll is not available on this OS"sv; + return; + } + + fn_WlanOpenHandle = (decltype(fn_WlanOpenHandle))GetProcAddress(wlanapi, "WlanOpenHandle"); + fn_WlanCloseHandle = (decltype(fn_WlanCloseHandle))GetProcAddress(wlanapi, "WlanCloseHandle"); + fn_WlanFreeMemory = (decltype(fn_WlanFreeMemory))GetProcAddress(wlanapi, "WlanFreeMemory"); + fn_WlanEnumInterfaces = (decltype(fn_WlanEnumInterfaces))GetProcAddress(wlanapi, "WlanEnumInterfaces"); + fn_WlanSetInterface = (decltype(fn_WlanSetInterface))GetProcAddress(wlanapi, "WlanSetInterface"); + + if(!fn_WlanOpenHandle || !fn_WlanCloseHandle || !fn_WlanFreeMemory || !fn_WlanEnumInterfaces || !fn_WlanSetInterface) { + BOOST_LOG(error) << "wlanapi.dll is missing exports?"sv; + + fn_WlanOpenHandle = nullptr; + fn_WlanCloseHandle = nullptr; + fn_WlanFreeMemory = nullptr; + fn_WlanEnumInterfaces = nullptr; + fn_WlanSetInterface = nullptr; + + FreeLibrary(wlanapi); + return; + } + }); + // Enable MMCSS scheduling for DWM DwmEnableMMCSS(true); @@ -516,6 +578,39 @@ void streaming_will_start() { // Promote ourselves to high priority class SetPriorityClass(GetCurrentProcess(), HIGH_PRIORITY_CLASS); + // Enable low latency mode on all connected WLAN NICs if wlanapi.dll is available + if(fn_WlanOpenHandle) { + DWORD negotiated_version; + + if(fn_WlanOpenHandle(WLAN_API_MAKE_VERSION(2, 0), nullptr, &negotiated_version, &wlan_handle) == ERROR_SUCCESS) { + PWLAN_INTERFACE_INFO_LIST wlan_interface_list; + + if(fn_WlanEnumInterfaces(wlan_handle, nullptr, &wlan_interface_list) == ERROR_SUCCESS) { + 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. + // + // 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 + BOOL value = TRUE; + auto error = fn_WlanSetInterface(wlan_handle, &wlan_interface_list->InterfaceInfo[i].InterfaceGuid, + wlan_intf_opcode_media_streaming_mode, sizeof(value), &value, nullptr); + if(error == ERROR_SUCCESS) { + BOOST_LOG(info) << "WLAN interface "sv << i << " is now in low latency mode"sv; + } + } + } + + fn_WlanFreeMemory(wlan_interface_list); + } + else { + fn_WlanCloseHandle(wlan_handle, nullptr); + wlan_handle = NULL; + } + } + } + // If there is no mouse connected, enable Mouse Keys to force the cursor to appear if(!GetSystemMetrics(SM_MOUSEPRESENT)) { BOOST_LOG(info) << "A mouse was not detected. Sunshine will enable Mouse Keys while streaming to force the mouse cursor to appear."; @@ -556,6 +651,12 @@ void streaming_will_stop() { // Disable MMCSS scheduling for DWM DwmEnableMMCSS(false); + // Closing our WLAN client handle will undo our optimizations + if(wlan_handle != nullptr) { + fn_WlanCloseHandle(wlan_handle, nullptr); + wlan_handle = nullptr; + } + // Restore Mouse Keys back to the previous settings if we turned it on if(enabled_mouse_keys) { enabled_mouse_keys = false; @@ -578,4 +679,166 @@ bool restart() { return true; } +SOCKADDR_IN to_sockaddr(boost::asio::ip::address_v4 address, uint16_t port) { + 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; +} + +SOCKADDR_IN6 to_sockaddr(boost::asio::ip::address_v6 address, uint16_t port) { + 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; +} + +// Use UDP segmentation offload if it is supported by the OS. If the NIC is capable, this will use +// hardware acceleration to reduce CPU usage. Support for USO was introduced in Windows 10 20H1. +bool send_batch(batched_send_info_t &send_info) { + WSAMSG msg; + + // Convert the target address into a SOCKADDR + SOCKADDR_IN saddr_v4; + SOCKADDR_IN6 saddr_v6; + if(send_info.target_address.is_v6()) { + saddr_v6 = to_sockaddr(send_info.target_address.to_v6(), send_info.target_port); + + msg.name = (PSOCKADDR)&saddr_v6; + msg.namelen = sizeof(saddr_v6); + } + else { + saddr_v4 = to_sockaddr(send_info.target_address.to_v4(), send_info.target_port); + + msg.name = (PSOCKADDR)&saddr_v4; + msg.namelen = sizeof(saddr_v4); + } + + WSABUF buf; + buf.buf = (char *)send_info.buffer; + buf.len = send_info.block_size * send_info.block_count; + + msg.lpBuffers = &buf; + msg.dwBufferCount = 1; + msg.dwFlags = 0; + + char cmbuf[WSA_CMSG_SPACE(sizeof(DWORD))]; + msg.Control.buf = cmbuf; + msg.Control.len = 0; + + if(send_info.block_count > 1) { + msg.Control.len += WSA_CMSG_SPACE(sizeof(DWORD)); + + auto cm = WSA_CMSG_FIRSTHDR(&msg); + 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; + } + + // 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; +} + +class qos_t : public deinit_t { +public: + qos_t(QOS_FLOWID flow_id) : flow_id(flow_id) {} + + virtual ~qos_t() { + if(!fn_QOSRemoveSocketFromFlow(qos_handle, (SOCKET)NULL, flow_id, 0)) { + auto winerr = GetLastError(); + BOOST_LOG(warning) << "QOSRemoveSocketFromFlow() failed: "sv << winerr; + } + } + +private: + QOS_FLOWID flow_id; +}; + +std::unique_ptr enable_socket_qos(uintptr_t native_socket, boost::asio::ip::address &address, uint16_t port, qos_data_type_e data_type) { + SOCKADDR_IN saddr_v4; + SOCKADDR_IN6 saddr_v6; + PSOCKADDR dest_addr; + + static std::once_flag load_qwave_once_flag; + std::call_once(load_qwave_once_flag, []() { + // qWAVE is not installed by default on Windows Server, so we load it dynamically + HMODULE qwave = LoadLibraryExA("qwave.dll", NULL, LOAD_LIBRARY_SEARCH_SYSTEM32); + if(!qwave) { + BOOST_LOG(debug) << "qwave.dll is not available on this OS"sv; + return; + } + + fn_QOSCreateHandle = (decltype(fn_QOSCreateHandle))GetProcAddress(qwave, "QOSCreateHandle"); + fn_QOSAddSocketToFlow = (decltype(fn_QOSAddSocketToFlow))GetProcAddress(qwave, "QOSAddSocketToFlow"); + fn_QOSRemoveSocketFromFlow = (decltype(fn_QOSRemoveSocketFromFlow))GetProcAddress(qwave, "QOSRemoveSocketFromFlow"); + + if(!fn_QOSCreateHandle || !fn_QOSAddSocketToFlow || !fn_QOSRemoveSocketFromFlow) { + BOOST_LOG(error) << "qwave.dll is missing exports?"sv; + + fn_QOSCreateHandle = nullptr; + fn_QOSAddSocketToFlow = nullptr; + fn_QOSRemoveSocketFromFlow = nullptr; + + FreeLibrary(qwave); + return; + } + + QOS_VERSION qos_version { 1, 0 }; + if(!fn_QOSCreateHandle(&qos_version, &qos_handle)) { + auto winerr = GetLastError(); + BOOST_LOG(warning) << "QOSCreateHandle() failed: "sv << winerr; + return; + } + }); + + // If qWAVE is unavailable, just return + if(!fn_QOSAddSocketToFlow || !qos_handle) { + return nullptr; + } + + if(address.is_v6()) { + saddr_v6 = to_sockaddr(address.to_v6(), port); + dest_addr = (PSOCKADDR)&saddr_v6; + } + else { + saddr_v4 = to_sockaddr(address.to_v4(), port); + dest_addr = (PSOCKADDR)&saddr_v4; + } + + QOS_TRAFFIC_TYPE traffic_type; + switch(data_type) { + case qos_data_type_e::audio: + traffic_type = QOSTrafficTypeVoice; + break; + case qos_data_type_e::video: + traffic_type = QOSTrafficTypeAudioVideo; + break; + default: + BOOST_LOG(error) << "Unknown traffic type: "sv << (int)data_type; + return nullptr; + } + + QOS_FLOWID flow_id = 0; + if(!fn_QOSAddSocketToFlow(qos_handle, (SOCKET)native_socket, dest_addr, traffic_type, QOS_NON_ADAPTIVE_FLOW, &flow_id)) { + auto winerr = GetLastError(); + BOOST_LOG(warning) << "QOSAddSocketToFlow() failed: "sv << winerr; + return nullptr; + } + + return std::make_unique(flow_id); +} + } // namespace platf \ No newline at end of file diff --git a/src/process.cpp b/src/process.cpp index 21e633ae..3d3bd3e7 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -150,7 +150,7 @@ int proc_t::execute(int app_id) { find_working_directory(cmd, _env) : boost::filesystem::path(proc.working_dir); BOOST_LOG(info) << "Spawning ["sv << cmd << "] in ["sv << working_dir << ']'; - auto child = platf::run_unprivileged(cmd, working_dir, _env, _pipe.get(), ec); + auto child = platf::run_unprivileged(cmd, working_dir, _env, _pipe.get(), ec, nullptr); if(ec) { BOOST_LOG(warning) << "Couldn't spawn ["sv << cmd << "]: System: "sv << ec.message(); } @@ -168,13 +168,11 @@ int proc_t::execute(int app_id) { find_working_directory(proc.cmd, _env) : boost::filesystem::path(proc.working_dir); BOOST_LOG(info) << "Executing: ["sv << proc.cmd << "] in ["sv << working_dir << ']'; - _process = platf::run_unprivileged(proc.cmd, working_dir, _env, _pipe.get(), ec); + _process = platf::run_unprivileged(proc.cmd, working_dir, _env, _pipe.get(), ec, &_process_handle); if(ec) { BOOST_LOG(warning) << "Couldn't run ["sv << proc.cmd << "]: System: "sv << ec.message(); return -1; } - - _process_handle.add(_process); } fg.disable(); diff --git a/src/rtsp.cpp b/src/rtsp.cpp index 9f2a7bbd..791d6888 100644 --- a/src/rtsp.cpp +++ b/src/rtsp.cpp @@ -613,6 +613,8 @@ void cmd_announce(rtsp_server_t *server, tcp::socket &sock, msg_t &&req) { args.try_emplace("x-nv-general.useReliableUdp"sv, "1"sv); args.try_emplace("x-nv-vqos[0].fec.minRequiredFecPackets"sv, "0"sv); args.try_emplace("x-nv-general.featureFlags"sv, "135"sv); + args.try_emplace("x-nv-vqos[0].qosTrafficType"sv, "5"sv); + args.try_emplace("x-nv-aqos.qosTrafficType"sv, "4"sv); config_t config; @@ -629,6 +631,8 @@ void cmd_announce(rtsp_server_t *server, tcp::socket &sock, msg_t &&req) { config.packetsize = util::from_view(args.at("x-nv-video[0].packetSize"sv)); config.minRequiredFecPackets = util::from_view(args.at("x-nv-vqos[0].fec.minRequiredFecPackets"sv)); config.featureFlags = util::from_view(args.at("x-nv-general.featureFlags"sv)); + config.audioQosType = util::from_view(args.at("x-nv-aqos.qosTrafficType"sv)); + config.videoQosType = util::from_view(args.at("x-nv-vqos[0].qosTrafficType"sv)); config.monitor.height = util::from_view(args.at("x-nv-video[0].clientViewportHt"sv)); config.monitor.width = util::from_view(args.at("x-nv-video[0].clientViewportWd"sv)); diff --git a/src/stream.cpp b/src/stream.cpp index 6b470ddb..c78fbbd2 100644 --- a/src/stream.cpp +++ b/src/stream.cpp @@ -33,6 +33,7 @@ extern "C" { #define IDX_PERIODIC_PING 8 #define IDX_REQUEST_IDR_FRAME 9 #define IDX_ENCRYPTED 10 +#define IDX_HDR_MODE 11 static const short packetTypes[] = { 0x0305, // Start A @@ -46,6 +47,7 @@ static const short packetTypes[] = { 0x0200, // Periodic Ping 0x0302, // IDR frame 0x0001, // fully encrypted + 0x010e, // HDR mode }; namespace asio = boost::asio; @@ -131,6 +133,15 @@ struct control_rumble_t { std::uint16_t highfreq; }; +struct control_hdr_mode_t { + control_header_v2 header; + + std::uint8_t enabled; + + // Sunshine protocol extension + SS_HDR_METADATA metadata; +}; + typedef struct control_encrypted_t { std::uint16_t encryptedHeaderType; // Always LE 0x0001 std::uint16_t length; // sizeof(seq) + 16 byte tag + secondary header and data @@ -287,6 +298,7 @@ struct session_t { int lowseq; udp::endpoint peer; safe::mail_raw_t::event_t idr_events; + std::unique_ptr qos; } video; struct { @@ -302,6 +314,7 @@ struct session_t { util::buffer_t shards_p; audio_fec_packet_t fec_packet; + std::unique_ptr qos; } audio; struct { @@ -312,6 +325,7 @@ struct session_t { std::uint8_t seq; platf::rumble_queue_t rumble_queue; + safe::mail_raw_t::event_t hdr_queue; } control; safe::mail_raw_t::event_t shutdown_event; @@ -397,13 +411,12 @@ session_t *control_server_t::get_session(const net::peer_t peer) { void control_server_t::call(std::uint16_t type, session_t *session, const std::string_view &payload) { auto cb = _map_type_cb.find(type); if(cb == std::end(_map_type_cb)) { - BOOST_LOG(warning) + BOOST_LOG(debug) << "type [Unknown] { "sv << util::hex(type).to_string_view() << " }"sv << std::endl << "---data---"sv << std::endl << util::hex_vec(payload) << std::endl << "---end data---"sv; } - else { cb->second(session, payload); } @@ -606,6 +619,36 @@ int send_rumble(session_t *session, std::uint16_t id, std::uint16_t lowfreq, std return 0; } +int send_hdr_mode(session_t *session, video::hdr_info_t hdr_info) { + if(!session->control.peer) { + BOOST_LOG(warning) << "Couldn't send HDR mode, still waiting for PING from Moonlight"sv; + // Still waiting for PING from Moonlight + return -1; + } + + control_hdr_mode_t plaintext {}; + plaintext.header.type = packetTypes[IDX_HDR_MODE]; + plaintext.header.payloadLength = sizeof(control_hdr_mode_t) - sizeof(control_header_v2); + + plaintext.enabled = hdr_info->enabled; + plaintext.metadata = hdr_info->metadata; + + std::array + encrypted_payload; + + 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 HDR mode to ["sv << addr << ':' << port << ']'; + + return -1; + } + + BOOST_LOG(debug) << "Sent HDR mode: " << hdr_info->enabled; + return 0; +} + void controlBroadcastThread(control_server_t *server) { server->map(packetTypes[IDX_PERIODIC_PING], [](session_t *session, const std::string_view &payload) { BOOST_LOG(verbose) << "type [IDX_START_A]"sv; @@ -762,7 +805,10 @@ void controlBroadcastThread(control_server_t *server) { if(session->state.load(std::memory_order_acquire) == session::state_e::STOPPING) { pos = server->_map_addr_session->erase(pos); - enet_peer_disconnect_now(session->control.peer, 0); + if(session->control.peer) { + enet_peer_disconnect_now(session->control.peer, 0); + } + session->controlEnd.raise(true); continue; } @@ -774,6 +820,16 @@ void controlBroadcastThread(control_server_t *server) { send_rumble(session, rumble->id, rumble->lowfreq, rumble->highfreq); } + // 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(); + + send_hdr_mode(session, std::move(hdr_info)); + } + ++pos; }) } @@ -1036,8 +1092,25 @@ void videoBroadcastThread(udp::socket &sock) { inspect->packet.multiFecBlocks = (blockIndex << 4) | lastBlockIndex; inspect->packet.frameIndex = av_packet->pts; + } - sock.send_to(asio::buffer(shards[x]), session->video.peer); + auto peer_address = session->video.peer.address(); + auto batch_info = platf::batched_send_info_t { + shards.shards.begin(), + shards.blocksize, + shards.nr_shards, + (uintptr_t)sock.native_handle(), + peer_address, + session->video.peer.port(), + }; + + // Use a batched send if it's supported on this platform + if(!platf::send_batch(batch_info)) { + // 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); + } } if(av_packet->flags & AV_PKT_FLAG_KEY) { @@ -1077,8 +1150,7 @@ void audioBroadcastThread(udp::socket &sock) { // works correctly. This is possible because the data and FEC shard count is // constant and known in advance. const unsigned char parity[] = { 0x77, 0x40, 0x38, 0x0e, 0xc7, 0xa7, 0x0d, 0x6c }; - memcpy(&rs.get()->m[16], parity, sizeof(parity)); - memcpy(rs.get()->parity, parity, sizeof(parity)); + memcpy(rs.get()->p, parity, sizeof(parity)); audio_packet->rtp.header = 0x80; audio_packet->rtp.packetType = 97; @@ -1319,6 +1391,13 @@ void videoThread(session_t *session) { return; } + // Enable QoS tagging on video traffic if requested by the client + if(session->config.videoQosType) { + auto address = session->video.peer.address(); + session->video.qos = std::move(platf::enable_socket_qos(ref->video_sock.native_handle(), address, + session->video.peer.port(), platf::qos_data_type_e::video)); + } + BOOST_LOG(debug) << "Start capturing Video"sv; video::capture(session->mail, session->config.monitor, session); } @@ -1336,6 +1415,13 @@ void audioThread(session_t *session) { return; } + // Enable QoS tagging on audio traffic if requested by the client + if(session->config.audioQosType) { + auto address = session->audio.peer.address(); + session->audio.qos = std::move(platf::enable_socket_qos(ref->audio_sock.native_handle(), address, + session->audio.peer.port(), platf::qos_data_type_e::audio)); + } + BOOST_LOG(debug) << "Start capturing Audio"sv; audio::capture(session->mail, session->config.audio, session); } @@ -1479,6 +1565,7 @@ std::shared_ptr alloc(config_t &config, crypto::aes_t &gcm_key, crypt session->config = config; session->control.rumble_queue = mail->queue(mail::rumble); + session->control.hdr_queue = mail->event(mail::hdr); session->control.iv = iv; session->control.cipher = crypto::cipher::gcm_t { gcm_key, false diff --git a/src/stream.h b/src/stream.h index 8e054aa9..02b30413 100644 --- a/src/stream.h +++ b/src/stream.h @@ -23,6 +23,8 @@ struct config_t { int minRequiredFecPackets; int featureFlags; int controlProtocolType; + int audioQosType; + int videoQosType; std::optional gcmap; }; diff --git a/src/upnp.cpp b/src/upnp.cpp index 36c06921..0dbdb361 100644 --- a/src/upnp.cpp +++ b/src/upnp.cpp @@ -91,6 +91,10 @@ static std::string_view status_string(int status) { } std::unique_ptr start() { + if(!config::sunshine.flags[config::flag::UPNP]) { + return nullptr; + } + int err {}; device_t device { upnpDiscover(2000, nullptr, nullptr, 0, IPv4, 2, &err) }; @@ -128,10 +132,6 @@ std::unique_ptr start() { } } - if(!config::sunshine.flags[config::flag::UPNP]) { - return nullptr; - } - auto rtsp = std::to_string(map_port(stream::RTSP_SETUP_PORT)); auto video = std::to_string(map_port(stream::VIDEO_STREAM_PORT)); auto audio = std::to_string(map_port(stream::AUDIO_STREAM_PORT)); diff --git a/src/video.cpp b/src/video.cpp index 1fd50e7e..9389ee3d 100644 --- a/src/video.cpp +++ b/src/video.cpp @@ -5,6 +5,7 @@ #include extern "C" { +#include #include } @@ -63,15 +64,29 @@ enum class profile_hevc_e : int { }; } // namespace nv +namespace qsv { -platf::mem_type_e map_dev_type(AVHWDeviceType type); +enum class profile_h264_e : int { + baseline = 66, + main = 77, + high = 100, +}; + +enum class profile_hevc_e : int { + main = 1, + main_10 = 2, +}; +} // namespace qsv + + +platf::mem_type_e map_base_dev_type(AVHWDeviceType type); platf::pix_fmt_e map_pix_fmt(AVPixelFormat fmt); util::Either dxgi_make_hwdevice_ctx(platf::hwdevice_t *hwdevice_ctx); util::Either vaapi_make_hwdevice_ctx(platf::hwdevice_t *hwdevice_ctx); util::Either cuda_make_hwdevice_ctx(platf::hwdevice_t *hwdevice_ctx); -int hwframe_ctx(ctx_t &ctx, buffer_t &hwdevice, AVPixelFormat format); +int hwframe_ctx(ctx_t &ctx, platf::hwdevice_t *hwdevice, buffer_t &hwdevice_ctx, AVPixelFormat format); class swdevice_t : public platf::hwdevice_t { public: @@ -116,17 +131,16 @@ public: return 0; } - int set_frame(AVFrame *frame) { + int set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) { this->frame = frame; // If it's a hwframe, allocate buffers for hardware - if(frame->hw_frames_ctx) { + if(hw_frames_ctx) { hw_frame.reset(frame); - if(av_hwframe_get_buffer(frame->hw_frames_ctx, frame, 0)) return -1; + if(av_hwframe_get_buffer(hw_frames_ctx, frame, 0)) return -1; } - - if(!frame->hw_frames_ctx) { + else { sw_frame.reset(frame); } @@ -181,9 +195,9 @@ public: return 0; } - int init(int in_width, int in_height, AVFrame *frame, AVPixelFormat format) { + int init(int in_width, int in_height, AVFrame *frame, AVPixelFormat format, bool hardware) { // If the device used is hardware, yet the image resides on main memory - if(frame->hw_frames_ctx) { + if(hardware) { sw_frame.reset(av_frame_alloc()); sw_frame->width = frame->width; @@ -235,11 +249,14 @@ public: }; 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 + 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 }; struct encoder_t { @@ -286,11 +303,10 @@ struct encoder_t { option_t(std::string &&name, decltype(value) &&value) : name { std::move(name) }, value { std::move(value) } {} }; - AVHWDeviceType dev_type; + AVHWDeviceType base_dev_type, derived_dev_type; AVPixelFormat dev_pix_fmt; - AVPixelFormat static_pix_fmt; - AVPixelFormat dynamic_pix_fmt; + AVPixelFormat static_pix_fmt, dynamic_pix_fmt; struct { std::vector common_options; @@ -357,6 +373,7 @@ struct sync_session_ctx_t { safe::mail_raw_t::event_t shutdown_event; safe::mail_raw_t::queue_t packets; safe::mail_raw_t::event_t idr_events; + safe::mail_raw_t::event_t hdr_events; safe::mail_raw_t::event_t touch_port_events; config_t config; @@ -376,7 +393,7 @@ using encode_e = platf::capture_e; struct capture_ctx_t { img_event_t images; - int framerate; + config_t config; }; struct capture_thread_async_ctx_t { @@ -404,10 +421,10 @@ auto capture_thread_sync = safe::make_shared(start_c static encoder_t nvenc { "nvenc"sv, #ifdef _WIN32 - AV_HWDEVICE_TYPE_D3D11VA, + AV_HWDEVICE_TYPE_D3D11VA, AV_HWDEVICE_TYPE_NONE, AV_PIX_FMT_D3D11, #else - AV_HWDEVICE_TYPE_CUDA, + AV_HWDEVICE_TYPE_CUDA, AV_HWDEVICE_TYPE_NONE, AV_PIX_FMT_CUDA, #endif AV_PIX_FMT_NV12, AV_PIX_FMT_P010, @@ -459,9 +476,64 @@ static encoder_t nvenc { }; #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, + { + // Common options + { + { "preset"s, &config::video.qsv.preset }, + { "forced_idr"s, 1 }, + { "async_depth"s, 1 }, + { "low_delay_brc"s, 1 }, + { "low_power"s, 1 }, + { "recovery_point_sei"s, 0 }, + { "pic_timing_sei"s, 0 }, + }, + // SDR-specific options + { + { "profile"s, (int)qsv::profile_hevc_e::main }, + }, + // HDR-specific options + { + { "profile"s, (int)qsv::profile_hevc_e::main_10 }, + }, + std::make_optional({ "qp"s, &config::video.qp }), + "hevc_qsv"s, + }, + { + // Common options + { + { "preset"s, &config::video.qsv.preset }, + { "cavlc"s, &config::video.qsv.cavlc }, + { "forced_idr"s, 1 }, + { "async_depth"s, 1 }, + { "low_delay_brc"s, 1 }, + { "low_power"s, 1 }, + { "recovery_point_sei"s, 0 }, + { "vcm"s, 1 }, + { "pic_timing_sei"s, 0 }, + { "max_dec_frame_buffering"s, 1 }, + }, + // SDR-specific options + { + { "profile"s, (int)qsv::profile_h264_e::high }, + }, + {}, // HDR-specific options + 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, +}; + static encoder_t amdvce { "amdvce"sv, - AV_HWDEVICE_TYPE_D3D11VA, + AV_HWDEVICE_TYPE_D3D11VA, AV_HWDEVICE_TYPE_NONE, AV_PIX_FMT_D3D11, AV_PIX_FMT_NV12, AV_PIX_FMT_P010, { @@ -506,7 +578,7 @@ static encoder_t amdvce { static encoder_t software { "software"sv, - AV_HWDEVICE_TYPE_NONE, + AV_HWDEVICE_TYPE_NONE, AV_HWDEVICE_TYPE_NONE, AV_PIX_FMT_NONE, AV_PIX_FMT_YUV420P, AV_PIX_FMT_YUV420P10, { @@ -544,7 +616,7 @@ static encoder_t software { #ifdef __linux__ static encoder_t vaapi { "vaapi"sv, - AV_HWDEVICE_TYPE_VAAPI, + AV_HWDEVICE_TYPE_VAAPI, AV_HWDEVICE_TYPE_NONE, AV_PIX_FMT_VAAPI, AV_PIX_FMT_NV12, AV_PIX_FMT_YUV420P10, { @@ -580,7 +652,7 @@ static encoder_t vaapi { #ifdef __APPLE__ static encoder_t videotoolbox { "videotoolbox"sv, - AV_HWDEVICE_TYPE_NONE, + AV_HWDEVICE_TYPE_NONE, AV_HWDEVICE_TYPE_NONE, AV_PIX_FMT_VIDEOTOOLBOX, AV_PIX_FMT_NV12, AV_PIX_FMT_NV12, { @@ -618,6 +690,7 @@ static std::vector encoders { nvenc, #endif #ifdef _WIN32 + quicksync, amdvce, #endif #ifdef __linux__ @@ -629,15 +702,16 @@ static std::vector encoders { software }; -void reset_display(std::shared_ptr &disp, AVHWDeviceType type, const std::string &display_name, int framerate) { +void reset_display(std::shared_ptr &disp, AVHWDeviceType 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_dev_type(type), display_name, framerate); + disp = platf::display(map_base_dev_type(type), display_name, config); if(disp) { break; } + // The capture code depends on us to sleep between failures std::this_thread::sleep_for(200ms); } } @@ -665,7 +739,7 @@ void captureThread( // 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_dev_type(encoder.dev_type)); + auto display_names = platf::display_names(map_base_dev_type(encoder.base_dev_type)); int display_p = 0; if(display_names.empty()) { @@ -684,7 +758,7 @@ void captureThread( capture_ctxs.emplace_back(std::move(*capture_ctx)); } - auto disp = platf::display(map_dev_type(encoder.dev_type), display_names[display_p], capture_ctxs.front().framerate); + auto disp = platf::display(map_base_dev_type(encoder.base_dev_type), display_names[display_p], capture_ctxs.front().config); if(!disp) { return; } @@ -766,16 +840,32 @@ void captureThread( // Wait for the other shared_ptr's of display to be destroyed. // New displays will only be created in this thread. while(display_wp->use_count() != 1) { - std::this_thread::sleep_for(100ms); + // Free images that weren't consumed by the encoders. These can reference the display and prevent + // the ref count from reaching 1. We do this here rather than on the encoder thread to avoid race + // conditions where the encoding loop might free a good frame after reinitializing if we capture + // a new frame here before the encoder has finished reinitializing. + KITTY_WHILE_LOOP(auto capture_ctx = std::begin(capture_ctxs), capture_ctx != std::end(capture_ctxs), { + if(!capture_ctx->images->running()) { + capture_ctx = capture_ctxs.erase(capture_ctx); + continue; + } + + while(capture_ctx->images->peek()) { + capture_ctx->images->pop(); + } + + ++capture_ctx; + }); + + std::this_thread::sleep_for(20ms); } while(capture_ctx_queue->running()) { - reset_display(disp, encoder.dev_type, display_names[display_p], capture_ctxs.front().framerate); - + // reset_display() will sleep between retries + reset_display(disp, encoder.base_dev_type, display_names[display_p], capture_ctxs.front().config); if(disp) { break; } - std::this_thread::sleep_for(200ms); } if(!disp) { return; @@ -868,8 +958,8 @@ int encode(int64_t frame_nr, session_t &session, frame_t::pointer frame, safe::m return 0; } -std::optional make_session(const encoder_t &encoder, const config_t &config, int width, int height, std::shared_ptr &&hwdevice) { - bool hardware = encoder.dev_type != AV_HWDEVICE_TYPE_NONE; +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; auto &video_format = config.videoFormat == 0 ? encoder.h264 : encoder.hevc; if(!video_format[encoder_t::PASSED]) { @@ -929,35 +1019,46 @@ std::optional make_session(const encoder_t &encoder, const config_t & ctx->color_range = (config.encoderCscMode & 0x1) ? AVCOL_RANGE_JPEG : AVCOL_RANGE_MPEG; int sws_color_space; - switch(config.encoderCscMode >> 1) { - case 0: - default: - // Rec. 601 - BOOST_LOG(info) << "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; - - case 1: - // Rec. 709 - BOOST_LOG(info) << "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) << "Color coding [Rec. 2020]"sv; + 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_BT2020_10; + ctx->color_trc = AVCOL_TRC_SMPTE2084; ctx->colorspace = AVCOL_SPC_BT2020_NCL; sws_color_space = SWS_CS_BT2020; - break; } + 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; + + 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; @@ -971,17 +1072,39 @@ std::optional make_session(const encoder_t &encoder, const config_t & // Used by cbs::make_sps_hevc ctx->sw_pix_fmt = sw_fmt; - buffer_t hwdevice_ctx; if(hardware) { + buffer_t hwdevice_ctx; + ctx->pix_fmt = encoder.dev_pix_fmt; + // Create the base hwdevice context auto buf_or_error = encoder.make_hwdevice_ctx(hwdevice.get()); if(buf_or_error.has_right()) { return std::nullopt; } - hwdevice_ctx = std::move(buf_or_error.left()); - if(hwframe_ctx(ctx, hwdevice_ctx, sw_fmt)) { + + // 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; + + // 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; + } + + auto err = av_hwdevice_ctx_create_derived(&derived_hwdevice_ctx, encoder.derived_dev_type, hwdevice_ctx.get(), 0); + if(err) { + char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 }; + BOOST_LOG(error) << "Failed to derive device context: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err); + + return std::nullopt; + } + + hwdevice_ctx = std::move(derived_hwdevice_ctx); + } + + if(hwframe_ctx(ctx, hwdevice.get(), hwdevice_ctx, sw_fmt)) { return std::nullopt; } @@ -1027,17 +1150,30 @@ std::optional make_session(const encoder_t &encoder, const config_t & auto bitrate = config.bitrate * 1000; ctx->rc_max_rate = bitrate; ctx->bit_rate = bitrate; - ctx->rc_min_rate = bitrate; - if(!hardware && (ctx->slices > 1 || config.videoFormat != 0)) { - // Use a larger rc_buffer_size for software encoding when slices are enabled, - // because libx264 can severely degrade quality if the buffer is too small. - // libx265 encounters this issue more frequently, so always scale the - // buffer by 1.5x for software HEVC encoding. - ctx->rc_buffer_size = bitrate / ((config.framerate * 10) / 15); + if(encoder.flags & CBR_WITH_VBR) { + // Ensure rc_max_bitrate != bit_rate to force VBR mode + ctx->bit_rate--; } else { - ctx->rc_buffer_size = bitrate / config.framerate; + ctx->rc_min_rate = bitrate; + } + + if(encoder.flags & RELAXED_COMPLIANCE) { + ctx->strict_std_compliance = FF_COMPLIANCE_UNOFFICIAL; + } + + if(!(encoder.flags & NO_RC_BUF_LIMIT)) { + if(!hardware && (ctx->slices > 1 || config.videoFormat != 0)) { + // Use a larger rc_buffer_size for software encoding when slices are enabled, + // because libx264 can severely degrade quality if the buffer is too small. + // libx265 encounters this issue more frequently, so always scale the + // buffer by 1.5x for software HEVC encoding. + ctx->rc_buffer_size = bitrate / ((config.framerate * 10) / 15); + } + else { + ctx->rc_buffer_size = bitrate / config.framerate; + } } } else if(video_format.qp) { @@ -1063,9 +1199,35 @@ std::optional make_session(const encoder_t &encoder, const config_t & frame->width = ctx->width; frame->height = ctx->height; + // Attach HDR metadata to the AVFrame + if(config.dynamicRange && disp->is_hdr()) { + SS_HDR_METADATA hdr_metadata; + if(disp->get_hdr_metadata(hdr_metadata)) { + auto mdm = av_mastering_display_metadata_create_side_data(frame.get()); - if(hardware) { - frame->hw_frames_ctx = av_buffer_ref(ctx->hw_frames_ctx); + mdm->display_primaries[0][0] = av_make_q(hdr_metadata.displayPrimaries[0].x, 50000); + mdm->display_primaries[0][1] = av_make_q(hdr_metadata.displayPrimaries[0].y, 50000); + mdm->display_primaries[1][0] = av_make_q(hdr_metadata.displayPrimaries[1].x, 50000); + mdm->display_primaries[1][1] = av_make_q(hdr_metadata.displayPrimaries[1].y, 50000); + mdm->display_primaries[2][0] = av_make_q(hdr_metadata.displayPrimaries[2].x, 50000); + mdm->display_primaries[2][1] = av_make_q(hdr_metadata.displayPrimaries[2].y, 50000); + + mdm->white_point[0] = av_make_q(hdr_metadata.whitePoint.x, 50000); + mdm->white_point[1] = av_make_q(hdr_metadata.whitePoint.y, 50000); + + mdm->min_luminance = av_make_q(hdr_metadata.minDisplayLuminance, 10000); + mdm->max_luminance = av_make_q(hdr_metadata.maxDisplayLuminance, 1); + + mdm->has_luminance = hdr_metadata.maxDisplayLuminance != 0 ? 1 : 0; + mdm->has_primaries = hdr_metadata.displayPrimaries[0].x != 0 ? 1 : 0; + + if(hdr_metadata.maxContentLightLevel != 0 || hdr_metadata.maxFrameAverageLightLevel != 0) { + auto clm = av_content_light_metadata_create_side_data(frame.get()); + + clm->MaxCLL = hdr_metadata.maxContentLightLevel; + clm->MaxFALL = hdr_metadata.maxFrameAverageLightLevel; + } + } } std::shared_ptr device; @@ -1073,7 +1235,7 @@ std::optional make_session(const encoder_t &encoder, const config_t & if(!hwdevice->data) { auto device_tmp = std::make_unique(); - if(device_tmp->init(width, height, frame.get(), sw_fmt)) { + if(device_tmp->init(width, height, frame.get(), sw_fmt, hardware)) { return std::nullopt; } @@ -1083,7 +1245,7 @@ std::optional make_session(const encoder_t &encoder, const config_t & device = std::move(hwdevice); } - if(device->set_frame(frame.release())) { + if(device->set_frame(frame.release(), ctx->hw_frames_ctx)) { return std::nullopt; } @@ -1111,13 +1273,13 @@ void encode_run( safe::mail_t mail, img_event_t images, config_t config, - int width, int height, + std::shared_ptr disp, std::shared_ptr &&hwdevice, safe::signal_t &reinit_event, const encoder_t &encoder, void *channel_data) { - auto session = make_session(encoder, config, width, height, std::move(hwdevice)); + auto session = make_session(disp.get(), encoder, config, disp->width, disp->height, std::move(hwdevice)); if(!session) { return; } @@ -1128,6 +1290,13 @@ void encode_run( auto packets = mail::man->queue(mail::video_packets); auto idr_events = mail->event(mail::idr); + // Load a dummy image into the AVFrame to ensure we have something to encode + // even if we time out waiting on the first frame. + auto dummy_img = disp->alloc_img(); + if(!dummy_img || disp->dummy_img(dummy_img.get()) || session->device->convert(*dummy_img)) { + return; + } + while(true) { if(shutdown_event->peek() || reinit_event.peek() || !images->running()) { break; @@ -1140,14 +1309,15 @@ void encode_run( idr_events->pop(); } + // Encode at a minimum of 10 FPS to avoid image quality issues with static content if(!frame->key_frame || images->peek()) { if(auto img = images->pop(100ms)) { - session->device->convert(*img); + if(session->device->convert(*img)) { + BOOST_LOG(error) << "Could not convert image"sv; + return; + } } - else if(images->running()) { - continue; - } - else { + else if(!images->running()) { break; } } @@ -1204,7 +1374,15 @@ std::optional make_synced_session(platf::display_t *disp, const // absolute mouse coordinates require that the dimensions of the screen are known ctx.touch_port_events->raise(make_port(disp, ctx.config)); - auto session = make_session(encoder, ctx.config, img.width, img.height, std::move(hwdevice)); + // 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; + } + ctx.hdr_events->raise(std::move(hdr_info)); + + auto session = make_session(disp, encoder, ctx.config, img.width, img.height, std::move(hwdevice)); if(!session) { return std::nullopt; } @@ -1219,7 +1397,7 @@ encode_e encode_run_sync( encode_session_ctx_queue_t &encode_session_ctx_queue) { const auto &encoder = encoders.front(); - auto display_names = platf::display_names(map_dev_type(encoder.dev_type)); + auto display_names = platf::display_names(map_base_dev_type(encoder.base_dev_type)); int display_p = 0; if(display_names.empty()) { @@ -1247,15 +1425,12 @@ encode_e encode_run_sync( synced_session_ctxs.emplace_back(std::make_unique(std::move(*ctx))); } - int framerate = synced_session_ctxs.front()->config.framerate; - while(encode_session_ctx_queue.running()) { - reset_display(disp, encoder.dev_type, display_names[display_p], framerate); + // reset_display() will sleep between retries + reset_display(disp, encoder.base_dev_type, display_names[display_p], synced_session_ctxs.front()->config); if(disp) { break; } - - std::this_thread::sleep_for(200ms); } if(!disp) { @@ -1410,8 +1585,7 @@ void capture_async( return; } - ref->capture_ctx_queue->raise(capture_ctx_t { - images, config.framerate }); + ref->capture_ctx_queue->raise(capture_ctx_t { images, config }); if(!ref->capture_ctx_queue->running()) { return; @@ -1420,6 +1594,7 @@ void capture_async( int frame_nr = 1; auto touch_port_event = mail->event(mail::touch_port); + auto hdr_event = mail->event(mail::hdr); // Encoding takes place on this thread platf::adjust_thread_priority(platf::thread_priority_e::high); @@ -1427,7 +1602,7 @@ void capture_async( while(!shutdown_event->peek() && images->running()) { // Wait for the main capture event when the display is being reinitialized if(ref->reinit_event.peek()) { - std::this_thread::sleep_for(100ms); + std::this_thread::sleep_for(20ms); continue; } // Wait for the display to be ready @@ -1448,29 +1623,24 @@ void capture_async( return; } - auto dummy_img = display->alloc_img(); - if(!dummy_img || display->dummy_img(dummy_img.get())) { - return; - } - - images->raise(std::move(dummy_img)); - // absolute mouse coordinates require that the dimensions of the screen are known touch_port_event->raise(make_port(display.get(), config)); + // 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; + } + hdr_event->raise(std::move(hdr_info)); + encode_run( frame_nr, mail, images, - config, display->width, display->height, + config, display, std::move(hwdevice), ref->reinit_event, *ref->encoder_p, channel_data); - - // Free images that weren't consumed by the encoder before it quit. - // This is critical to allow the display_t to be freed correctly. - while(images->peek()) { - images->pop(); - } } } @@ -1493,6 +1663,7 @@ void capture( mail->event(mail::shutdown), mail::man->queue(mail::video_packets), std::move(idr_events), + mail->event(mail::hdr), mail->event(mail::touch_port), config, 1, @@ -1510,7 +1681,7 @@ enum validate_flag_e { }; int validate_config(std::shared_ptr &disp, const encoder_t &encoder, const config_t &config) { - reset_display(disp, encoder.dev_type, config::video.output_name, config.framerate); + reset_display(disp, encoder.base_dev_type, config::video.output_name, config); if(!disp) { return -1; } @@ -1521,7 +1692,7 @@ int validate_config(std::shared_ptr &disp, const encoder_t &en return -1; } - auto session = make_session(encoder, config, disp->width, disp->height, std::move(hwdevice)); + auto session = make_session(disp.get(), encoder, config, disp->width, disp->height, std::move(hwdevice)); if(!session) { return -1; } @@ -1812,8 +1983,8 @@ int init() { return 0; } -int hwframe_ctx(ctx_t &ctx, buffer_t &hwdevice, AVPixelFormat format) { - buffer_t frame_ref { av_hwframe_ctx_alloc(hwdevice.get()) }; +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; @@ -1822,6 +1993,9 @@ int hwframe_ctx(ctx_t &ctx, buffer_t &hwdevice, AVPixelFormat format) { 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; } @@ -1932,7 +2106,7 @@ int start_capture_sync(capture_thread_sync_ctx_t &ctx) { } void end_capture_sync(capture_thread_sync_ctx_t &ctx) {} -platf::mem_type_e map_dev_type(AVHWDeviceType type) { +platf::mem_type_e map_base_dev_type(AVHWDeviceType type) { switch(type) { case AV_HWDEVICE_TYPE_D3D11VA: return platf::mem_type_e::dxgi; diff --git a/src/video.h b/src/video.h index 3d99f855..3d48af14 100644 --- a/src/video.h +++ b/src/video.h @@ -48,6 +48,16 @@ struct packet_raw_t { using packet_t = std::unique_ptr; +struct hdr_info_raw_t { + explicit hdr_info_raw_t(bool enabled) : enabled { enabled }, metadata {} {}; + explicit hdr_info_raw_t(bool enabled, const SS_HDR_METADATA &metadata) : enabled { enabled }, metadata { metadata } {}; + + bool enabled; + SS_HDR_METADATA metadata; +}; + +using hdr_info_t = std::unique_ptr; + struct config_t { int width; int height; diff --git a/src_assets/common/assets/web/config.html b/src_assets/common/assets/web/config.html index 096233dc..1bc06507 100644 --- a/src_assets/common/assets/web/config.html +++ b/src_assets/common/assets/web/config.html @@ -523,6 +523,7 @@ + +
+
+ + +
+
+ + +
+
@@ -820,6 +844,10 @@ id: "nv", name: "NVIDIA NVENC Encoder", }, + { + id: "qsv", + name: "Intel QuickSync Encoder", + }, { id: "amd", name: "AMD AMF Encoder", @@ -855,12 +883,12 @@ } if (this.platform == "linux") { this.tabs = this.tabs.filter((el) => { - return el.id !== "amd" && el.id !== "vt"; + return el.id !== "amd" && el.id !== "qsv" && el.id !== "vt"; }); } if (this.platform == "macos") { this.tabs = this.tabs.filter((el) => { - return el.id !== "amd" && el.id !== "nv" && el.id !== "va-api"; + return el.id !== "amd" && el.id !== "nv" && el.id !== "qsv" && el.id !== "va-api"; }); } @@ -884,6 +912,8 @@ this.config.nv_tune = this.config.nv_tune || "ull"; this.config.nv_coder = this.config.nv_coder || "auto"; this.config.nv_rc = this.config.nv_rc || "cbr"; + this.config.qsv_preset = this.config.qsv_preset || "medium"; + this.config.qsv_coder = this.config.qsv_coder || "auto"; this.config.amd_coder = this.config.amd_coder || "auto" this.config.amd_quality = this.config.amd_quality || "balanced"; this.config.amd_rc = this.config.amd_rc || "vbr_latency"; diff --git a/src_assets/linux/assets/apps.json b/src_assets/linux/assets/apps.json index 5331dcf8..04d98b62 100644 --- a/src_assets/linux/assets/apps.json +++ b/src_assets/linux/assets/apps.json @@ -18,7 +18,7 @@ ] }, { - "name": "Steam BigPicture", + "name": "Steam Big Picture", "detached": [ "setsid steam steam://open/bigpicture" ], diff --git a/src_assets/macos/assets/apps.json b/src_assets/macos/assets/apps.json index 88559542..dcb8637c 100644 --- a/src_assets/macos/assets/apps.json +++ b/src_assets/macos/assets/apps.json @@ -8,7 +8,7 @@ "image-path": "desktop.png" }, { - "name": "Steam BigPicture", + "name": "Steam Big Picture", "detached": [ "open steam://open/bigpicture" ], diff --git a/src_assets/windows/assets/apps.json b/src_assets/windows/assets/apps.json index 08d2386d..72a56a7e 100644 --- a/src_assets/windows/assets/apps.json +++ b/src_assets/windows/assets/apps.json @@ -8,7 +8,7 @@ "image-path": "desktop.png" }, { - "name": "Steam BigPicture", + "name": "Steam Big Picture", "detached": [ "steam steam://open/bigpicture" ], diff --git a/src_assets/windows/assets/shaders/directx/ConvertUVPS_PQ.hlsl b/src_assets/windows/assets/shaders/directx/ConvertUVPS_PQ.hlsl new file mode 100644 index 00000000..3ae0fb1d --- /dev/null +++ b/src_assets/windows/assets/shaders/directx/ConvertUVPS_PQ.hlsl @@ -0,0 +1,69 @@ +Texture2D image : register(t0); + +SamplerState def_sampler : register(s0); + +struct FragTexWide { + float3 uuv : TEXCOORD0; +}; + +cbuffer ColorMatrix : register(b0) { + float4 color_vec_y; + float4 color_vec_u; + float4 color_vec_v; + float2 range_y; + float2 range_uv; +}; + +float3 NitsToPQ(float3 L) +{ + // Constants from SMPTE 2084 PQ + static const float m1 = 2610.0 / 4096.0 / 4; + static const float m2 = 2523.0 / 4096.0 * 128; + static const float c1 = 3424.0 / 4096.0; + static const float c2 = 2413.0 / 4096.0 * 32; + static const float c3 = 2392.0 / 4096.0 * 32; + + float3 Lp = pow(saturate(L / 10000.0), m1); + return pow((c1 + c2 * Lp) / (1 + c3 * Lp), m2); +} + +float3 Rec709toRec2020(float3 rec709) +{ + static const float3x3 ConvMat = + { + 0.627402, 0.329292, 0.043306, + 0.069095, 0.919544, 0.011360, + 0.016394, 0.088028, 0.895578 + }; + return mul(ConvMat, rec709); +} + +float3 scRGBTo2100PQ(float3 rgb) +{ + // Convert from Rec 709 primaries (used by scRGB) to Rec 2020 primaries (used by Rec 2100) + rgb = Rec709toRec2020(rgb); + + // 1.0f is defined as 80 nits in the scRGB colorspace + rgb *= 80; + + // Apply the PQ transfer function on the raw color values in nits + return NitsToPQ(rgb); +} + +//-------------------------------------------------------------------------------------- +// Pixel Shader +//-------------------------------------------------------------------------------------- +float2 main_ps(FragTexWide input) : SV_Target +{ + float3 rgb_left = scRGBTo2100PQ(image.Sample(def_sampler, input.uuv.xz).rgb); + float3 rgb_right = scRGBTo2100PQ(image.Sample(def_sampler, input.uuv.yz).rgb); + float3 rgb = (rgb_left + rgb_right) * 0.5; + + float u = dot(color_vec_u.xyz, rgb) + color_vec_u.w; + float v = dot(color_vec_v.xyz, rgb) + color_vec_v.w; + + u = u * range_uv.x + range_uv.y; + v = v * range_uv.x + range_uv.y; + + return float2(u, v); +} diff --git a/src_assets/windows/assets/shaders/directx/ConvertYPS_PQ.hlsl b/src_assets/windows/assets/shaders/directx/ConvertYPS_PQ.hlsl new file mode 100644 index 00000000..cc3054e8 --- /dev/null +++ b/src_assets/windows/assets/shaders/directx/ConvertYPS_PQ.hlsl @@ -0,0 +1,62 @@ +Texture2D image : register(t0); + +SamplerState def_sampler : register(s0); + +cbuffer ColorMatrix : register(b0) { + float4 color_vec_y; + float4 color_vec_u; + float4 color_vec_v; + float2 range_y; + float2 range_uv; +}; + +struct PS_INPUT +{ + float4 pos : SV_POSITION; + float2 tex : TEXCOORD; +}; + +float3 NitsToPQ(float3 L) +{ + // Constants from SMPTE 2084 PQ + static const float m1 = 2610.0 / 4096.0 / 4; + static const float m2 = 2523.0 / 4096.0 * 128; + static const float c1 = 3424.0 / 4096.0; + static const float c2 = 2413.0 / 4096.0 * 32; + static const float c3 = 2392.0 / 4096.0 * 32; + + float3 Lp = pow(saturate(L / 10000.0), m1); + return pow((c1 + c2 * Lp) / (1 + c3 * Lp), m2); +} + +float3 Rec709toRec2020(float3 rec709) +{ + static const float3x3 ConvMat = + { + 0.627402, 0.329292, 0.043306, + 0.069095, 0.919544, 0.011360, + 0.016394, 0.088028, 0.895578 + }; + return mul(ConvMat, rec709); +} + +float3 scRGBTo2100PQ(float3 rgb) +{ + // Convert from Rec 709 primaries (used by scRGB) to Rec 2020 primaries (used by Rec 2100) + rgb = Rec709toRec2020(rgb); + + // 1.0f is defined as 80 nits in the scRGB colorspace + rgb *= 80; + + // Apply the PQ transfer function on the raw color values in nits + return NitsToPQ(rgb); +} + +float main_ps(PS_INPUT frag_in) : SV_Target +{ + float3 rgb = scRGBTo2100PQ(image.Sample(def_sampler, frag_in.tex, 0).rgb); + + float y = dot(color_vec_y.xyz, rgb) + color_vec_y.w; + + return y * range_y.x + range_y.y; +} diff --git a/src_assets/windows/assets/shaders/directx/ScenePS_NW.hlsl b/src_assets/windows/assets/shaders/directx/ScenePS_NW.hlsl new file mode 100644 index 00000000..9088b767 --- /dev/null +++ b/src_assets/windows/assets/shaders/directx/ScenePS_NW.hlsl @@ -0,0 +1,22 @@ +Texture2D image : register(t0); + +SamplerState def_sampler : register(s0); + +struct PS_INPUT +{ + float4 pos : SV_POSITION; + float2 tex : TEXCOORD; +}; + +cbuffer SdrScaling : register(b0) { + float scale_factor; +}; + +float4 main_ps(PS_INPUT frag_in) : SV_Target +{ + float4 rgba = image.Sample(def_sampler, frag_in.tex, 0); + + rgba.rgb = rgba.rgb * scale_factor; + + return rgba; +} diff --git a/sunshine.service.in b/sunshine.service.in index 157b3a4d..965b7871 100644 --- a/sunshine.service.in +++ b/sunshine.service.in @@ -1,8 +1,12 @@ [Unit] Description=@PROJECT_DESCRIPTION@ +StartLimitIntervalSec=500 +StartLimitBurst=5 [Service] ExecStart=@SUNSHINE_EXECUTABLE_PATH@ +Restart=on-failure +RestartSec=5s [Install] WantedBy=graphical-session.target diff --git a/third-party/ffmpeg-linux-aarch64 b/third-party/ffmpeg-linux-aarch64 index 0341a8fe..12dfcbe5 160000 --- a/third-party/ffmpeg-linux-aarch64 +++ b/third-party/ffmpeg-linux-aarch64 @@ -1 +1 @@ -Subproject commit 0341a8fe5a90cd3ea297de3af479dae336370993 +Subproject commit 12dfcbe5f3b2af81c4333116c8a3125a7d6f5c0b diff --git a/third-party/ffmpeg-linux-x86_64 b/third-party/ffmpeg-linux-x86_64 index 999e6746..09011c45 160000 --- a/third-party/ffmpeg-linux-x86_64 +++ b/third-party/ffmpeg-linux-x86_64 @@ -1 +1 @@ -Subproject commit 999e6746bdf3f4ce2e5eda2e6f8d6ea92cb5372a +Subproject commit 09011c45a76903e7c7fea28133315298e00db1cf diff --git a/third-party/ffmpeg-macos-aarch64 b/third-party/ffmpeg-macos-aarch64 index 3507f2d5..9bc91958 160000 --- a/third-party/ffmpeg-macos-aarch64 +++ b/third-party/ffmpeg-macos-aarch64 @@ -1 +1 @@ -Subproject commit 3507f2d5793a1809e522e7fcf98bad5758653f43 +Subproject commit 9bc919589b8d3ed475bca14b4d749da210ea3efd diff --git a/third-party/ffmpeg-macos-x86_64 b/third-party/ffmpeg-macos-x86_64 index 01421e5a..4f572c8d 160000 --- a/third-party/ffmpeg-macos-x86_64 +++ b/third-party/ffmpeg-macos-x86_64 @@ -1 +1 @@ -Subproject commit 01421e5a1435d0428d335ea1c19a9e0720180cbe +Subproject commit 4f572c8d1408a8220d0f3464ead2f199a7d69abb diff --git a/third-party/ffmpeg-windows-x86_64 b/third-party/ffmpeg-windows-x86_64 index e0ba0df1..843be4fa 160000 --- a/third-party/ffmpeg-windows-x86_64 +++ b/third-party/ffmpeg-windows-x86_64 @@ -1 +1 @@ -Subproject commit e0ba0df13687bd6355564bfd0e1dc690856abde3 +Subproject commit 843be4fa79fda6a633977edece4ec35b73d884b4 diff --git a/third-party/miniupnp b/third-party/miniupnp index 207cf440..014c9df8 160000 --- a/third-party/miniupnp +++ b/third-party/miniupnp @@ -1 +1 @@ -Subproject commit 207cf440a22c075cb55fb067a850be4f9c204e6e +Subproject commit 014c9df8ee7a36e5bf85aa619062a2d4b95ec8f6 diff --git a/third-party/moonlight-common-c b/third-party/moonlight-common-c index ef9ad529..07beb0f0 160000 --- a/third-party/moonlight-common-c +++ b/third-party/moonlight-common-c @@ -1 +1 @@ -Subproject commit ef9ad529a493d699724d84a189cc1899afcc2d72 +Subproject commit 07beb0f0a520106c49fda7664369b29e6938ea6e diff --git a/third-party/nanors b/third-party/nanors new file mode 160000 index 00000000..395e5ada --- /dev/null +++ b/third-party/nanors @@ -0,0 +1 @@ +Subproject commit 395e5ada44dd8d5974eaf6bb6b17f23406e3ca72 diff --git a/third-party/nv-codec-headers b/third-party/nv-codec-headers index b550d404..2055784e 160000 --- a/third-party/nv-codec-headers +++ b/third-party/nv-codec-headers @@ -1 +1 @@ -Subproject commit b550d4042f1ac0990efa1fa9f0f0c08fb6b24446 +Subproject commit 2055784e5d5bfb3df78d4d3645f345f19062dce2 diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index a66bbb2c..c2121a7d 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -27,3 +27,12 @@ target_link_libraries(sunshinesvc wtsapi32 ${PLATFORM_LIBRARIES}) target_compile_options(sunshinesvc PRIVATE ${SUNSHINE_COMPILE_OPTIONS}) + +add_executable(ddprobe ddprobe.cpp) +set_target_properties(ddprobe PROPERTIES CXX_STANDARD 17) +target_link_libraries(ddprobe + ${CMAKE_THREAD_LIBS_INIT} + dxgi + d3d11 + ${PLATFORM_LIBRARIES}) +target_compile_options(ddprobe PRIVATE ${SUNSHINE_COMPILE_OPTIONS}) diff --git a/tools/ddprobe.cpp b/tools/ddprobe.cpp new file mode 100644 index 00000000..bf5613b3 --- /dev/null +++ b/tools/ddprobe.cpp @@ -0,0 +1,153 @@ +#include +#include + +#include +#include +#include +#include + +#include "src/utility.h" + +using namespace std::literals; +namespace dxgi { +template +void Release(T *dxgi) { + dxgi->Release(); +} + +using factory1_t = util::safe_ptr>; +using adapter_t = util::safe_ptr>; +using output_t = util::safe_ptr>; +using output1_t = util::safe_ptr>; +using device_t = util::safe_ptr>; +using dup_t = util::safe_ptr>; + +} // namespace dxgi + +LSTATUS set_gpu_preference(int preference) { + // The GPU preferences key uses app path as the value name. + WCHAR executable_path[MAX_PATH]; + GetModuleFileNameW(NULL, executable_path, ARRAYSIZE(executable_path)); + + WCHAR value_data[128]; + swprintf_s(value_data, L"GpuPreference=%d;", preference); + + auto status = RegSetKeyValueW(HKEY_CURRENT_USER, + L"Software\\Microsoft\\DirectX\\UserGpuPreferences", + executable_path, + REG_SZ, + value_data, + (wcslen(value_data) + 1) * sizeof(WCHAR)); + if(status != ERROR_SUCCESS) { + std::cout << "Failed to set GPU preference: "sv << status << std::endl; + return status; + } + + return ERROR_SUCCESS; +} + +HRESULT test_dxgi_duplication(dxgi::adapter_t &adapter, dxgi::output_t &output) { + D3D_FEATURE_LEVEL featureLevels[] { + D3D_FEATURE_LEVEL_11_1, + D3D_FEATURE_LEVEL_11_0, + D3D_FEATURE_LEVEL_10_1, + D3D_FEATURE_LEVEL_10_0, + D3D_FEATURE_LEVEL_9_3, + D3D_FEATURE_LEVEL_9_2, + D3D_FEATURE_LEVEL_9_1 + }; + + dxgi::device_t device; + auto status = D3D11CreateDevice( + adapter.get(), + D3D_DRIVER_TYPE_UNKNOWN, + nullptr, + D3D11_CREATE_DEVICE_VIDEO_SUPPORT, + featureLevels, sizeof(featureLevels) / sizeof(D3D_FEATURE_LEVEL), + D3D11_SDK_VERSION, + &device, + nullptr, + nullptr); + if(FAILED(status)) { + std::cout << "Failed to create D3D11 device for DD test [0x"sv << util::hex(status).to_string_view() << ']' << std::endl; + return status; + } + + dxgi::output1_t output1; + status = output->QueryInterface(IID_IDXGIOutput1, (void **)&output1); + if(FAILED(status)) { + std::cout << "Failed to query IDXGIOutput1 from the output"sv << std::endl; + return status; + } + + // Return the result of DuplicateOutput() to Sunshine + dxgi::dup_t dup; + return output1->DuplicateOutput((IUnknown *)device.get(), &dup); +} + +int main(int argc, char *argv[]) { + HRESULT status; + + // Display name may be omitted + if(argc != 2 && argc != 3) { + std::cout << "ddprobe.exe [GPU preference value] [display name]"sv << std::endl; + return -1; + } + + std::wstring display_name; + if(argc == 3) { + std::wstring_convert, wchar_t> converter; + display_name = converter.from_bytes(argv[2]); + } + + // We must set the GPU preference before making any DXGI/D3D calls + status = set_gpu_preference(atoi(argv[1])); + if(status != ERROR_SUCCESS) { + return status; + } + + // Remove the GPU preference when we're done + auto reset_gpu = util::fail_guard([]() { + WCHAR tool_path[MAX_PATH]; + GetModuleFileNameW(NULL, tool_path, ARRAYSIZE(tool_path)); + + RegDeleteKeyValueW(HKEY_CURRENT_USER, + L"Software\\Microsoft\\DirectX\\UserGpuPreferences", + tool_path); + }); + + dxgi::factory1_t factory; + status = CreateDXGIFactory1(IID_IDXGIFactory1, (void **)&factory); + if(FAILED(status)) { + std::cout << "Failed to create DXGIFactory1 [0x"sv << util::hex(status).to_string_view() << ']' << std::endl; + return status; + } + + dxgi::adapter_t::pointer adapter_p {}; + for(int x = 0; factory->EnumAdapters1(x, &adapter_p) != DXGI_ERROR_NOT_FOUND; ++x) { + dxgi::adapter_t adapter { adapter_p }; + + dxgi::output_t::pointer output_p {}; + for(int y = 0; adapter->EnumOutputs(y, &output_p) != DXGI_ERROR_NOT_FOUND; ++y) { + dxgi::output_t output { output_p }; + + DXGI_OUTPUT_DESC desc; + output->GetDesc(&desc); + + // If a display name was specified and this one doesn't match, skip it + if(!display_name.empty() && desc.DeviceName != display_name) { + continue; + } + + // If this display is not part of the desktop, we definitely can't capture it + if(!desc.AttachedToDesktop) { + continue; + } + + // We found the matching output. Test it and return the result. + return test_dxgi_duplication(adapter, output); + } + } + + return 0; +}