Merge pull request #738 from LizardByte/nightly

v0.18.0
This commit is contained in:
ReenigneArcher
2023-01-29 22:26:36 -05:00
committed by GitHub
78 changed files with 3905 additions and 746 deletions

View File

@@ -1 +0,0 @@
linux/amd64,linux/arm64/v8

View File

@@ -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/

View File

@@ -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

View File

@@ -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

View File

@@ -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 }}

View File

@@ -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<<EOF" >> $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

View File

@@ -9,7 +9,7 @@ name: Stale Issues / PRs
on:
schedule:
- cron: '00 00 * * *'
- cron: '00 10 * * *'
jobs:
stale:

4
.gitmodules vendored
View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
`<SUNSHINE_VERSION>-<SUNSHINE_OS>`. 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-<SUNSHINE_OS>` or `ghcr.io/lizardbyte/sunshine:latest-<SUNSHINE_OS>` 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 | ✅ |

View File

@@ -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"]

View File

@@ -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"]

155
docker/fedora-36.dockerfile Normal file
View File

@@ -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"]

155
docker/fedora-37.dockerfile Normal file
View File

@@ -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"]

View File

@@ -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"]

View File

@@ -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"]

View File

@@ -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"]

View File

@@ -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

View File

@@ -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 <https://docs.nvidia.com/video-technologies/video-codec-sdk/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
^^^^^^^^^^^

View File

@@ -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 <https://developer.nvidia.com/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.

View File

@@ -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=<see table>
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 <https://www.youtube.com/playlist?list=PLMYr5_xSeuXAbhxYHz86hA1eCDugoxXY0>`_.

View File

@@ -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 <https://docs.nvidia.com/deploy/cuda-compatibility/index.html>`_ for more info.
Select the appropriate run file based on your desired CUDA version and architecture according to
`CUDA Toolkit Archive <https://developer.nvidia.com/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
----------------

View File

@@ -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
----------------

View File

@@ -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.

View File

@@ -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@)

View File

@@ -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)

View File

@@ -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<int> 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<int> 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<std::string, std::string> &&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()) {

View File

@@ -28,6 +28,11 @@ struct video_t {
int coder;
} nv;
struct {
std::optional<int> preset;
std::optional<int> cavlc;
} qsv;
struct {
std::optional<int> quality_h264;
std::optional<int> quality_hevc;

View File

@@ -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);
}
}
}

View File

@@ -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_t> input, std::vector<std::uint8_t
case SCROLL_MAGIC_GEN5:
passthrough((PNV_SCROLL_PACKET)payload);
break;
case SS_HSCROLL_MAGIC:
passthrough((PSS_HSCROLL_PACKET)payload);
break;
case KEY_DOWN_EVENT_MAGIC:
case KEY_UP_EVENT_MAGIC:
passthrough(input, (PNV_KEYBOARD_PACKET)payload);

View File

@@ -51,6 +51,7 @@ MAIL(switch_display);
MAIL(touch_port);
MAIL(idr);
MAIL(rumble);
MAIL(hdr);
#undef MAIL
} // namespace mail
#endif // SUNSHINE_MAIN_H

View File

@@ -31,7 +31,8 @@
using namespace std::literals;
namespace nvhttp {
constexpr auto VERSION = "7.1.431.0";
// The negative 4th version number tells Moonlight that this is Sunshine
constexpr auto VERSION = "7.1.431.-1";
constexpr auto GFE_VERSION = "3.23.0.74";
namespace fs = std::filesystem;

View File

@@ -15,22 +15,37 @@
#include "src/thread_safe.h"
#include "src/utility.h"
extern "C" {
#include <moonlight-common-c/src/Limelight.h>
}
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<typename Char>
class basic_environment;
typedef basic_environment<char> 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<hwdevice_t>();
}
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_t> 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_t> display(mem_type_e hwdevice_type, const std::string &display_name, int framerate);
std::shared_ptr<display_t> 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<std::string> 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<deinit_t> 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);

View File

@@ -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<display_t> nvfbc_display(mem_type_e hwdevice_type, const std::string &display_name, int framerate) {
std::shared_ptr<display_t> 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<display_t> nvfbc_display(mem_type_e hwdevice_type, const std::st
auto display = std::make_shared<cuda::nvfbc::display_t>();
if(display->init(display_name, framerate)) {
if(display->init(display_name, config)) {
return nullptr;
}

View File

@@ -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];

View File

@@ -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<display_t> kms_display(mem_type_e hwdevice_type, const std::string &display_name, int framerate) {
std::shared_ptr<display_t> 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<kms::display_vram_t>(hwdevice_type);
if(!disp->init(display_name, framerate)) {
if(!disp->init(display_name, config)) {
return disp;
}
@@ -885,7 +915,7 @@ std::shared_ptr<display_t> kms_display(mem_type_e hwdevice_type, const std::stri
auto disp = std::make_shared<kms::display_ram_t>(hwdevice_type);
if(disp->init(display_name, framerate)) {
if(disp->init(display_name, config)) {
return nullptr;
}

View File

@@ -2,6 +2,7 @@
#include <dlfcn.h>
#include <fcntl.h>
#include <ifaddrs.h>
#include <netinet/udp.h>
#include <pwd.h>
#include <unistd.h>
@@ -14,6 +15,7 @@
#include "src/main.h"
#include "src/platform/common.h"
#include <boost/asio/ip/address.hpp>
#include <boost/process.hpp>
#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<deinit_t> 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<qos_t>(sockfd, level, option);
}
namespace source {
enum source_e : std::size_t {
#ifdef SUNSHINE_BUILD_CUDA
@@ -197,7 +418,7 @@ static std::bitset<source::MAX_FLAGS> sources;
#ifdef SUNSHINE_BUILD_CUDA
std::vector<std::string> nvfbc_display_names();
std::shared_ptr<display_t> nvfbc_display(mem_type_e hwdevice_type, const std::string &display_name, int framerate);
std::shared_ptr<display_t> 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<std::string> wl_display_names();
std::shared_ptr<display_t> wl_display(mem_type_e hwdevice_type, const std::string &display_name, int framerate);
std::shared_ptr<display_t> 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<std::string> kms_display_names();
std::shared_ptr<display_t> kms_display(mem_type_e hwdevice_type, const std::string &display_name, int framerate);
std::shared_ptr<display_t> 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<std::string> x11_display_names();
std::shared_ptr<display_t> x11_display(mem_type_e hwdevice_type, const std::string &display_name, int framerate);
std::shared_ptr<display_t> 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<std::string> display_names(mem_type_e hwdevice_type) {
return {};
}
std::shared_ptr<display_t> display(mem_type_e hwdevice_type, const std::string &display_name, int framerate) {
std::shared_ptr<display_t> 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

View File

@@ -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;

View File

@@ -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<display_t> wl_display(mem_type_e hwdevice_type, const std::string &display_name, int framerate) {
std::shared_ptr<display_t> 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<display_t> wl_display(mem_type_e hwdevice_type, const std::strin
if(hwdevice_type == platf::mem_type_e::vaapi) {
auto wlr = std::make_shared<wl::wlr_vram_t>();
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<display_t> wl_display(mem_type_e hwdevice_type, const std::strin
}
auto wlr = std::make_shared<wl::wlr_ram_t>();
if(wlr->init(hwdevice_type, display_name, framerate)) {
if(wlr->init(hwdevice_type, display_name, config)) {
return nullptr;
}

View File

@@ -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<display_t> x11_display(platf::mem_type_e hwdevice_type, const std::string &display_name, int framerate) {
std::shared_ptr<display_t> 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<display_t> 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<shm_attr_t>(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<display_t> x11_display(platf::mem_type_e hwdevice_type, const st
// Fallback
auto x11_disp = std::make_shared<x11_attr_t>(hwdevice_type);
if(x11_disp->init(display_name, framerate)) {
if(x11_disp->init(display_name, config)) {
return nullptr;
}

View File

@@ -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_t> display(platf::mem_type_e hwdevice_type, const std::string &display_name, int framerate) {
std::shared_ptr<display_t> 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_t> 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;

View File

@@ -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() };

View File

@@ -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<deinit_t> 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 {

View File

@@ -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);

View File

@@ -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);
};

View File

@@ -10,7 +10,7 @@
#include <d3dcommon.h>
#include <dwmapi.h>
#include <dxgi.h>
#include <dxgi1_2.h>
#include <dxgi1_6.h>
#include "src/platform/common.h"
#include "src/utility.h"
@@ -37,6 +37,7 @@ using adapter_t = util::safe_ptr<IDXGIAdapter1, Release<IDXGIAdapter
using output_t = util::safe_ptr<IDXGIOutput, Release<IDXGIOutput>>;
using output1_t = util::safe_ptr<IDXGIOutput1, Release<IDXGIOutput1>>;
using output5_t = util::safe_ptr<IDXGIOutput5, Release<IDXGIOutput5>>;
using output6_t = util::safe_ptr<IDXGIOutput6, Release<IDXGIOutput6>>;
using dup_t = util::safe_ptr<IDXGIOutputDuplication, Release<IDXGIOutputDuplication>>;
using texture2d_t = util::safe_ptr<ID3D11Texture2D, Release<ID3D11Texture2D>>;
using texture1d_t = util::safe_ptr<ID3D11Texture1D, Release<ID3D11Texture1D>>;
@@ -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_t> 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<DXGI_FORMAT> 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<DXGI_FORMAT> get_supported_sdr_capture_formats() = 0;
virtual std::vector<DXGI_FORMAT> 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_t> 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<img_t> alloc_img() override;
int dummy_img(img_t *img) override;
int complete_img(img_t *img, bool dummy) override;
std::vector<DXGI_FORMAT> get_supported_sdr_capture_formats() override;
std::vector<DXGI_FORMAT> 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<display_vram_t> {
public:
capture_e capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr<img_t> 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<img_t> alloc_img() override;
int dummy_img(img_t *img_base) override;
int complete_img(img_t *img_base, bool dummy) override;
std::vector<DXGI_FORMAT> get_supported_sdr_capture_formats() override;
std::vector<DXGI_FORMAT> 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<platf::hwdevice_t> 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<uint32_t> next_image_id;
};
} // namespace platf::dxgi

View File

@@ -6,16 +6,25 @@
#include <codecvt>
#include <initguid.h>
#include <boost/process.hpp>
// 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<std::chrono::microseconds>(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_t> display(mem_type_e hwdevice_type, const std::string &display_name, int framerate) {
std::shared_ptr<display_t> 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<dxgi::display_vram_t>();
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<dxgi::display_ram_t>();
if(!disp->init(framerate, display_name)) {
if(!disp->init(config, display_name)) {
return disp;
}
}
@@ -520,10 +853,15 @@ std::vector<std::string> display_names(mem_type_e) {
std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>, 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<std::string> 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));
}
}
}

View File

@@ -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<DXGI_FORMAT> 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<DXGI_FORMAT> 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;
}

View File

@@ -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<platf::display_t> 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<img_t> 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<uint32_t, encoder_img_ctx_t> img_ctx_map;
std::shared_ptr<platf::display_t> 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<std::uint8_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<platf::img_t> 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<DXGI_FORMAT> 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<DXGI_FORMAT> 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<platf::hwdevice_t> 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;

View File

@@ -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();

View File

@@ -4,6 +4,7 @@
#include <sstream>
#include <boost/algorithm/string.hpp>
#include <boost/asio/ip/address.hpp>
#include <boost/process.hpp>
// prevent clang format from "optimizing" the header include order
@@ -16,12 +17,27 @@
#include <userenv.h>
#include <dwmapi.h>
#include <timeapi.h>
#include <wlanapi.h>
// 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 <qos2.h>
#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<IP_ADAPTER_ADDRESSES>;
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<deinit_t> 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<qos_t>(flow_id);
}
} // namespace platf

View File

@@ -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();

View File

@@ -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));

View File

@@ -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<bool> idr_events;
std::unique_ptr<platf::deinit_t> qos;
} video;
struct {
@@ -302,6 +314,7 @@ struct session_t {
util::buffer_t<uint8_t *> shards_p;
audio_fec_packet_t fec_packet;
std::unique_ptr<platf::deinit_t> 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<video::hdr_info_t> hdr_queue;
} control;
safe::mail_raw_t::event_t<bool> 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<std::uint8_t,
sizeof(control_encrypted_t) + crypto::cipher::round_to_pkcs7_padded(sizeof(plaintext)) + crypto::cipher::tag_size>
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<session_t> alloc(config_t &config, crypto::aes_t &gcm_key, crypt
session->config = config;
session->control.rumble_queue = mail->queue<platf::rumble_t>(mail::rumble);
session->control.hdr_queue = mail->event<video::hdr_info_t>(mail::hdr);
session->control.iv = iv;
session->control.cipher = crypto::cipher::gcm_t {
gcm_key, false

View File

@@ -23,6 +23,8 @@ struct config_t {
int minRequiredFecPackets;
int featureFlags;
int controlProtocolType;
int audioQosType;
int videoQosType;
std::optional<int> gcmap;
};

View File

@@ -91,6 +91,10 @@ static std::string_view status_string(int status) {
}
std::unique_ptr<platf::deinit_t> 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<platf::deinit_t> 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));

View File

@@ -5,6 +5,7 @@
#include <thread>
extern "C" {
#include <libavutil/mastering_display_metadata.h>
#include <libswscale/swscale.h>
}
@@ -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<buffer_t, int> dxgi_make_hwdevice_ctx(platf::hwdevice_t *hwdevice_ctx);
util::Either<buffer_t, int> vaapi_make_hwdevice_ctx(platf::hwdevice_t *hwdevice_ctx);
util::Either<buffer_t, int> 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<option_t> common_options;
@@ -357,6 +373,7 @@ struct sync_session_ctx_t {
safe::mail_raw_t::event_t<bool> shutdown_event;
safe::mail_raw_t::queue_t<packet_t> packets;
safe::mail_raw_t::event_t<bool> idr_events;
safe::mail_raw_t::event_t<hdr_info_t> hdr_events;
safe::mail_raw_t::event_t<input::touch_port_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<capture_thread_sync_ctx_t>(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<encoder_t::option_t>({ "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<encoder_t::option_t>({ "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<encoder_t> encoders {
nvenc,
#endif
#ifdef _WIN32
quicksync,
amdvce,
#endif
#ifdef __linux__
@@ -629,15 +702,16 @@ static std::vector<encoder_t> encoders {
software
};
void reset_display(std::shared_ptr<platf::display_t> &disp, AVHWDeviceType type, const std::string &display_name, int framerate) {
void reset_display(std::shared_ptr<platf::display_t> &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<session_t> make_session(const encoder_t &encoder, const config_t &config, int width, int height, std::shared_ptr<platf::hwdevice_t> &&hwdevice) {
bool hardware = encoder.dev_type != AV_HWDEVICE_TYPE_NONE;
std::optional<session_t> make_session(platf::display_t *disp, const encoder_t &encoder, const config_t &config, int width, int height, std::shared_ptr<platf::hwdevice_t> &&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<session_t> 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<session_t> 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<session_t> 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<session_t> 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<platf::hwdevice_t> device;
@@ -1073,7 +1235,7 @@ std::optional<session_t> make_session(const encoder_t &encoder, const config_t &
if(!hwdevice->data) {
auto device_tmp = std::make_unique<swdevice_t>();
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<session_t> 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<platf::display_t> disp,
std::shared_ptr<platf::hwdevice_t> &&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<packet_t>(mail::video_packets);
auto idr_events = mail->event<bool>(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<sync_session_t> 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<hdr_info_raw_t>(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<sync_session_ctx_t>(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<input::touch_port_t>(mail::touch_port);
auto hdr_event = mail->event<hdr_info_t>(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<hdr_info_raw_t>(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<bool>(mail::shutdown),
mail::man->queue<packet_t>(mail::video_packets),
std::move(idr_events),
mail->event<hdr_info_t>(mail::hdr),
mail->event<input::touch_port_t>(mail::touch_port),
config,
1,
@@ -1510,7 +1681,7 @@ enum validate_flag_e {
};
int validate_config(std::shared_ptr<platf::display_t> &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<platf::display_t> &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;

View File

@@ -48,6 +48,16 @@ struct packet_raw_t {
using packet_t = std::unique_ptr<packet_raw_t>;
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<hdr_info_raw_t>;
struct config_t {
int width;
int height;

View File

@@ -523,6 +523,7 @@
<select id="encoder" class="form-select" v-model="config.encoder">
<option value>Autodetect</option>
<option value="nvenc" v-if="platform === 'windows' || platform === 'linux'">NVIDIA NVENC</option>
<option value="quicksync" v-if="platform === 'windows'">Intel QuickSync</option>
<option value="amdvce" v-if="platform === 'windows'">AMD AMF/VCE</option>
<option value="vaapi" v-if="platform === 'linux'">VA-API</option>
<option value="videotoolbox" v-if="platform === 'macos'">VideoToolbox</option>
@@ -695,6 +696,29 @@
</select>
</div>
</div>
<!--Intel Encoder Settings-->
<div v-if="currentTab === 'qsv'" class="config-page">
<div class="mb-3">
<label for="qsv_preset" class="form-label">QuickSync Preset</label>
<select id="qsv_preset" class="form-select" v-model="config.qsv_preset">
<option value="veryfast">fastest (lowest quality)</option>
<option value="faster">faster (lower quality)</option>
<option value="fast">fast (low quality)</option>
<option value="medium">medium (default)</option>
<option value="slow">slow (good quality)</option>
<option value="slower">slower (better quality)</option>
<option value="slowest">slowest (best quality)</option>
</select>
</div>
<div class="mb-3">
<label for="qsv_coder" class="form-label">QuickSync Coder (H264)</label>
<select id="qsv_coder" class="form-select" v-model="config.qsv_coder">
<option value="auto">auto -- let ffmpeg decide (default)</option>
<option value="cabac">cabac -- context adaptive binary arithmetic coding - higher quality</option>
<option value="cavlc">cavlc -- context adaptive variable-length coding - faster decode</option>
</select>
</div>
</div>
<!--AMD Encoder Settings-->
<div v-if="currentTab === 'amd'" class="config-page">
<!--Presets-->
@@ -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";

View File

@@ -18,7 +18,7 @@
]
},
{
"name": "Steam BigPicture",
"name": "Steam Big Picture",
"detached": [
"setsid steam steam://open/bigpicture"
],

View File

@@ -8,7 +8,7 @@
"image-path": "desktop.png"
},
{
"name": "Steam BigPicture",
"name": "Steam Big Picture",
"detached": [
"open steam://open/bigpicture"
],

View File

@@ -8,7 +8,7 @@
"image-path": "desktop.png"
},
{
"name": "Steam BigPicture",
"name": "Steam Big Picture",
"detached": [
"steam steam://open/bigpicture"
],

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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

1
third-party/nanors vendored Submodule

Submodule third-party/nanors added at 395e5ada44

View File

@@ -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})

153
tools/ddprobe.cpp Normal file
View File

@@ -0,0 +1,153 @@
#include <d3d11.h>
#include <dxgi1_2.h>
#include <codecvt>
#include <iostream>
#include <locale>
#include <string>
#include "src/utility.h"
using namespace std::literals;
namespace dxgi {
template<class T>
void Release(T *dxgi) {
dxgi->Release();
}
using factory1_t = util::safe_ptr<IDXGIFactory1, Release<IDXGIFactory1>>;
using adapter_t = util::safe_ptr<IDXGIAdapter1, Release<IDXGIAdapter1>>;
using output_t = util::safe_ptr<IDXGIOutput, Release<IDXGIOutput>>;
using output1_t = util::safe_ptr<IDXGIOutput1, Release<IDXGIOutput1>>;
using device_t = util::safe_ptr<ID3D11Device, Release<ID3D11Device>>;
using dup_t = util::safe_ptr<IDXGIOutputDuplication, Release<IDXGIOutputDuplication>>;
} // 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<std::codecvt_utf8_utf16<wchar_t>, 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;
}