diff --git a/.codeql-prebuild-cpp.sh b/.codeql-prebuild-cpp-Linux.sh similarity index 89% rename from .codeql-prebuild-cpp.sh rename to .codeql-prebuild-cpp-Linux.sh index c3e09d84e1a..d00c671ec2c 100644 --- a/.codeql-prebuild-cpp.sh +++ b/.codeql-prebuild-cpp-Linux.sh @@ -1,4 +1,5 @@ # install dependencies for C++ analysis +set -e sudo apt-get update -y sudo apt-get install -y \ @@ -54,3 +55,12 @@ sudo wget \ sudo chmod a+x /root/cuda.run sudo /root/cuda.run --silent --toolkit --toolkitpath=/usr --no-opengl-libs --no-man-page --no-drm sudo rm /root/cuda.run + +# build +mkdir -p build +cd build || exit 1 +cmake -G "Unix Makefiles" .. +make -j"$(nproc)" + +# skip autobuild +echo "skip_autobuild=true" >> "$GITHUB_OUTPUT" diff --git a/.codeql-prebuild-cpp-Windows.sh b/.codeql-prebuild-cpp-Windows.sh new file mode 100644 index 00000000000..034f07a8918 --- /dev/null +++ b/.codeql-prebuild-cpp-Windows.sh @@ -0,0 +1,34 @@ +# install dependencies for C++ analysis +set -e + +# update pacman +pacman --noconfirm -Suy + +# install dependencies +pacman --noconfirm -S \ + base-devel \ + 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-miniupnpc \ + mingw-w64-x86_64-nlohmann-json \ + mingw-w64-x86_64-nodejs \ + mingw-w64-x86_64-onevpl \ + mingw-w64-x86_64-openssl \ + mingw-w64-x86_64-opus \ + mingw-w64-x86_64-rust \ + mingw-w64-x86_64-toolchain + +# build +mkdir -p build +cd build || exit 1 +cmake -G "MinGW Makefiles" .. +mingw32-make -j"$(nproc)" + +# skip autobuild +echo "skip_autobuild=true" >> "$GITHUB_OUTPUT" diff --git a/.codeql-prebuild-cpp-macOS.sh b/.codeql-prebuild-cpp-macOS.sh new file mode 100644 index 00000000000..4e74c8599e5 --- /dev/null +++ b/.codeql-prebuild-cpp-macOS.sh @@ -0,0 +1,20 @@ +# install dependencies for C++ analysis +set -e + +# install dependencies +brew install \ + boost \ + cmake \ + miniupnpc \ + node \ + opus \ + pkg-config + +# build +mkdir -p build +cd build || exit 1 +cmake -G "Unix Makefiles" .. +make -j"$(sysctl -n hw.logicalcpu)" + +# skip autobuild +echo "skip_autobuild=true" >> "$GITHUB_OUTPUT" diff --git a/.dockerignore b/.dockerignore index 15191b977aa..a6ccb41e660 100644 --- a/.dockerignore +++ b/.dockerignore @@ -4,8 +4,11 @@ # do not ignore .git, needed for versioning !/.git +# do not ignore .rstcheck.cfg, needed to test building docs +!/.rstcheck.cfg + # ignore repo directories and files -docs/ +docker/ gh-pages-template/ scripts/ tools/ @@ -13,6 +16,7 @@ crowdin.yml # ignore dev directories build/ +cmake-*/ venv/ # ignore artifacts diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000000..1ccee08a107 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +# ensure dockerfiles are checked out with LF line endings +Dockerfile text eol=lf +*.dockerfile text eol=lf diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index fb74710eda1..b442fd746a5 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -165,16 +165,12 @@ jobs: remove-android: 'true' remove-haskell: 'true' remove-codeql: 'true' - remove-docker-images: 'false' + remove-docker-images: 'true' - name: Checkout uses: actions/checkout@v4 - - - name: Checkout Flathub Shared Modules - uses: actions/checkout@v4 with: - repository: flathub/shared-modules - path: build/shared-modules + submodules: recursive - name: Setup Dependencies Linux Flatpak run: | @@ -185,8 +181,10 @@ jobs: cmake \ flatpak \ qemu-user-static + sudo su $(whoami) -c "flatpak --user remote-add --if-not-exists flathub \ https://flathub.org/repo/flathub.flatpakrepo" + sudo su $(whoami) -c "flatpak --user install -y flathub \ org.flatpak.Builder \ org.freedesktop.Platform/${{ matrix.arch }}/${PLATFORM_VERSION} \ @@ -286,66 +284,54 @@ jobs: - name: Maximize build space uses: easimon/maximize-build-space@v8 with: - root-reserve-mb: 20480 + root-reserve-mb: 30720 remove-dotnet: 'true' remove-android: 'true' remove-haskell: 'true' remove-codeql: 'true' - remove-docker-images: 'false' + remove-docker-images: 'true' - name: Checkout uses: actions/checkout@v4 with: submodules: recursive + - name: Install wget + run: | + sudo apt-get update -y + sudo apt-get install -y \ + wget + + - name: Install CUDA + env: + CUDA_VERSION: 11.8.0 + CUDA_BUILD: 520.61.05 + timeout-minutes: 4 + run: | + url_base="https://developer.download.nvidia.com/compute/cuda/${CUDA_VERSION}/local_installers" + url="${url_base}/cuda_${CUDA_VERSION}_${CUDA_BUILD}_linux.run" + sudo wget -q -O /root/cuda.run ${url} + sudo chmod a+x /root/cuda.run + sudo /root/cuda.run --silent --toolkit --toolkitpath=/usr/local/cuda --no-opengl-libs --no-man-page --no-drm + sudo rm /root/cuda.run + - name: Setup Dependencies Linux + timeout-minutes: 5 run: | + # allow newer gcc sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y - if [[ ${{ matrix.dist }} == "18.04" ]]; then - # Ubuntu 18.04 packages - sudo add-apt-repository ppa:savoury1/boost-defaults-1.71 -y - - sudo apt-get update -y - sudo apt-get install -y \ - libboost-filesystem1.71-dev \ - libboost-locale1.71-dev \ - libboost-log1.71-dev \ - libboost-regex1.71-dev \ - libboost-program-options1.71-dev - - # Install cmake - wget https://cmake.org/files/v3.22/cmake-3.22.2-linux-x86_64.sh - chmod +x cmake-3.22.2-linux-x86_64.sh - mkdir /opt/cmake - ./cmake-3.22.2-linux-x86_64.sh --prefix=/opt/cmake --skip-license - ln --force --symbolic /opt/cmake/bin/cmake /usr/local/bin/cmake - cmake --version - - # install newer tar from focal... appimagelint fails on 18.04 without this - echo "original tar version" - tar --version - wget -O tar.deb http://security.ubuntu.com/ubuntu/pool/main/t/tar/tar_1.30+dfsg-7ubuntu0.20.04.3_amd64.deb - sudo apt-get -y install -f ./tar.deb - echo "new tar version" - tar --version - else - # Ubuntu 20.04+ packages - sudo apt-get update -y - sudo apt-get install -y \ - cmake \ - libboost-filesystem-dev \ - libboost-locale-dev \ - libboost-log-dev \ - libboost-program-options-dev - fi - sudo apt-get install -y \ build-essential \ + cmake \ gcc-10 \ g++-10 \ libayatana-appindicator3-dev \ libavdevice-dev \ + libboost-filesystem-dev \ + libboost-locale-dev \ + libboost-log-dev \ + libboost-program-options-dev \ libcap-dev \ libcurl4-openssl-dev \ libdrm-dev \ @@ -367,7 +353,7 @@ jobs: libxfixes-dev \ libxrandr-dev \ libxtst-dev \ - wget + python3 # clean apt cache sudo apt-get clean @@ -382,25 +368,29 @@ jobs: --slave /usr/bin/gcc-ar gcc-ar /usr/bin/gcc-ar-10 \ --slave /usr/bin/gcc-ranlib gcc-ranlib /usr/bin/gcc-ranlib-10 - # Install CUDA - sudo wget \ - https://developer.download.nvidia.com/compute/cuda/11.8.0/local_installers/cuda_11.8.0_520.61.05_linux.run \ - --progress=bar:force:noscroll -q --show-progress -O /root/cuda.run - sudo chmod a+x /root/cuda.run - sudo /root/cuda.run --silent --toolkit --toolkitpath=/usr --no-opengl-libs --no-man-page --no-drm - sudo rm /root/cuda.run + - name: Setup python + id: python + uses: actions/setup-python@v5 + with: + python-version: '3.11' - name: Build Linux env: BRANCH: ${{ github.head_ref || github.ref_name }} BUILD_VERSION: ${{ needs.check_changelog.outputs.next_version_bare }} COMMIT: ${{ github.event.pull_request.head.sha || github.sha }} + timeout-minutes: 5 run: | + echo "nproc: $(nproc)" + mkdir -p build mkdir -p artifacts cd build - cmake -DCMAKE_BUILD_TYPE=Release \ + cmake \ + -DBUILD_WERROR=ON \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_CUDA_COMPILER:PATH=/usr/local/cuda/bin/nvcc \ -DCMAKE_INSTALL_PREFIX=/usr \ -DSUNSHINE_ASSETS_DIR=share/sunshine \ -DSUNSHINE_EXECUTABLE_PATH=/usr/bin/sunshine \ @@ -410,20 +400,7 @@ jobs: -DSUNSHINE_ENABLE_CUDA=ON \ ${{ matrix.EXTRA_ARGS }} \ .. - make -j ${nproc} - - - name: Package Linux - CPACK - # todo - this is no longer used - if: ${{ matrix.type == 'cpack' }} - working-directory: build - run: | - cpack -G DEB - mv ./cpack_artifacts/Sunshine.deb ../artifacts/sunshine-${{ matrix.dist }}.deb - - if [[ ${{ matrix.dist }} == "20.04" ]]; then - cpack -G RPM - mv ./cpack_artifacts/Sunshine.rpm ../artifacts/sunshine.rpm - fi + make -j $(expr $(nproc) - 1) # use all but one core - name: Set AppImage Version if: | @@ -450,12 +427,12 @@ jobs: # AppImage # https://docs.appimage.org/packaging-guide/index.html - wget https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage + wget -q https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage chmod +x linuxdeploy-x86_64.AppImage # https://github.com/linuxdeploy/linuxdeploy-plugin-gtk sudo apt-get install libgtk-3-dev librsvg2-dev -y - wget https://raw.githubusercontent.com/linuxdeploy/linuxdeploy-plugin-gtk/master/linuxdeploy-plugin-gtk.sh + wget -q https://raw.githubusercontent.com/linuxdeploy/linuxdeploy-plugin-gtk/master/linuxdeploy-plugin-gtk.sh chmod +x linuxdeploy-plugin-gtk.sh export DEPLOY_GTK_VERSION=3 @@ -473,14 +450,17 @@ jobs: # permissions chmod +x ../artifacts/sunshine.AppImage + - name: Delete cuda + # free up space on the runner + run: | + sudo rm -rf /usr/local/cuda + - name: Verify AppImage if: ${{ matrix.type == 'AppImage' }} run: | wget https://github.com/TheAssassin/appimagelint/releases/download/continuous/appimagelint-x86_64.AppImage chmod +x appimagelint-x86_64.AppImage - # rm -rf ~/.cache/appimagelint/ - ./appimagelint-x86_64.AppImage ./artifacts/sunshine.AppImage - name: Upload Artifacts @@ -489,6 +469,52 @@ jobs: name: sunshine-linux-${{ matrix.type }}-${{ matrix.dist }} path: artifacts/ + - name: Install test deps + run: | + sudo apt-get update -y + sudo apt-get install -y \ + doxygen \ + graphviz \ + python3-venv \ + x11-xserver-utils \ + xvfb + + # clean apt cache + sudo apt-get clean + sudo rm -rf /var/lib/apt/lists/* + + - name: Run tests + id: test + working-directory: build/tests + run: | + export DISPLAY=:1 + Xvfb ${DISPLAY} -screen 0 1024x768x24 & + + ./test_sunshine --gtest_color=yes + + - name: Generate gcov report + # any except canceled or skipped + if: always() && (steps.test.outcome == 'success' || steps.test.outcome == 'failure') + id: test_report + working-directory: build + run: | + ${{ steps.python.outputs.python-path }} -m pip install gcovr + ${{ steps.python.outputs.python-path }} -m gcovr -r .. \ + --exclude '.*tests/.*' \ + --exclude '.*tests/.*' \ + --xml-pretty \ + -o coverage.xml + + - name: Upload coverage + # any except canceled or skipped + if: always() && (steps.test_report.outcome == 'success') + uses: codecov/codecov-action@v4 + with: + fail_ci_if_error: true + files: ./build/coverage.xml + flags: ${{ runner.os }} + token: ${{ secrets.CODECOV_TOKEN }} + - name: Create/Update GitHub Release if: ${{ needs.setup_release.outputs.create_release == 'true' }} uses: ncipollo/release-action@v1 @@ -503,122 +529,114 @@ jobs: discussionCategory: announcements prerelease: ${{ needs.setup_release.outputs.pre_release }} - build_mac: - name: MacOS - runs-on: macos-11 + build_mac_brew: needs: [check_changelog, setup_release] - env: - BOOST_VERSION: 1.83.0 + strategy: + fail-fast: false # false to test all, true to fail entire job if any fail + matrix: + include: + # https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#standard-github-hosted-runners-for-public-repositories + # while GitHub has larger macOS runners, they are not available for our repos :( + - os_version: "12" + release: true + - os_version: "13" + - os_version: "14" + name: Homebrew (macOS-${{ matrix.os_version }}) + runs-on: macos-${{ matrix.os_version }} steps: - name: Checkout uses: actions/checkout@v4 - with: - submodules: recursive - - name: Setup Dependencies MacOS + - name: Setup Dependencies Homebrew run: | # install dependencies using homebrew - brew install cmake curl miniupnpc node openssl opus pkg-config - - # fix openssl header not found - # ln -sf /usr/local/opt/openssl/include/openssl /usr/local/include/openssl - - # by installing boost from source, several headers cannot be found... - # the above commented out link only works if boost is installed from homebrew... does not make sense - ln -sf $(find /usr/local/Cellar -type d -name "openssl" -path "*/openssl@3/*/include" | head -n 1) \ - /usr/local/include/openssl - - # fix opus header not found - ln -sf $(find /usr/local/Cellar -type d -name "opus" -path "*/opus/*/include" | head -n 1) \ - /usr/local/include/opus - - # fix miniupnpc header not found - ln -sf $(find /usr/local/Cellar -type d -name "miniupnpc" -path "*/miniupnpc/*/include" | head -n 1) \ - /usr/local/include/miniupnpc - - - name: Install Boost - # installing boost from homebrew takes 30 minutes in a GitHub runner - run: | - export BOOST_ROOT=${HOME}/boost-${BOOST_VERSION} - - # install boost - wget \ - https://github.com/boostorg/boost/releases/download/boost-${BOOST_VERSION}/boost-${BOOST_VERSION}.tar.gz \ - --progress=bar:force:noscroll -q --show-progress - tar xf boost-${BOOST_VERSION}.tar.gz - cd boost-${BOOST_VERSION} - - # libdir should be set by --prefix but isn't - ./bootstrap.sh \ - --prefix=${BOOST_ROOT} \ - --libdir=${BOOST_ROOT}/lib \ - --with-libraries=locale,log,program_options,system,thread - ./b2 headers - ./b2 install \ - --prefix=${BOOST_ROOT} \ - --libdir=${BOOST_ROOT}/lib \ - -j$(sysctl -n hw.ncpu) \ - link=shared,static \ - variant=release \ - cxxflags=-std=c++14 \ - cxxflags=-stdlib=libc++ \ - linkflags=-stdlib=libc++ - - # put boost in cmake prefix path - echo "BOOST_ROOT=${BOOST_ROOT}" >> ${GITHUB_ENV} - - - name: Build MacOS - env: - BRANCH: ${{ github.head_ref || github.ref_name }} - BUILD_VERSION: ${{ needs.check_changelog.outputs.next_version_bare }} - COMMIT: ${{ github.event.pull_request.head.sha || github.sha }} + brew install cmake + + - name: Configure formula run: | + # variables for formula + branch=${GITHUB_HEAD_REF} + + # check the branch variable + if [ -z "$branch" ] + then + echo "This is a PUSH event" + clone_url=${{ github.event.repository.clone_url }} + branch="${{ github.ref_name }}" + default_branch="${{ github.event.repository.default_branch }}" + else + echo "This is a PR event" + clone_url=${{ github.event.pull_request.head.repo.clone_url }} + branch="${{ github.event.pull_request.head.ref }}" + default_branch="${{ github.event.pull_request.head.repo.default_branch }}" + fi + echo "Branch: ${branch}" + echo "Clone URL: ${clone_url}" + mkdir build cd build - cmake -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_INSTALL_PREFIX=/usr \ - -DSUNSHINE_ASSETS_DIR=local/sunshine/assets \ - -DSUNSHINE_EXECUTABLE_PATH=/usr/bin/sunshine \ + cmake \ + -DGITHUB_BRANCH="${branch}" \ + -DGITHUB_CLONE_URL="${clone_url}" \ + -DGITHUB_DEFAULT_BRANCH="${default_branch}" \ + -DSUNSHINE_CONFIGURE_HOMEBREW=ON \ + -DSUNSHINE_CONFIGURE_ONLY=ON \ .. - make -j $(sysctl -n hw.ncpu) - - - name: Package MacOS - run: | - mkdir -p artifacts - cd build + cd .. - # package - cpack -G DragNDrop - mv ./cpack_artifacts/Sunshine.dmg ../artifacts/sunshine.dmg + # copy formula to artifacts + mkdir -p homebrew + cp -f ./build/sunshine.rb ./homebrew/sunshine.rb - # cpack -G Bundle - # mv ./cpack_artifacts/Sunshine.dmg ../artifacts/sunshine-bundle.dmg + # testing + cat ./homebrew/sunshine.rb - name: Upload Artifacts + if: ${{ matrix.release }} uses: actions/upload-artifact@v4 with: - name: sunshine-macos - path: artifacts/ + name: sunshine-homebrew + path: homebrew/ - - name: Create/Update GitHub Release - if: ${{ needs.setup_release.outputs.create_release == 'true' }} - uses: ncipollo/release-action@v1 + - name: Should Publish Homebrew Formula + id: homebrew_publish + run: | + PUBLISH=false + if [[ \ + "${{ matrix.release }}" == "true" && \ + "${{ github.repository_owner }}" == "LizardByte" && \ + "${{ needs.setup_release.outputs.create_release }}" == "true" && \ + "${{ github.ref }}" == "refs/heads/master" \ + ]]; then + PUBLISH=true + fi + + echo "publish=${PUBLISH}" >> $GITHUB_OUTPUT + + - name: Validate and Publish Homebrew Formula + uses: LizardByte/homebrew-release-action@v2024.409.24405 with: - name: ${{ needs.setup_release.outputs.release_name }} - tag: ${{ needs.setup_release.outputs.release_tag }} - commit: ${{ needs.setup_release.outputs.release_commit }} - artifacts: "*artifacts/*" + formula_file: ${{ github.workspace }}/homebrew/sunshine.rb + git_email: ${{ secrets.GH_BOT_EMAIL }} + git_username: ${{ secrets.GH_BOT_NAME }} + publish: ${{ steps.homebrew_publish.outputs.publish }} token: ${{ secrets.GH_BOT_TOKEN }} - allowUpdates: true - body: ${{ needs.setup_release.outputs.release_body }} - discussionCategory: announcements - prerelease: ${{ needs.setup_release.outputs.pre_release }} build_mac_port: - name: Macports needs: [check_changelog, setup_release] - runs-on: macos-11 + strategy: + fail-fast: false # false to test all, true to fail entire job if any fail + matrix: + include: + # https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#standard-github-hosted-runners-for-public-repositories + # while GitHub has larger macOS runners, they are not available for our repos :( + - os_version: "12" + release: true + - os_version: "13" + - os_version: "14" + name: Macports (macOS-${{ matrix.os_version }}) + runs-on: macos-${{ matrix.os_version }} steps: - name: Checkout @@ -642,6 +660,12 @@ jobs: # install dependencies using homebrew brew install cmake + - name: Setup python + id: python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Configure Portfile run: | # variables for Portfile @@ -699,6 +723,7 @@ jobs: - name: Build port env: subportlist: ${{ steps.subportlist.outputs.subportlist }} + id: build run: | subport="Sunshine" @@ -720,14 +745,103 @@ jobs: "$subport" echo "::endgroup::" + - name: Build Logs + if: always() + run: | + logfile="/opt/local/var/macports/logs/_Users_runner_work_Sunshine_Sunshine_ports_multimedia_Sunshine/Sunshine/main.log" + cat "$logfile" + sudo mv "${logfile}" "${logfile}.bak" + - name: Upload Artifacts + if: ${{ matrix.release }} uses: actions/upload-artifact@v4 with: name: sunshine-macports path: artifacts/ + - name: Fix screen capture permissions + if: ${{ matrix.os_version != 12 }} # macOS-12 is okay + # can be removed if the following is fixed in the runner image + # https://github.com/actions/runner-images/issues/9529 + # https://github.com/actions/runner-images/pull/9530 + run: | + # https://apple.stackexchange.com/questions/362865/macos-list-apps-authorized-for-full-disk-access + + # permissions for screen capture + values="'kTCCServiceScreenCapture','/opt/off/opt/runner/provisioner/provisioner',1,2,4,1,NULL,NULL,0,'UNUSED',NULL,0,1687786159" + if [[ "${{ matrix.os_version }}" == "14" ]]; then + # TCC access table in Sonoma has extra 4 columns: pid, pid_version, boot_uuid, last_reminded + values="${values},NULL,NULL,'UNUSED',${values##*,}" + fi + + # system and user databases + dbPaths=( + "/Library/Application Support/com.apple.TCC/TCC.db" + "$HOME/Library/Application Support/com.apple.TCC/TCC.db" + ) + + sqlQuery="INSERT OR IGNORE INTO access VALUES($values);" + + for dbPath in "${dbPaths[@]}"; do + echo "Column names for $dbPath" + echo "-------------------" + sudo sqlite3 "$dbPath" "PRAGMA table_info(access);" + echo "Current permissions for $dbPath" + echo "-------------------" + sudo sqlite3 "$dbPath" "SELECT * FROM access WHERE service='kTCCServiceScreenCapture';" + sudo sqlite3 "$dbPath" "$sqlQuery" + echo "Updated permissions for $dbPath" + echo "-------------------" + sudo sqlite3 "$dbPath" "SELECT * FROM access WHERE service='kTCCServiceScreenCapture';" + done + + - name: Run tests + id: test + timeout-minutes: 10 + run: | + sudo port test "Sunshine" + + - name: Test Logs + if: always() + run: | + logfile="/opt/local/var/macports/logs/_Users_runner_work_Sunshine_Sunshine_ports_multimedia_Sunshine/Sunshine/main.log" + cat "$logfile" + + - name: Generate gcov report + # any except canceled or skipped + if: always() && (steps.test.outcome == 'success' || steps.test.outcome == 'failure') + id: test_report + working-directory: + /opt/local/var/macports/build/_Users_runner_work_Sunshine_Sunshine_ports_multimedia_Sunshine/Sunshine/work + run: | + base_dir=$(pwd) + build_dir=${base_dir}/build + + # get the directory name that starts with Sunshine-* + dir=$(ls -d Sunshine-*) + + cd ${build_dir} + ${{ steps.python.outputs.python-path }} -m pip install gcovr + sudo ${{ steps.python.outputs.python-path }} -m gcovr -r ../${dir} \ + --exclude '.*${dir}/tests/.*' \ + --exclude '.*${dir}/third-party/.*' \ + --gcov-object-directory $(pwd) \ + --verbose \ + --xml-pretty \ + -o ${{ github.workspace }}/build/coverage.xml + + - name: Upload coverage + # any except canceled or skipped + if: always() && (steps.test_report.outcome == 'success') + uses: codecov/codecov-action@v4 + with: + fail_ci_if_error: false # todo: re-enable this when action is fixed + files: ./build/coverage.xml + flags: ${{ runner.os }}-${{ matrix.os_version }} + token: ${{ secrets.CODECOV_TOKEN }} + - name: Create/Update GitHub Release - if: ${{ needs.setup_release.outputs.create_release == 'true' }} + if: ${{ needs.setup_release.outputs.create_release == 'true' && matrix.release }} uses: ncipollo/release-action@v1 with: name: ${{ needs.setup_release.outputs.release_name }} @@ -751,6 +865,110 @@ jobs: with: submodules: recursive + - name: Prepare tests + id: prepare-tests + if: false # todo: DirectX11 is not available, so even software encoder fails + run: | + # function to download and extract a zip file + function DownloadAndExtract { + param ( + [string]$Uri, + [string]$OutFile + ) + + $maxRetries = 5 + $retryCount = 0 + $success = $false + + while (-not $success -and $retryCount -lt $maxRetries) { + $retryCount++ + Write-Host "Downloading $Uri to $OutFile, attempt $retryCount of $maxRetries" + try { + Invoke-WebRequest -Uri $Uri -OutFile $OutFile + $success = $true + } catch { + Write-Host "Attempt $retryCount of $maxRetries failed with error: $($_.Exception.Message). Retrying..." + Start-Sleep -Seconds 5 + } + } + + if (-not $success) { + Write-Host "Failed to download the file after $maxRetries attempts." + exit 1 + } + + # use .NET to get the base name of the file + $baseName = (Get-Item $OutFile).BaseName + + # Extract the zip file + Expand-Archive -Path $OutFile -DestinationPath $baseName + } + + # virtual display driver + DownloadAndExtract ` + -Uri "https://www.amyuni.com/downloads/usbmmidd_v2.zip" ` + -OutFile "usbmmidd_v2.zip" + + # install + Set-Location -Path usbmmidd_v2/usbmmidd_v2 + ./deviceinstaller64 install usbmmidd.inf usbmmidd + + # create the virtual display + ./deviceinstaller64 enableidd 1 + + # move up a directory + Set-Location -Path ../.. + + # install devcon + DownloadAndExtract ` + -Uri "https://github.com/Drawbackz/DevCon-Installer/releases/download/1.4-rc/Devcon.Installer.zip" ` + -OutFile "Devcon.Installer.zip" + Set-Location -Path Devcon.Installer + # hash needs to match OS version + # https://github.com/Drawbackz/DevCon-Installer/blob/master/devcon_sources.json + Start-Process -FilePath "./Devcon Installer.exe" -Wait -ArgumentList ` + 'install', ` + '-hash', '54004C83EE34F6A55380528A8B29F4C400E61FBB947A19E0AB9E5A193D7D961E', ` + '-addpath', ` + '-update', ` + '-dir', 'C:\Windows\System32' + + # disable Hyper-V Video + # https://stackoverflow.com/a/59490940 + C:\Windows\System32\devcon.exe disable "VMBUS\{da0a7802-e377-4aac-8e77-0558eb1073f8}" + + # move up a directory + Set-Location -Path .. + + # multi monitor tool + DownloadAndExtract ` + -Uri "http://www.nirsoft.net/utils/multimonitortool-x64.zip" ` + -OutFile "multimonitortool.zip" + + # enable the virtual display + # http://www.nirsoft.net/utils/multi_monitor_tool.html + Set-Location -Path multimonitortool + + # Original Hyper-V is \\.\DISPLAY1, it will recreate itself as \\.\DISPLAY6 (or something higher than 2) + # USB Mobile Monitor Virtual Display is \\.\DISPLAY2 + + # these don't seem to work if not using runAs + # todo: do they work if not using runAs? + Start-Process powershell -Verb runAs -ArgumentList '-Command ./MultiMonitorTool.exe /enable \\.\DISPLAY2' + Start-Process powershell -Verb runAs -ArgumentList '-Command ./MultiMonitorTool.exe /SetPrimary \\.\DISPLAY2' + + # wait a few seconds + Start-Sleep -s 5 + + # list monitors + ./MultiMonitorTool.exe /stext monitor_list.txt + + # wait a few seconds + Start-Sleep -s 5 + + # print the monitor list + Get-Content -Path monitor_list.txt + - name: Setup Dependencies Windows uses: msys2/setup-msys2@v2 with: @@ -758,13 +976,16 @@ jobs: install: >- base-devel diffutils + doxygen 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-graphviz mingw-w64-x86_64-miniupnpc + mingw-w64-x86_64-nlohmann-json mingw-w64-x86_64-nodejs mingw-w64-x86_64-nsis mingw-w64-x86_64-onevpl @@ -775,6 +996,24 @@ jobs: wget yasm + - name: Setup python + # use this instead of msys2 python due to known issues using wheels, https://www.msys2.org/docs/python/ + id: setup-python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Python Path + id: python-path + shell: msys2 {0} + run: | + # replace backslashes with double backslashes + python_path=$(echo "${{ steps.setup-python.outputs.python-path }}" | sed 's/\\/\\\\/g') + + # step output + echo "python-path=${python_path}" + echo "python-path=${python_path}" >> $GITHUB_OUTPUT + - name: Build Windows shell: msys2 {0} env: @@ -784,8 +1023,12 @@ jobs: run: | mkdir build cd build - cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo \ + cmake \ + -DBUILD_WERROR=ON \ + -DCMAKE_BUILD_TYPE=RelWithDebInfo \ -DSUNSHINE_ASSETS_DIR=assets \ + -DTESTS_PYTHON_EXECUTABLE='${{ steps.python-path.outputs.python-path }}' \ + -DTESTS_SOFTWARE_ENCODER_UNAVAILABLE='skip' \ -G "MinGW Makefiles" \ .. mingw32-make -j$(nproc) @@ -804,14 +1047,49 @@ jobs: mv ./cpack_artifacts/Sunshine.exe ../artifacts/sunshine-windows-installer.exe mv ./cpack_artifacts/Sunshine.zip ../artifacts/sunshine-windows-portable.zip + - name: Run tests + id: test + shell: msys2 {0} + working-directory: build/tests + run: | + ./test_sunshine.exe --gtest_color=yes + + - name: Generate gcov report + # any except canceled or skipped + if: always() && (steps.test.outcome == 'success' || steps.test.outcome == 'failure') + id: test_report + shell: msys2 {0} + working-directory: build + run: | + ${{ steps.python-path.outputs.python-path }} -m pip install gcovr + ${{ steps.python-path.outputs.python-path }} -m gcovr -r .. \ + --exclude '.*tests/.*' \ + --exclude '.*tests/.*' \ + --xml-pretty \ + -o coverage.xml + + - name: Upload coverage + # any except canceled or skipped + if: always() && (steps.test_report.outcome == 'success') + uses: codecov/codecov-action@v4 + with: + fail_ci_if_error: true + files: ./build/coverage.xml + flags: ${{ runner.os }} + token: ${{ secrets.CODECOV_TOKEN }} + - name: Package Windows Debug Info working-directory: build run: | - # save the original binaries with debug info + # use .dbg file extension for binaries to avoid confusion with real packages + Get-ChildItem -File -Recurse | ` + % { Rename-Item -Path $_.PSPath -NewName $_.Name.Replace(".exe",".dbg") } + + # save the binaries with debug info 7z -r ` "-xr!CMakeFiles" ` "-xr!cpack_artifacts" ` - a "../artifacts/sunshine-debuginfo-win32.zip" "*.exe" + a "../artifacts/sunshine-win32-debuginfo.7z" "*.dbg" - name: Upload Artifacts uses: actions/upload-artifact@v4 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index ae5248763ec..ff12034f0c6 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -16,7 +16,7 @@ on: - cron: '00 12 * * 0' # every Sunday at 12:00 UTC concurrency: - group: ${{ github.workflow }}-${{ github.ref }} + group: "${{ github.workflow }}-${{ github.ref }}" cancel-in-progress: true jobs: @@ -57,10 +57,25 @@ jobs: console.log(`Remapping language: ${key} to ${remap_languages[key.toLowerCase()]}`) key = remap_languages[key.toLowerCase()] } - if (supported_languages.includes(key.toLowerCase()) && - !matrix['include'].includes({"language": key.toLowerCase()})) { + if (supported_languages.includes(key.toLowerCase())) { console.log(`Found supported language: ${key}`) - matrix['include'].push({"language": key.toLowerCase()}) + let osList = ['ubuntu-latest']; + if (key.toLowerCase() === 'swift') { + osList = ['macos-latest']; + } else if (key.toLowerCase() === 'cpp') { + osList = ['macos-latest', 'ubuntu-latest', 'windows-latest']; + } + for (let os of osList) { + // set name for matrix + if (osList.length == 1) { + name = key.toLowerCase() + } else { + name = `${key.toLowerCase()}, ${os}` + } + + // add to matrix + matrix['include'].push({"language": key.toLowerCase(), "os": os, "name": name}) + } } } @@ -84,10 +99,15 @@ jobs: } analyze: - name: Analyze + name: Analyze (${{ matrix.name }}) if: ${{ needs.languages.outputs.continue == 'true' }} + defaults: + run: + shell: ${{ matrix.os == 'windows-latest' && 'msys2 {0}' || 'bash' }} + env: + GITHUB_CODEQL_BUILD: true needs: [languages] - runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + runs-on: ${{ matrix.os || 'ubuntu-latest' }} timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} permissions: actions: read @@ -100,6 +120,7 @@ jobs: steps: - name: Maximize build space + if: runner.os == 'Linux' uses: easimon/maximize-build-space@v8 with: root-reserve-mb: 20480 @@ -114,6 +135,12 @@ jobs: with: submodules: recursive + - name: Setup msys2 + if: runner.os == 'Windows' + uses: msys2/setup-msys2@v2 + with: + update: true + # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v3 @@ -129,16 +156,20 @@ jobs: # Pre autobuild # create a file named .codeql-prebuild-${{ matrix.language }}.sh in the root of your repository + # create a file named .codeql-build-${{ matrix.language }}.sh in the root of your repository - name: Prebuild + id: prebuild run: | - # check if .qodeql-prebuild-${{ matrix.language }}.sh exists - if [ -f "./.codeql-prebuild-${{ matrix.language }}.sh" ]; then - echo "Running .codeql-prebuild-${{ matrix.language }}.sh" - ./.codeql-prebuild-${{ matrix.language }}.sh + # check if prebuild script exists + filename=".codeql-prebuild-${{ matrix.language }}-${{ runner.os }}.sh" + if [ -f "./${filename}" ]; then + echo "Running prebuild script: ${filename}" + ./${filename} fi # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). - name: Autobuild + if: steps.prebuild.outputs.skip_autobuild != 'true' uses: github/codeql-action/autobuild@v3 - name: Perform CodeQL Analysis diff --git a/.github/workflows/localize.yml b/.github/workflows/localize.yml index f9cd64859bb..815b67585ce 100644 --- a/.github/workflows/localize.yml +++ b/.github/workflows/localize.yml @@ -77,7 +77,7 @@ jobs: run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT - name: Create/Update Pull Request - uses: peter-evans/create-pull-request@v5 + uses: peter-evans/create-pull-request@v6 with: add-paths: | locale/*.po diff --git a/.gitmodules b/.gitmodules index 67522f450ed..4efc01425bd 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,7 +1,19 @@ +[submodule "packaging/linux/flatpak/deps/org.flatpak.Builder.BaseApp"] + path = packaging/linux/flatpak/deps/org.flatpak.Builder.BaseApp + url = https://github.com/flathub/org.flatpak.Builder.BaseApp + branch = branch/23.08 +[submodule "packaging/linux/flatpak/deps/shared-modules"] + path = packaging/linux/flatpak/deps/shared-modules + url = https://github.com/flathub/shared-modules + branch = master [submodule "third-party/build-deps"] path = third-party/build-deps url = https://github.com/LizardByte/build-deps.git branch = dist +[submodule "third-party/googletest"] + path = third-party/googletest + url = https://github.com/google/googletest/ + branch = v1.14.x [submodule "third-party/moonlight-common-c"] path = third-party/moonlight-common-c url = https://github.com/moonlight-stream/moonlight-common-c.git @@ -10,10 +22,6 @@ path = third-party/nanors url = https://github.com/sleepybishop/nanors.git branch = master -[submodule "third-party/nlohmann_json"] - path = third-party/nlohmann_json - url = https://github.com/nlohmann/json - branch = master [submodule "third-party/nv-codec-headers"] path = third-party/nv-codec-headers url = https://github.com/FFmpeg/nv-codec-headers diff --git a/CHANGELOG.md b/CHANGELOG.md index bbeb4028382..0ca2b370d21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,208 @@ # Changelog +## [0.23.0] - 2024-04-06 +Attention, this release contains critical security fixes. Please update as soon as possible. + +**Breaking** +- (Linux) Drop support for Ubuntu 20.04 +- (Linux) No longer provide arm64 rpm packages, due to extreme compile time on GitHub hosted runners + +**Fixed** +- (Network) Ensure unpairing takes effect without restart +- (Capture/Linux) Fix logical comparison of texture size +- (Service/Windows) Quote the path to sunshinesvc.exe when launching the termination helper + +**Added** +- (WebUI) Localization support +- (Capture/Linux) Populate host latency for kmx/x11 grab +- (Capture/Windows) AMF rate control improvements +- (Linux) Add support for Ubuntu 24.04 (x86_64 only) + +**Dependencies** +- Bump rstcheck from 6.2.0 to 6.2.1 +- Bump org.flatpak.Builder.BaseApp from 644487f to 6e295e6 +- Bump ffmpeg +- Bump @fortawesome/fontawesome-free from 6.5.1 to 6.5.2 + +**Misc** +- (Style) Refactored video encoder declarations +- (CI) Refactored Linux build in CI +- (CI) Added unit testing and code coverage +- (Docs/macOS) Update curl command for Portfile install +- (Style) Refactor logging initialization + + +## [0.22.2] - 2024-03-15 +**Fixed** +- (Tray/Windows) Fix broken system tray icon on some systems +- (Linux) Fix crash when XDG_CONFIG_HOME or CONFIGURATION_DIRECTORY are set +- (Linux) Fix config migration across filesystems and with non-existent parent directories + +## [0.22.1] - 2024-03-13 +**Breaking** +- (ArchLinux) Drop support for standalone PKGBUILD files. Use the binary Arch package or install via AUR instead. +- (macOS) Drop support for experimental dmg package. Use Homebrew or MacPorts instead. + +**Added** +- (macOS) Added Homebrew support + +**Changed** +- (Process/Windows) The working directory is now searched first when the command contains a relative path +- (ArchLinux) The kmsgrab capture backend is now compiled by default to support Wayland capture on non-wlroots-based compositors +- (Capture/Linux) X11 capture is now preferred over kmsgrab for cards that lack atomic modesetting support to ensure cursor capture works +- (Capture/Linux) Kmsgrab will only choose NVENC by default if the display is connected to the Nvidia GPU to avoid possible EGL import failures + +**Fixed** +- (Config) Fix unsupported resolution error with some Moonlight clients +- (Capture/Windows) Fix crash when streaming Ryujinx, Red Alert 2, and other apps that use unusually sized monochrome cursors +- (Capture/Linux) Fix crash in KMS cursor capture when running on Arch-based distros +- (Capture/Linux) Fix crash if CUDA GPU has a PCI ID with hexadecimal digits greater than 9 +- (Process/Windows) Fix starting apps when the working directory is enclosed in quotes +- (Process/Windows) Fix process tree tracking when the app is launched via a cmd.exe trampoline +- (Installer/Windows) Fix slow operation during ViGEmBus installation that may cause the installer to appear stuck +- (Build/macOS) Fix issues building on macOS 13 and 14 +- (Build/Linux) Fix missing install script in the Arch binary package +- (Build/Linux) Fix missing optional dependencies in the Arch binary package +- (Build/Linux) Ensure correct Arch pkg is published to GitHub releases +- (Capture/Linux) Fix mismatched case and unhandled exception in CUDA device lookup +- (Config) Add missing resolution to default config ui +- (Linux) Fix udev rules for uinput access not working until after reboot +- (Linux) Fix wrong path in desktop files +- (Tray) Cache icons to avoid possible DRM issues +- (Tray) Fix attempt to update tray icon after it was destroyed +- (Linux) Migrate old config files to new location if env SUNSHINE_MIGRATE_CONFIG=1 is set (automatically set for Flatpak) +- (Linux/Fedora) Re-enable CUDA support and bump to 12.4.0 + +**Misc** +- (Build/Windows) Adjust Windows debuginfo artifact to reduce confusion with real release binaries + +## [0.22.0] - 2024-03-03 +**Breaking** +- (Network) Clients must now be paired with the host before they can use Wake-on-LAN +- (Build/Linux) Drop Fedora 37 support + +**Added** +- (Input/Linux) Add native/pen touch support for Linux +- (Capture/Linux) Add HDR streaming support for Linux using KMS capture backend +- (Capture/Linux) Add KMS capture support for Nvidia GPUs running Wayland +- (Network) Add support for full E2E stream encryption, configurable for LAN and WAN independently +- (Process) Add process group tracking to automatically handle launchers that spawn other child processes +- (Capture/Windows) Add setting for controlling GPU power saving and encoding latency tradeoff for NVENC +- (Capture/Windows) Add additional encoding settings for NVENC +- (Process/Windows) Add experimental support for launching URLs and other non-exe files +- (Capture/Windows) Add setting to allow use of slower HEVC encoding on older Intel GPUs +- (Input/Windows) Add settings to control automatic gamepad type selection heuristics +- (Input/Windows) Add setting to allow DS4 back/select button to trigger touchpad click +- (Input) Add setting to disable high resolution scrolling and native pen/touch support +- (Network) Add support for certificates types other than RSA-2048 +- (Build/Linux) Add Fedora 39 docker image and rpm package +- (Capture/Linux) Display monitor indexes in logs for wlroots and KMS capture backends +- (UI) Add link to logs inside fatal error container +- (UI) Add hash handler and ids for all configuration categories and settings + +**Changed** +- (UI) Several configuration options have been moved to more suitable locations +- (Network) Client-selected bitrate is now adjusted for FEC percentage and other stream overhead +- (Capture/Linux) Improve VAAPI encoding performance on Intel GPUs +- (Capture) Connection establishment delay is reduced by eliminating many encoder probing operations +- (Process) Graceful termination of running processes is attempted first when stopping apps +- (Capture) Improve software encoding performance by enabling multi-threaded color conversion +- (Capture) Adjust default CPU thread count for software encoding from 1 to 2 for improved performance +- (Steam/Windows) Modernized the default Steam app shortcut to avoid depending on Steam's install location and support app termination +- (Linux) Updated desktop files +- (Config) Add 2560x1440 to default resolutions +- (Network) Use the configured ping timeout for the initial launch event timeout +- (UI) Migrate UI to Vite and Vue3, and various UX improvements +- (Logging) Adjust wording and severity of some log messages +- (Build) Use a single submodule for ffmpeg +- (Install/Windows) Skip ViGEmBus installation if a supported version is already installed +- (Build/Linux) Optionally, allow using the system installation of wayland-protocols +- (Build/Linux) Make vaapi optional +- (Windows) Replace boost::json with nlohmann/json + +**Fixed** +- (Network/Windows) Fix auto-discovery of hosts by iOS/tvOS clients +- (Network) Fix immediate connection termination when streaming over some Internet connections +- (Capture/Linux) Fix missing mouse cursor when using KMS capture on a GPU with hardware cursor support +- (Capture/Windows) Add workaround for Nvidia driver bug causing Sunshine to crash when RTX HDR is globally enabled +- (Capture/Windows) Add workaround for AMD driver bug on pre-RDNA GPUs causing hardware encoding failure +- (Capture/Windows) Reintroduce support for NVENC on older Nvidia GPU drivers (v456.71-v522.25) +- (Capture/Windows) Fix encoding on old Intel GPUs that don't support low-power H.264 encoding +- (Capture/Linux) Fix GL errors or corrupt video output on GPUs that use aux planes such as Intel Arc +- (Capture/Linux) Fix GL errors or corrupt video output on GPUs that use DRM modifiers on YUV buffers +- (Input/Windows) Fix non-functional duplicate controllers appearing in rare cases +- (Input/Windows) Avoid triggering crash in ViGEmBus when the system goes to sleep +- (Input/Linux) Fix scrolling in applications that don't support high-resolution scrolling +- (Input/Linux) Fix absolute mouse input being interpreted as touch input +- (Capture/Linux) Fix wlroots capture causing GL errors and crashes +- (Capture/Linux) Fix wlroots capture failing when the display scale factor was not 1 +- (Capture/Linux) Fix excessive CPU usage when using wlroots capture backend +- (Capture/Linux) Fix capture of virtual displays created by the amdgpu kernel driver +- (Audio/Windows) Fix audio capture failures on Insider Preview versions of Windows 11 +- (Capture/Windows) Fix incorrect portrait mode rotation +- (Capture/Windows) Fix capture recovery when a driver update/crash occurs while streaming +- (Capture/Windows) Fix delay displaying UAC dialogs when the mouse cursor is not moving +- (Capture/Linux) Fix corrupt video output or stream disconnections if the display resolution changes while streaming +- (Capture/Linux) Fix color of aspect ratio padding in the capture image with VAAPI +- (Capture/Linux) Fix NVENC initialization error when using X11 capture with some GPUs +- (Tray/Linux) Fix random crash when the tray icon is updating +- (Network) Fix QoS tagging when running in IPv4+IPv6 mode +- (Process) Fix termination of child processes upon app quit when the parent has already terminated +- (Process) Fix notification of graceful termination to connected clients when Sunshine quits +- (Capture) Fix corrupt output or green aspect-ratio padding when using software encoding with some video resolutions +- (Windows) Fix crashes when processing file paths or other strings with certain non-ASCII characters +- (Capture) Ensure user supplied framerates are used exclusively in place of pre-defined framerates +- (CMake/Linux) Skip including unnecessary headers +- (Capture/Linux) Replace vaTerminate method with dl handle +- (Capture/Linux) Fix capture when DRM is enabled and x11 is disabled +- (Tray) Use PROJECT_NAME definition for tooltip +- (CMake) Use GNUInstallDirs to install data and lib directories +- (macOS) Replace deprecated code +- (API) Allow trailing slashes in on API endpoints +- (API) Add additional pin validation +- (Linux) Use XDG spec for fetching config directory +- (CMake) Properly find evdev +- (Config) Properly save global_prep_cmd and fps settings + +**Dependencies** +- Bump third-party/wayland-protocols from 681c33c to 46f201b +- Bump third-party/nv-codec-headers from 9402b5a to 22441b5 +- Bump third-party/nanors from 395e5ad to e9e242e +- Bump third-party/Simple-Web-Server from 2f29926 to 27b41f5 +- Bump ffmpeg +- Bump third-party/tray from 2664388 to 2bf1c61 +- Bump actions/setup-python from 4 to 5 +- Bump actions/upload-artifact from 3 to 4 +- Bump @fortawesome/fontawesome-free from 6.4.2 to 6.5.1 +- Bump babel from 2.13.0 to 2.14.0 +- Move miniupnpc from submodule to system installed package +- Bump furo from 2023.9.10 to 2024.1.29 +- Bump third-party/moonlight-common-c from f78f213 to cbd0ec1 +- Bump third-party/ViGEmClient from 1920260 to 8d71f67 +- Bump peter-evans/create-pull-request from 5 to 6 +- Bump bootstrap from 5.3.2 to 5.3.3 + +**Misc** +- (Build) Update global workflows +- (Docs/Linux) Add example for setting custom resolution with NVIDIA +- (Docs) Fix broken links +- (Docs/Windows) Add information about disk permissions +- (Docs) Fix failing images +- (Docs) Use glob pattern to match source code docs +- (CI/macOS) Install boost from source +- (Docs) Add reset credentials examples for unique packages +- (Docs) Refactor and general cleanup +- (Docs) Cross-reference config settings to the UI +- (Docs/Docker) Add podman notes +- (Build) Use CMAKE_SOURCE_DIR property everywhere +- (Build/Docker) Add docker toolchain file for CLion +- (macOS) Various code style fixes +- (Deps) Alphabetize git submodules +- (Docs/Examples) Update URI examples +- (Refactor) Refactored some code in preparation for unit testing implementation +- (CMake) Add option to skip cuda inheriting compile options +- (CMake) Add option to error build on warnings + ## [0.21.0] - 2023-10-15 **Added** - (Input) Add support for automatically selecting the emulated controller type based on the physical controller connected to the client @@ -592,3 +795,7 @@ settings. In v0.17.0, games now run under your user account without elevated pri [0.19.1]: https://github.com/LizardByte/Sunshine/releases/tag/v0.19.1 [0.20.0]: https://github.com/LizardByte/Sunshine/releases/tag/v0.20.0 [0.21.0]: https://github.com/LizardByte/Sunshine/releases/tag/v0.21.0 +[0.22.0]: https://github.com/LizardByte/Sunshine/releases/tag/v0.22.0 +[0.22.1]: https://github.com/LizardByte/Sunshine/releases/tag/v0.22.1 +[0.22.2]: https://github.com/LizardByte/Sunshine/releases/tag/v0.22.2 +[0.23.0]: https://github.com/LizardByte/Sunshine/releases/tag/v0.23.0 diff --git a/CMakeLists.txt b/CMakeLists.txt index 53d8bde2881..180a9911bdc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,10 +1,11 @@ cmake_minimum_required(VERSION 3.18) # `CMAKE_CUDA_ARCHITECTURES` requires 3.18 +# set_source_files_properties requires 3.18 # todo - set this conditionally # todo - set version to 0.0.0 once confident in automated versioning -project(Sunshine VERSION 0.21.0 - DESCRIPTION "Sunshine is a self-hosted game stream host for Moonlight." +project(Sunshine VERSION 0.23.0 + DESCRIPTION "Self-hosted game stream host for Moonlight" HOMEPAGE_URL "https://app.lizardbyte.dev/Sunshine") set(PROJECT_LICENSE "GPL-3.0") @@ -28,6 +29,9 @@ include(${CMAKE_MODULE_PATH}/prep/build_version.cmake) # cmake build flags include(${CMAKE_MODULE_PATH}/prep/options.cmake) +# initial prep +include(${CMAKE_MODULE_PATH}/prep/init.cmake) + # configure special package files, such as sunshine.desktop, Flatpak manifest, Portfile , etc. include(${CMAKE_MODULE_PATH}/prep/special_package_configuration.cmake) diff --git a/README.rst b/README.rst index fbc78658668..5ea90014848 100644 --- a/README.rst +++ b/README.rst @@ -17,69 +17,48 @@ System Requirements **Minimum Requirements** -+------------+------------------------------------------------------------+ -| GPU | AMD: VCE 1.0 or higher, see `obs-amd hardware support`_ | -| +------------------------------------------------------------+ -| | Intel: VAAPI-compatible, see: `VAAPI hardware support`_ | -| +------------------------------------------------------------+ -| | Nvidia: NVENC enabled cards, see `nvenc support matrix`_ | -+------------+------------------------------------------------------------+ -| CPU | AMD: Ryzen 3 or higher | -| +------------------------------------------------------------+ -| | Intel: Core i3 or higher | -+------------+------------------------------------------------------------+ -| RAM | 4GB or more | -+------------+------------------------------------------------------------+ -| OS | Windows: 10+ (Windows Server not supported) | -| +------------------------------------------------------------+ -| | macOS: 11.7+ | -| +------------------------------------------------------------+ -| | Linux/Debian: 11 (bullseye) | -| +------------------------------------------------------------+ -| | Linux/Fedora: 37+ | -| +------------------------------------------------------------+ -| | Linux/Ubuntu: 20.04+ (focal) | -+------------+------------------------------------------------------------+ -| Network | Host: 5GHz, 802.11ac | -| +------------------------------------------------------------+ -| | Client: 5GHz, 802.11ac | -+------------+------------------------------------------------------------+ +.. csv-table:: + :widths: 15, 60 + + "GPU", "AMD: VCE 1.0 or higher, see: `obs-amd hardware support `_" + "", "Intel: VAAPI-compatible, see: `VAAPI hardware support `_" + "", "Nvidia: NVENC enabled cards, see: `nvenc support matrix `_" + "CPU", "AMD: Ryzen 3 or higher" + "", "Intel: Core i3 or higher" + "RAM", "4GB or more" + "OS", "Windows: 10+ (Windows Server does not support virtual gamepads)" + "", "macOS: 12+" + "", "Linux/Debian: 11 (bullseye)" + "", "Linux/Fedora: 38+" + "", "Linux/Ubuntu: 22.04+ (jammy)" + "Network", "Host: 5GHz, 802.11ac" + "", "Client: 5GHz, 802.11ac" **4k Suggestions** -+------------+------------------------------------------------------------+ -| GPU | AMD: Video Coding Engine 3.1 or higher | -| +------------------------------------------------------------+ -| | Intel: HD Graphics 510 or higher | -| +------------------------------------------------------------+ -| | Nvidia: GeForce GTX 1080 or higher | -+------------+------------------------------------------------------------+ -| CPU | AMD: Ryzen 5 or higher | -| +------------------------------------------------------------+ -| | Intel: Core i5 or higher | -+------------+------------------------------------------------------------+ -| Network | Host: CAT5e ethernet or better | -| +------------------------------------------------------------+ -| | Client: CAT5e ethernet or better | -+------------+------------------------------------------------------------+ +.. csv-table:: + :widths: 15, 60 + + "GPU", "AMD: Video Coding Engine 3.1 or higher" + "", "Intel: HD Graphics 510 or higher" + "", "Nvidia: GeForce GTX 1080 or higher" + "CPU", "AMD: Ryzen 5 or higher" + "", "Intel: Core i5 or higher" + "Network", "Host: CAT5e ethernet or better" + "", "Client: CAT5e ethernet or better" **HDR Suggestions** -+------------+------------------------------------------------------------+ -| GPU | AMD: Video Coding Engine 3.4 or higher | -| +------------------------------------------------------------+ -| | Intel: UHD Graphics 730 or higher | -| +------------------------------------------------------------+ -| | Nvidia: Pascal-based GPU (GTX 10-series) or higher | -+------------+------------------------------------------------------------+ -| CPU | AMD: todo | -| +------------------------------------------------------------+ -| | Intel: todo | -+------------+------------------------------------------------------------+ -| Network | Host: CAT5e ethernet or better | -| +------------------------------------------------------------+ -| | Client: CAT5e ethernet or better | -+------------+------------------------------------------------------------+ +.. csv-table:: + :widths: 15, 60 + + "GPU", "AMD: Video Coding Engine 3.4 or higher" + "", "Intel: UHD Graphics 730 or higher" + "", "Nvidia: Pascal-based GPU (GTX 10-series) or higher" + "CPU", "AMD: todo" + "", "Intel: todo" + "Network", "Host: CAT5e ethernet or better" + "", "Client: CAT5e ethernet or better" Integrations ------------ @@ -96,6 +75,10 @@ Integrations :alt: Read the Docs :target: http://sunshinestream.readthedocs.io/ +.. image:: https://img.shields.io/codecov/c/gh/LizardByte/Sunshine?token=SMGXQ5NVMJ&style=for-the-badge&logo=codecov&label=codecov + :alt: Codecov + :target: https://codecov.io/gh/LizardByte/Sunshine + Support ------- @@ -122,7 +105,3 @@ Stats .. image:: https://img.shields.io/github/stars/lizardbyte/sunshine.svg?logo=github&style=for-the-badge :alt: GitHub stars :target: https://github.com/LizardByte/Sunshine - -.. _nvenc support matrix: https://developer.nvidia.com/video-encode-and-decode-gpu-support-matrix-new -.. _obs-amd hardware support: https://github.com/obsproject/obs-amd-encoder/wiki/Hardware-Support -.. _VAAPI hardware support: https://www.intel.com/content/www/us/en/developer/articles/technical/linuxmedia-vaapi.html diff --git a/cmake/compile_definitions/common.cmake b/cmake/compile_definitions/common.cmake index e51dbf56526..6e038295847 100644 --- a/cmake/compile_definitions/common.cmake +++ b/cmake/compile_definitions/common.cmake @@ -3,7 +3,25 @@ list(APPEND SUNSHINE_COMPILE_OPTIONS -Wall -Wno-sign-compare) # Wall - enable all warnings +# Werror - treat warnings as errors +# Wno-maybe-uninitialized/Wno-uninitialized - disable warnings for maybe uninitialized variables # Wno-sign-compare - disable warnings for signed/unsigned comparisons +if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + # GCC specific compile options + + # GCC 12 and higher will complain about maybe-uninitialized + if(CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 12) + list(APPEND SUNSHINE_COMPILE_OPTIONS -Wno-maybe-uninitialized) + endif() +elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + # Clang specific compile options + + # Clang doesn't actually complain about this this, so disabling for now + # list(APPEND SUNSHINE_COMPILE_OPTIONS -Wno-uninitialized) +endif() +if(BUILD_WERROR) + list(APPEND SUNSHINE_COMPILE_OPTIONS -Werror) +endif() # setup assets directory if(NOT SUNSHINE_ASSETS_DIR) @@ -28,7 +46,7 @@ file(GLOB NVENC_SOURCES CONFIGURE_DEPENDS "src/nvenc/*.cpp" "src/nvenc/*.h") list(APPEND PLATFORM_TARGET_FILES ${NVENC_SOURCES}) configure_file("${CMAKE_SOURCE_DIR}/src/version.h.in" version.h @ONLY) -include_directories("${CMAKE_CURRENT_BINARY_DIR}") +include_directories("${CMAKE_CURRENT_BINARY_DIR}") # required for importing version.h set(SUNSHINE_TARGET_FILES "${CMAKE_SOURCE_DIR}/third-party/nanors/rs.c" @@ -45,8 +63,12 @@ set(SUNSHINE_TARGET_FILES "${CMAKE_SOURCE_DIR}/src/uuid.h" "${CMAKE_SOURCE_DIR}/src/config.h" "${CMAKE_SOURCE_DIR}/src/config.cpp" + "${CMAKE_SOURCE_DIR}/src/entry_handler.cpp" + "${CMAKE_SOURCE_DIR}/src/entry_handler.h" "${CMAKE_SOURCE_DIR}/src/file_handler.cpp" "${CMAKE_SOURCE_DIR}/src/file_handler.h" + "${CMAKE_SOURCE_DIR}/src/globals.cpp" + "${CMAKE_SOURCE_DIR}/src/globals.h" "${CMAKE_SOURCE_DIR}/src/logging.cpp" "${CMAKE_SOURCE_DIR}/src/logging.h" "${CMAKE_SOURCE_DIR}/src/main.cpp" @@ -88,11 +110,6 @@ set(SUNSHINE_TARGET_FILES "${CMAKE_SOURCE_DIR}/src/stat_trackers.cpp" ${PLATFORM_TARGET_FILES}) -set_source_files_properties("${CMAKE_SOURCE_DIR}/src/upnp.cpp" PROPERTIES COMPILE_FLAGS -Wno-pedantic) - -set_source_files_properties("${CMAKE_SOURCE_DIR}/third-party/nanors/rs.c" - PROPERTIES COMPILE_FLAGS "-include deps/obl/autoshim.h -ftree-vectorize") - if(NOT SUNSHINE_ASSETS_DIR_DEF) set(SUNSHINE_ASSETS_DIR_DEF "${SUNSHINE_ASSETS_DIR}") endif() @@ -112,15 +129,6 @@ include_directories( ${PLATFORM_INCLUDE_DIRS} ) -string(TOUPPER "x${CMAKE_BUILD_TYPE}" BUILD_TYPE) -if("${BUILD_TYPE}" STREQUAL "XDEBUG") - if(WIN32) - set_source_files_properties("${CMAKE_SOURCE_DIR}/src/nvhttp.cpp" PROPERTIES COMPILE_FLAGS -O2) - endif() -else() - add_definitions(-DNDEBUG) -endif() - list(APPEND SUNSHINE_EXTERNAL_LIBRARIES ${MINIUPNP_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT} @@ -130,5 +138,4 @@ list(APPEND SUNSHINE_EXTERNAL_LIBRARIES ${Boost_LIBRARIES} ${OPENSSL_LIBRARIES} ${CURL_LIBRARIES} - ${PLATFORM_LIBRARIES} - nlohmann_json::nlohmann_json) + ${PLATFORM_LIBRARIES}) diff --git a/cmake/compile_definitions/linux.cmake b/cmake/compile_definitions/linux.cmake index 613a090947d..5647dc6b3ea 100644 --- a/cmake/compile_definitions/linux.cmake +++ b/cmake/compile_definitions/linux.cmake @@ -8,10 +8,6 @@ if(${SUNSHINE_BUILD_APPIMAGE}) string(REPLACE "${CMAKE_INSTALL_PREFIX}" ".${CMAKE_INSTALL_PREFIX}" SUNSHINE_ASSETS_DIR_DEF ${SUNSHINE_ASSETS_DIR}) endif() -if(NOT DEFINED SUNSHINE_EXECUTABLE_PATH) - set(SUNSHINE_EXECUTABLE_PATH "sunshine") -endif() - # cuda set(CUDA_FOUND OFF) if(${SUNSHINE_ENABLE_CUDA}) @@ -120,6 +116,17 @@ elseif(NOT LIBDRM_FOUND) message(WARNING "Missing libcap") endif() +# evdev +pkg_check_modules(PC_EVDEV libevdev REQUIRED) +find_path(EVDEV_INCLUDE_DIR libevdev/libevdev.h + HINTS ${PC_EVDEV_INCLUDE_DIRS} ${PC_EVDEV_INCLUDEDIR}) +find_library(EVDEV_LIBRARY + NAMES evdev libevdev) +if(EVDEV_INCLUDE_DIR AND EVDEV_LIBRARY) + include_directories(SYSTEM ${EVDEV_INCLUDE_DIR}) + list(APPEND PLATFORM_LIBRARIES ${EVDEV_LIBRARY}) +endif() + # vaapi if(${SUNSHINE_ENABLE_VAAPI}) find_package(Libva) @@ -192,13 +199,13 @@ endif() # tray icon if(${SUNSHINE_ENABLE_TRAY}) - pkg_check_modules(APPINDICATOR appindicator3-0.1) + pkg_check_modules(APPINDICATOR ayatana-appindicator3-0.1) if(APPINDICATOR_FOUND) - list(APPEND SUNSHINE_DEFINITIONS TRAY_LEGACY_APPINDICATOR=1) + list(APPEND SUNSHINE_DEFINITIONS TRAY_AYATANA_APPINDICATOR=1) else() - pkg_check_modules(APPINDICATOR ayatana-appindicator3-0.1) + pkg_check_modules(APPINDICATOR appindicator3-0.1) if(APPINDICATOR_FOUND) - list(APPEND SUNSHINE_DEFINITIONS TRAY_AYATANA_APPINDICATOR=1) + list(APPEND SUNSHINE_DEFINITIONS TRAY_LEGACY_APPINDICATOR=1) endif () endif() pkg_check_modules(LIBNOTIFY libnotify) @@ -241,13 +248,11 @@ list(APPEND PLATFORM_TARGET_FILES list(APPEND PLATFORM_LIBRARIES Boost::dynamic_linking dl - evdev numa pulse pulse-simple) include_directories( SYSTEM - /usr/include/libevdev-1.0 "${CMAKE_SOURCE_DIR}/third-party/nv-codec-headers/include" "${CMAKE_SOURCE_DIR}/third-party/glad/include") diff --git a/cmake/compile_definitions/windows.cmake b/cmake/compile_definitions/windows.cmake index e3729927fc1..26e89a20b82 100644 --- a/cmake/compile_definitions/windows.cmake +++ b/cmake/compile_definitions/windows.cmake @@ -6,6 +6,9 @@ enable_language(RC) set(CMAKE_RC_COMPILER windres) set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static") +# gcc complains about misleading indentation in some mingw includes +list(APPEND SUNSHINE_COMPILE_OPTIONS -Wno-misleading-indentation) + # curl add_definitions(-DCURL_STATICLIB) include_directories(SYSTEM ${CURL_STATIC_INCLUDE_DIRS}) @@ -26,10 +29,6 @@ file(GLOB NVPREFS_FILES CONFIGURE_DEPENDS # vigem include_directories(SYSTEM "${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/include") -set_source_files_properties("${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/src/ViGEmClient.cpp" - PROPERTIES COMPILE_DEFINITIONS "UNICODE=1;ERROR_INVALID_DEVICE_OBJECT_PARAMETER=650") -set_source_files_properties("${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/src/ViGEmClient.cpp" - PROPERTIES COMPILE_FLAGS "-Wno-unknown-pragmas -Wno-misleading-indentation -Wno-class-memaccess") # sunshine icon if(NOT DEFINED SUNSHINE_ICON_PATH) @@ -75,6 +74,7 @@ list(PREPEND PLATFORM_LIBRARIES avrt iphlpapi shlwapi + PkgConfig::NLOHMANN_JSON ${CURL_STATIC_LIBRARIES}) if(SUNSHINE_ENABLE_TRAY) diff --git a/cmake/dependencies/common.cmake b/cmake/dependencies/common.cmake index a1f35128005..29bed10e5cd 100644 --- a/cmake/dependencies/common.cmake +++ b/cmake/dependencies/common.cmake @@ -19,9 +19,6 @@ pkg_check_modules(CURL REQUIRED libcurl) pkg_check_modules(MINIUPNP miniupnpc REQUIRED) include_directories(SYSTEM ${MINIUPNP_INCLUDE_DIRS}) -# nlohmann_json -add_subdirectory("${CMAKE_SOURCE_DIR}/third-party/nlohmann_json") - # ffmpeg pre-compiled binaries if(WIN32) if(NOT CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64") diff --git a/cmake/dependencies/windows.cmake b/cmake/dependencies/windows.cmake index 376c44da65a..a7ecce3963d 100644 --- a/cmake/dependencies/windows.cmake +++ b/cmake/dependencies/windows.cmake @@ -2,3 +2,6 @@ set(Boost_USE_STATIC_LIBS ON) # cmake-lint: disable=C0103 find_package(Boost 1.71.0 COMPONENTS locale log filesystem program_options REQUIRED) + +# nlohmann_json +pkg_check_modules(NLOHMANN_JSON nlohmann_json REQUIRED IMPORTED_TARGET) diff --git a/cmake/packaging/common.cmake b/cmake/packaging/common.cmake index ad3f9bc0682..c7c5b3a5cc9 100644 --- a/cmake/packaging/common.cmake +++ b/cmake/packaging/common.cmake @@ -12,10 +12,18 @@ set(CPACK_PACKAGE_ICON ${PROJECT_SOURCE_DIR}/sunshine.png) set(CPACK_PACKAGE_FILE_NAME "${CMAKE_PROJECT_NAME}") set(CPACK_STRIP_FILES YES) -#install common assets +# install common assets install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/common/assets/" DESTINATION "${SUNSHINE_ASSETS_DIR}" PATTERN "web" EXCLUDE) +# copy assets to build directory, for running without install +file(GLOB_RECURSE ALL_ASSETS + RELATIVE "${SUNSHINE_SOURCE_ASSETS_DIR}/common/assets/" "${SUNSHINE_SOURCE_ASSETS_DIR}/common/assets/*") +list(FILTER ALL_ASSETS EXCLUDE REGEX "^web/.*$") # Filter out the web directory +foreach(asset ${ALL_ASSETS}) # Copy assets to build directory, excluding the web directory + file(COPY "${SUNSHINE_SOURCE_ASSETS_DIR}/common/assets/${asset}" + DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/assets") +endforeach() # install built vite assets install(DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/assets/web" diff --git a/cmake/packaging/linux.cmake b/cmake/packaging/linux.cmake index 8563414a40e..4d9cfbcec72 100644 --- a/cmake/packaging/linux.cmake +++ b/cmake/packaging/linux.cmake @@ -2,8 +2,11 @@ install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/linux/assets/" DESTINATION "${SUNSHINE_ASSETS_DIR}") +# copy assets to build directory, for running without install +file(COPY "${SUNSHINE_SOURCE_ASSETS_DIR}/linux/assets/" + DESTINATION "${CMAKE_BINARY_DIR}/assets") if(${SUNSHINE_BUILD_APPIMAGE} OR ${SUNSHINE_BUILD_FLATPAK}) - install(FILES "${SUNSHINE_SOURCE_ASSETS_DIR}/linux/misc/85-sunshine.rules" + install(FILES "${SUNSHINE_SOURCE_ASSETS_DIR}/linux/misc/60-sunshine.rules" DESTINATION "${SUNSHINE_ASSETS_DIR}/udev/rules.d") install(FILES "${CMAKE_CURRENT_BINARY_DIR}/sunshine.service" DESTINATION "${SUNSHINE_ASSETS_DIR}/systemd/user") @@ -11,7 +14,7 @@ else() find_package(Systemd) find_package(Udev) - install(FILES "${SUNSHINE_SOURCE_ASSETS_DIR}/linux/misc/85-sunshine.rules" + install(FILES "${SUNSHINE_SOURCE_ASSETS_DIR}/linux/misc/60-sunshine.rules" DESTINATION "${UDEV_RULES_INSTALL_DIR}") install(FILES "${CMAKE_CURRENT_BINARY_DIR}/sunshine.service" DESTINATION "${SYSTEMD_USER_UNIT_INSTALL_DIR}") diff --git a/cmake/packaging/macos.cmake b/cmake/packaging/macos.cmake index f7c3a518c97..a16fdb66a26 100644 --- a/cmake/packaging/macos.cmake +++ b/cmake/packaging/macos.cmake @@ -10,15 +10,16 @@ if(SUNSHINE_PACKAGE_MACOS) # todo set(MAC_PREFIX "${CMAKE_PROJECT_NAME}.app/Contents") set(INSTALL_RUNTIME_DIR "${MAC_PREFIX}/MacOS") - install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/macos/assets/" - DESTINATION "${SUNSHINE_ASSETS_DIR}") - install(TARGETS sunshine BUNDLE DESTINATION . COMPONENT Runtime RUNTIME DESTINATION ${INSTALL_RUNTIME_DIR} COMPONENT Runtime) else() - install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/macos/assets/" - DESTINATION "${SUNSHINE_ASSETS_DIR}") install(FILES "${SUNSHINE_SOURCE_ASSETS_DIR}/macos/misc/uninstall_pkg.sh" DESTINATION "${SUNSHINE_ASSETS_DIR}") endif() + +install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/macos/assets/" + DESTINATION "${SUNSHINE_ASSETS_DIR}") +# copy assets to build directory, for running without install +file(COPY "${SUNSHINE_SOURCE_ASSETS_DIR}/macos/assets/" + DESTINATION "${CMAKE_BINARY_DIR}/assets") diff --git a/cmake/packaging/unix.cmake b/cmake/packaging/unix.cmake index d6a4ae93575..bacbfc910de 100644 --- a/cmake/packaging/unix.cmake +++ b/cmake/packaging/unix.cmake @@ -1,8 +1,6 @@ # unix specific packaging # put anything here that applies to both linux and macos -include(GNUInstallDirs) - # return here if building a macos package if(SUNSHINE_PACKAGE_MACOS) return() diff --git a/cmake/packaging/windows.cmake b/cmake/packaging/windows.cmake index 1ea1afe191d..bbd497ee3a0 100644 --- a/cmake/packaging/windows.cmake +++ b/cmake/packaging/windows.cmake @@ -39,6 +39,9 @@ install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/windows/misc/gamepad/" install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/windows/assets/" DESTINATION "${SUNSHINE_ASSETS_DIR}" COMPONENT assets) +# copy assets to build directory, for running without install +file(COPY "${SUNSHINE_SOURCE_ASSETS_DIR}/windows/assets/" + DESTINATION "${CMAKE_BINARY_DIR}/assets") # set(CPACK_NSIS_MUI_HEADERIMAGE "") # TODO: image should be 150x57 bmp set(CPACK_PACKAGE_ICON "${CMAKE_SOURCE_DIR}\\\\sunshine.ico") @@ -57,8 +60,8 @@ SET(CPACK_NSIS_EXTRA_INSTALL_COMMANDS nsExec::ExecToLog 'icacls \\\"$INSTDIR\\\" /reset' nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\migrate-config.bat\\\"' nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\add-firewall-rule.bat\\\"' - nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\install-service.bat\\\"' nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\install-gamepad.bat\\\"' + nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\install-service.bat\\\"' nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\autostart-service.bat\\\"' NoController: ") diff --git a/cmake/prep/init.cmake b/cmake/prep/init.cmake new file mode 100644 index 00000000000..93e8b597721 --- /dev/null +++ b/cmake/prep/init.cmake @@ -0,0 +1,9 @@ +if (WIN32) +elseif (APPLE) +elseif (UNIX) + include(GNUInstallDirs) + + if(NOT DEFINED SUNSHINE_EXECUTABLE_PATH) + set(SUNSHINE_EXECUTABLE_PATH "sunshine") + endif() +endif () diff --git a/cmake/prep/options.cmake b/cmake/prep/options.cmake index 7a8d728ba47..1555036eeb0 100644 --- a/cmake/prep/options.cmake +++ b/cmake/prep/options.cmake @@ -1,3 +1,12 @@ +option(BUILD_TESTS "Build tests" ON) +option(TESTS_ENABLE_PYTHON_TESTS "Enable Python tests" ON) + +# DirectX11 is not available in GitHub runners, so even software encoding fails +set(TESTS_SOFTWARE_ENCODER_UNAVAILABLE "fail" + CACHE STRING "How to handle unavailable software encoders in tests. 'fail/skip'") + +option(BUILD_WERROR "Enable -Werror flag." OFF) + # if this option is set, the build will exit after configuring special package configuration files option(SUNSHINE_CONFIGURE_ONLY "Configure special files only, then exit." OFF) @@ -6,7 +15,19 @@ option(SUNSHINE_REQUIRE_TRAY "Require system tray icon. Fail the build if tray r option(SUNSHINE_SYSTEM_WAYLAND_PROTOCOLS "Use system installation of wayland-protocols rather than the submodule." OFF) +option(CUDA_INHERIT_COMPILE_OPTIONS + "When building CUDA code, inherit compile options from the the main project. You may want to disable this if + your IDE throws errors about unknown flags after running cmake." ON) + +if(UNIX) + # technically, the homebrew build could be on linux as well... no idea if it would actually work + option(SUNSHINE_BUILD_HOMEBREW + "Enable a Homebrew build." OFF) +endif () + if(APPLE) + option(SUNSHINE_CONFIGURE_HOMEBREW + "Configure macOS Homebrew formula. Recommended to use with SUNSHINE_CONFIGURE_ONLY" OFF) option(SUNSHINE_CONFIGURE_PORTFILE "Configure macOS Portfile. Recommended to use with SUNSHINE_CONFIGURE_ONLY" OFF) option(SUNSHINE_PACKAGE_MACOS diff --git a/cmake/prep/special_package_configuration.cmake b/cmake/prep/special_package_configuration.cmake index 3094c2399a5..17e724c90d0 100644 --- a/cmake/prep/special_package_configuration.cmake +++ b/cmake/prep/special_package_configuration.cmake @@ -2,6 +2,9 @@ if (APPLE) if(${SUNSHINE_CONFIGURE_PORTFILE}) configure_file(packaging/macos/Portfile Portfile @ONLY) endif() + if(${SUNSHINE_CONFIGURE_HOMEBREW}) + configure_file(packaging/macos/sunshine.rb sunshine.rb @ONLY) + endif() elseif (UNIX) # configure the .desktop file if(${SUNSHINE_BUILD_APPIMAGE}) @@ -24,11 +27,13 @@ elseif (UNIX) # configure the arch linux pkgbuild if(${SUNSHINE_CONFIGURE_PKGBUILD}) configure_file(packaging/linux/Arch/PKGBUILD PKGBUILD @ONLY) + configure_file(packaging/linux/Arch/sunshine.install sunshine.install @ONLY) endif() # configure the flatpak manifest if(${SUNSHINE_CONFIGURE_FLATPAK_MAN}) configure_file(packaging/linux/flatpak/dev.lizardbyte.sunshine.yml dev.lizardbyte.sunshine.yml @ONLY) + file(COPY packaging/linux/flatpak/deps/ DESTINATION ${CMAKE_BINARY_DIR}) endif() endif() diff --git a/cmake/targets/common.cmake b/cmake/targets/common.cmake index cb5fe4e67d4..094bbca7c20 100644 --- a/cmake/targets/common.cmake +++ b/cmake/targets/common.cmake @@ -3,6 +3,18 @@ add_executable(sunshine ${SUNSHINE_TARGET_FILES}) +# Homebrew build fails the vite build if we set these environment variables +# this block must be before the platform specific code +if(${SUNSHINE_BUILD_HOMEBREW}) + set(NPM_SOURCE_ASSETS_DIR "") + set(NPM_ASSETS_DIR "") + set(NPM_BUILD_HOMEBREW "true") +else() + set(NPM_SOURCE_ASSETS_DIR ${SUNSHINE_SOURCE_ASSETS_DIR}) + set(NPM_ASSETS_DIR ${CMAKE_BINARY_DIR}) + set(NPM_BUILD_HOMEBREW "") +endif() + # platform specific target definitions if(WIN32) include(${CMAKE_MODULE_PATH}/targets/windows.cmake) @@ -28,14 +40,59 @@ set_target_properties(sunshine PROPERTIES CXX_STANDARD 17 VERSION ${PROJECT_VERSION} SOVERSION ${PROJECT_VERSION_MAJOR}) -foreach(flag IN LISTS SUNSHINE_COMPILE_OPTIONS) - list(APPEND SUNSHINE_COMPILE_OPTIONS_CUDA "$<$:--compiler-options=${flag}>") -endforeach() +# CLion complains about unknown flags after running cmake, and cannot add symbols to the index for cuda files +if(CUDA_INHERIT_COMPILE_OPTIONS) + foreach(flag IN LISTS SUNSHINE_COMPILE_OPTIONS) + list(APPEND SUNSHINE_COMPILE_OPTIONS_CUDA "$<$:--compiler-options=${flag}>") + endforeach() +endif() target_compile_options(sunshine PRIVATE $<$:${SUNSHINE_COMPILE_OPTIONS}>;$<$:${SUNSHINE_COMPILE_OPTIONS_CUDA};-std=c++17>) # cmake-lint: disable=C0301 -#WebUI build -add_custom_target(web-ui ALL - WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" - COMMENT "Installing NPM Dependencies and Building the Web UI" - COMMAND bash -c \"npm install && SUNSHINE_SOURCE_ASSETS_DIR=${SUNSHINE_SOURCE_ASSETS_DIR} SUNSHINE_ASSETS_DIR=${CMAKE_BINARY_DIR} npm run build\") # cmake-lint: disable=C0301 +# tests +if(BUILD_TESTS) + add_subdirectory(tests) +endif() + +# custom compile flags, must be after adding tests + +if (NOT BUILD_TESTS) + set(TEST_DIR "") +else() + set(TEST_DIR "${CMAKE_SOURCE_DIR}/tests") +endif() + +# src/upnp +set_source_files_properties("${CMAKE_SOURCE_DIR}/src/upnp.cpp" + DIRECTORY "${CMAKE_SOURCE_DIR}" "${TEST_DIR}" + PROPERTIES COMPILE_FLAGS -Wno-pedantic) + +# third-party/nanors +set_source_files_properties("${CMAKE_SOURCE_DIR}/third-party/nanors/rs.c" + DIRECTORY "${CMAKE_SOURCE_DIR}" "${TEST_DIR}" + PROPERTIES COMPILE_FLAGS "-include deps/obl/autoshim.h -ftree-vectorize") + +# third-party/ViGEmClient +set(VIGEM_COMPILE_FLAGS "") +string(APPEND VIGEM_COMPILE_FLAGS "-Wno-unknown-pragmas ") +string(APPEND VIGEM_COMPILE_FLAGS "-Wno-misleading-indentation ") +string(APPEND VIGEM_COMPILE_FLAGS "-Wno-class-memaccess ") +string(APPEND VIGEM_COMPILE_FLAGS "-Wno-unused-function ") +string(APPEND VIGEM_COMPILE_FLAGS "-Wno-unused-variable ") +set_source_files_properties("${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/src/ViGEmClient.cpp" + DIRECTORY "${CMAKE_SOURCE_DIR}" "${TEST_DIR}" + PROPERTIES + COMPILE_DEFINITIONS "UNICODE=1;ERROR_INVALID_DEVICE_OBJECT_PARAMETER=650" + COMPILE_FLAGS ${VIGEM_COMPILE_FLAGS}) + +# src/nvhttp +string(TOUPPER "x${CMAKE_BUILD_TYPE}" BUILD_TYPE) +if("${BUILD_TYPE}" STREQUAL "XDEBUG") + if(WIN32) + set_source_files_properties("${CMAKE_SOURCE_DIR}/src/nvhttp.cpp" + DIRECTORY "${CMAKE_SOURCE_DIR}" "${CMAKE_SOURCE_DIR}/tests" + PROPERTIES COMPILE_FLAGS -O2) + endif() +else() + add_definitions(-DNDEBUG) +endif() diff --git a/cmake/targets/unix.cmake b/cmake/targets/unix.cmake index 047a0b3d381..5527a9874fa 100644 --- a/cmake/targets/unix.cmake +++ b/cmake/targets/unix.cmake @@ -1,2 +1,8 @@ # unix specific target definitions # put anything here that applies to both linux and macos + +#WebUI build +add_custom_target(web-ui ALL + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" + COMMENT "Installing NPM Dependencies and Building the Web UI" + COMMAND sh -c \"npm install && SUNSHINE_BUILD_HOMEBREW=${NPM_BUILD_HOMEBREW} SUNSHINE_SOURCE_ASSETS_DIR=${NPM_SOURCE_ASSETS_DIR} SUNSHINE_ASSETS_DIR=${NPM_ASSETS_DIR} npm run build\") # cmake-lint: disable=C0301 diff --git a/cmake/targets/windows.cmake b/cmake/targets/windows.cmake index 341d7c2e74e..e429feaa821 100644 --- a/cmake/targets/windows.cmake +++ b/cmake/targets/windows.cmake @@ -4,3 +4,9 @@ set(CMAKE_FIND_LIBRARY_SUFFIXES ".dll") find_library(ZLIB ZLIB1) list(APPEND SUNSHINE_EXTERNAL_LIBRARIES Wtsapi32.lib) + +#WebUI build +add_custom_target(web-ui ALL + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" + COMMENT "Installing NPM Dependencies and Building the Web UI" + COMMAND cmd /C "npm install && set \"SUNSHINE_SOURCE_ASSETS_DIR=${NPM_SOURCE_ASSETS_DIR}\" && set \"SUNSHINE_ASSETS_DIR=${NPM_ASSETS_DIR}\" && npm run build") # cmake-lint: disable=C0301 diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 00000000000..b50701c758b --- /dev/null +++ b/codecov.yml @@ -0,0 +1,19 @@ +--- +codecov: + branch: nightly + +coverage: + status: + project: + default: + target: auto + threshold: 10% + +comment: + layout: "diff, flags, files" + behavior: default + require_changes: false # if true: only post the comment if coverage changes + +ignore: + - "tests" + - "third-party" diff --git a/crowdin.yml b/crowdin.yml index 0be504ba7d8..3dd19366ef0 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -1,7 +1,7 @@ --- "base_path": "." "base_url": "https://api.crowdin.com" # optional (for Crowdin Enterprise only) -"preserve_hierarchy": false # flatten tree on crowdin +"preserve_hierarchy": true # false will flatten tree on crowdin, but doesn't work with dest option "pull_request_labels": [ "crowdin", "l10n" @@ -10,6 +10,7 @@ "files": [ { "source": "/locale/*.po", + "dest": "/%original_file_name%", "translation": "/locale/%two_letters_code%/LC_MESSAGES/%original_file_name%", "languages_mapping": { "two_letters_code": { @@ -17,6 +18,13 @@ "en-GB": "en_GB", "en-US": "en_US" } - } + }, + "update_option": "update_as_unapproved" + }, + { + "source": "/src_assets/common/assets/web/public/assets/locale/en.json", + "dest": "/sunshine.json", + "translation": "/src_assets/common/assets/web/public/assets/locale/%two_letters_code%.%file_extension%", + "update_option": "update_as_unapproved" } ] diff --git a/docker/archlinux.dockerfile b/docker/archlinux.dockerfile index 15cb5d23685..26c1b186d7d 100644 --- a/docker/archlinux.dockerfile +++ b/docker/archlinux.dockerfile @@ -34,7 +34,7 @@ ENV COMMIT=${COMMIT} SHELL ["/bin/bash", "-o", "pipefail", "-c"] # install dependencies -# cuda, libcap, and libdrm are optional dependencies for PKGBUILD +# cuda is an optional build-time dependency for PKGBUILD RUN <<_DEPS #!/bin/bash set -e @@ -43,9 +43,8 @@ pacman -Syu --disable-download-timeout --needed --noconfirm \ cmake \ cuda \ git \ - libcap \ - libdrm \ - namcap + namcap \ + xorg-server-xvfb _DEPS # Setup builder user @@ -80,19 +79,22 @@ _MAKE WORKDIR /build/sunshine/pkg RUN mv /build/sunshine/build/PKGBUILD . +RUN mv /build/sunshine/build/sunshine.install . # namcap and build PKGBUILD file RUN <<_PKGBUILD #!/bin/bash set -e +export DISPLAY=:1 +Xvfb ${DISPLAY} -screen 0 1024x768x24 & namcap -i PKGBUILD makepkg -si --noconfirm +rm -f /build/sunshine/pkg/sunshine-debug*.pkg.tar.zst ls -a _PKGBUILD FROM scratch as artifacts -COPY --link --from=sunshine-build /build/sunshine/pkg/PKGBUILD /PKGBUILD COPY --link --from=sunshine-build /build/sunshine/pkg/sunshine*.pkg.tar.zst /sunshine.pkg.tar.zst FROM sunshine-base as sunshine diff --git a/docker/clion-toolchain.dockerfile b/docker/clion-toolchain.dockerfile index bb4604f54f5..204450bf1c9 100644 --- a/docker/clion-toolchain.dockerfile +++ b/docker/clion-toolchain.dockerfile @@ -14,6 +14,8 @@ FROM toolchain-base as toolchain ARG TARGETPLATFORM RUN echo "target_platform: ${TARGETPLATFORM}" +ENV DISPLAY=:0 + SHELL ["/bin/bash", "-o", "pipefail", "-c"] # install dependencies RUN <<_DEPS @@ -24,12 +26,13 @@ apt-get install -y --no-install-recommends \ build-essential \ cmake=3.22.* \ ca-certificates \ + doxygen \ gcc=4:11.2.* \ g++=4:11.2.* \ gdb \ git \ + graphviz \ libayatana-appindicator3-dev \ - libavdevice-dev \ libboost-filesystem-dev=1.74.* \ libboost-locale-dev=1.74.* \ libboost-log-dev=1.74.* \ @@ -54,8 +57,12 @@ apt-get install -y --no-install-recommends \ libxfixes-dev \ libxrandr-dev \ libxtst-dev \ + python3.10 \ + python3.10-venv \ udev \ - wget + wget \ + x11-xserver-utils \ + xvfb if [[ "${TARGETPLATFORM}" == 'linux/amd64' ]]; then apt-get install -y --no-install-recommends \ libmfx-dev @@ -98,3 +105,28 @@ chmod a+x ./cuda.run ./cuda.run --silent --toolkit --toolkitpath=/usr/local --no-opengl-libs --no-man-page --no-drm rm ./cuda.run _INSTALL_CUDA + +WORKDIR / +# Write a shell script that starts Xvfb and then runs a shell +RUN <<_ENTRYPOINT +#!/bin/bash +set -e +cat < /entrypoint.sh +#!/bin/bash +Xvfb ${DISPLAY} -screen 0 1024x768x24 & +if [ "\$#" -eq 0 ]; then + exec "/bin/bash" +else + exec "\$@" +fi +EOF +_ENTRYPOINT + +# Make the script executable +RUN chmod +x /entrypoint.sh + +# Note about CLion +RUN echo "ATTENTION: CLion will override the entrypoint, you can disable this in the toolchain settings" + +# Use the shell script as the entrypoint +ENTRYPOINT ["/entrypoint.sh"] diff --git a/docker/debian-bookworm.dockerfile b/docker/debian-bookworm.dockerfile index a62e092eab4..34cf29bedc9 100644 --- a/docker/debian-bookworm.dockerfile +++ b/docker/debian-bookworm.dockerfile @@ -32,8 +32,9 @@ apt-get update -y apt-get install -y --no-install-recommends \ build-essential \ cmake=3.25.* \ + doxygen \ git \ - libavdevice-dev \ + graphviz \ libayatana-appindicator3-dev \ libboost-filesystem-dev=1.74.* \ libboost-locale-dev=1.74.* \ @@ -61,8 +62,12 @@ apt-get install -y --no-install-recommends \ libxtst-dev \ nodejs \ npm \ + python3.11 \ + python3.11-venv \ udev \ - wget + wget \ + x11-xserver-utils \ + xvfb if [[ "${TARGETPLATFORM}" == 'linux/amd64' ]]; then apt-get install -y --no-install-recommends \ libmfx-dev @@ -105,6 +110,7 @@ RUN <<_MAKE #!/bin/bash set -e cmake \ + -DBUILD_WERROR=ON \ -DCMAKE_CUDA_COMPILER:PATH=/build/cuda/bin/nvcc \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_INSTALL_PREFIX=/usr \ @@ -119,6 +125,17 @@ make -j "$(nproc)" cpack -G DEB _MAKE +# run tests +WORKDIR /build/sunshine/build/tests +# hadolint ignore=SC1091 +RUN <<_TEST +#!/bin/bash +set -e +export DISPLAY=:1 +Xvfb ${DISPLAY} -screen 0 1024x768x24 & +./test_sunshine --gtest_color=yes +_TEST + FROM scratch AS artifacts ARG BASE ARG TAG diff --git a/docker/debian-bullseye.dockerfile b/docker/debian-bullseye.dockerfile index f355307631d..2a491559083 100644 --- a/docker/debian-bullseye.dockerfile +++ b/docker/debian-bullseye.dockerfile @@ -33,8 +33,9 @@ apt-get install -y --no-install-recommends \ build-essential \ ca-certificates \ cmake=3.18.* \ + doxygen \ git \ - libavdevice-dev \ + graphviz \ libayatana-appindicator3-dev \ libboost-filesystem-dev=1.74.* \ libboost-locale-dev=1.74.* \ @@ -60,8 +61,12 @@ apt-get install -y --no-install-recommends \ libxfixes-dev \ libxrandr-dev \ libxtst-dev \ + python3.9 \ + python3.9-venv \ udev \ - wget + wget \ + x11-xserver-utils \ + xvfb if [[ "${TARGETPLATFORM}" == 'linux/amd64' ]]; then apt-get install -y --no-install-recommends \ libmfx-dev @@ -119,6 +124,7 @@ set -e source "$HOME/.nvm/nvm.sh" nvm use 20.9.0 cmake \ + -DBUILD_WERROR=ON \ -DCMAKE_CUDA_COMPILER:PATH=/build/cuda/bin/nvcc \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_INSTALL_PREFIX=/usr \ @@ -133,6 +139,17 @@ make -j "$(nproc)" cpack -G DEB _MAKE +# run tests +WORKDIR /build/sunshine/build/tests +# hadolint ignore=SC1091 +RUN <<_TEST +#!/bin/bash +set -e +export DISPLAY=:1 +Xvfb ${DISPLAY} -screen 0 1024x768x24 & +./test_sunshine --gtest_color=yes +_TEST + FROM scratch AS artifacts ARG BASE ARG TAG diff --git a/docker/fedora-38.dockerfile b/docker/fedora-38.dockerfile index 5408782d074..52796d509f1 100644 --- a/docker/fedora-38.dockerfile +++ b/docker/fedora-38.dockerfile @@ -1,6 +1,6 @@ # syntax=docker/dockerfile:1.4 # artifacts: true -# platforms: linux/amd64,linux/arm64/v8 +# platforms: linux/amd64 # platforms_pr: linux/amd64 # no-cache-filters: sunshine-base,artifacts,sunshine ARG BASE=fedora @@ -32,9 +32,11 @@ dnf -y group install "Development Tools" dnf -y install \ boost-devel-1.78.0* \ cmake-3.27.* \ + doxygen \ gcc-13.2.* \ gcc-c++-13.2.* \ git \ + graphviz \ libappindicator-gtk3-devel \ libcap-devel \ libcurl-devel \ @@ -58,9 +60,11 @@ dnf -y install \ openssl-devel \ opus-devel \ pulseaudio-libs-devel \ + python3.10 \ rpm-build \ wget \ - which + which \ + xorg-x11-server-Xvfb if [[ "${TARGETPLATFORM}" == 'linux/amd64' ]]; then dnf -y install intel-mediasdk-devel fi @@ -68,28 +72,27 @@ dnf clean all rm -rf /var/cache/yum _DEPS -# todo - enable cuda once it's supported for gcc 13 and fedora 38 ## install cuda -#WORKDIR /build/cuda +WORKDIR /build/cuda ## versions: https://developer.nvidia.com/cuda-toolkit-archive -#ENV CUDA_VERSION="12.0.0" -#ENV CUDA_BUILD="525.60.13" +ENV CUDA_VERSION="12.4.0" +ENV CUDA_BUILD="550.54.14" ## hadolint ignore=SC3010 -#RUN <<_INSTALL_CUDA -##!/bin/bash -#set -e -#cuda_prefix="https://developer.download.nvidia.com/compute/cuda/" -#cuda_suffix="" -#if [[ "${TARGETPLATFORM}" == 'linux/arm64' ]]; then -# cuda_suffix="_sbsa" -#fi -#url="${cuda_prefix}${CUDA_VERSION}/local_installers/cuda_${CUDA_VERSION}_${CUDA_BUILD}_linux${cuda_suffix}.run" -#echo "cuda url: ${url}" -#wget "$url" --progress=bar:force:noscroll -q --show-progress -O ./cuda.run -#chmod a+x ./cuda.run -#./cuda.run --silent --toolkit --toolkitpath=/build/cuda --no-opengl-libs --no-man-page --no-drm -#rm ./cuda.run -#_INSTALL_CUDA +RUN <<_INSTALL_CUDA +#!/bin/bash +set -e +cuda_prefix="https://developer.download.nvidia.com/compute/cuda/" +cuda_suffix="" +if [[ "${TARGETPLATFORM}" == 'linux/arm64' ]]; then + cuda_suffix="_sbsa" +fi +url="${cuda_prefix}${CUDA_VERSION}/local_installers/cuda_${CUDA_VERSION}_${CUDA_BUILD}_linux${cuda_suffix}.run" +echo "cuda url: ${url}" +wget "$url" --progress=bar:force:noscroll -q --show-progress -O ./cuda.run +chmod a+x ./cuda.run +./cuda.run --silent --toolkit --toolkitpath=/build/cuda --no-opengl-libs --no-man-page --no-drm +rm ./cuda.run +_INSTALL_CUDA # copy repository WORKDIR /build/sunshine/ @@ -99,12 +102,12 @@ COPY --link .. . WORKDIR /build/sunshine/build # cmake and cpack -# todo - add cmake argument back in for cuda support "-DCMAKE_CUDA_COMPILER:PATH=/build/cuda/bin/nvcc \" -# todo - re-enable "DSUNSHINE_ENABLE_CUDA" RUN <<_MAKE #!/bin/bash set -e cmake \ + -DCMAKE_CUDA_COMPILER:PATH=/build/cuda/bin/nvcc \ + -DBUILD_WERROR=ON \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_INSTALL_PREFIX=/usr \ -DSUNSHINE_ASSETS_DIR=share/sunshine \ @@ -112,12 +115,23 @@ cmake \ -DSUNSHINE_ENABLE_WAYLAND=ON \ -DSUNSHINE_ENABLE_X11=ON \ -DSUNSHINE_ENABLE_DRM=ON \ - -DSUNSHINE_ENABLE_CUDA=OFF \ + -DSUNSHINE_ENABLE_CUDA=ON \ /build/sunshine make -j "$(nproc)" cpack -G RPM _MAKE +# run tests +WORKDIR /build/sunshine/build/tests +# hadolint ignore=SC1091 +RUN <<_TEST +#!/bin/bash +set -e +export DISPLAY=:1 +Xvfb ${DISPLAY} -screen 0 1024x768x24 & +./test_sunshine --gtest_color=yes +_TEST + FROM scratch AS artifacts ARG BASE ARG TAG diff --git a/docker/fedora-39.dockerfile b/docker/fedora-39.dockerfile index cc781a6730f..262b40fc7ac 100644 --- a/docker/fedora-39.dockerfile +++ b/docker/fedora-39.dockerfile @@ -1,6 +1,6 @@ # syntax=docker/dockerfile:1.4 # artifacts: true -# platforms: linux/amd64,linux/arm64/v8 +# platforms: linux/amd64 # platforms_pr: linux/amd64 # no-cache-filters: sunshine-base,artifacts,sunshine ARG BASE=fedora @@ -32,9 +32,11 @@ dnf -y group install "Development Tools" dnf -y install \ boost-devel-1.81.0* \ cmake-3.27.* \ + doxygen \ gcc-13.2.* \ gcc-c++-13.2.* \ git \ + graphviz \ libappindicator-gtk3-devel \ libcap-devel \ libcurl-devel \ @@ -58,9 +60,11 @@ dnf -y install \ openssl-devel \ opus-devel \ pulseaudio-libs-devel \ + python3.11 \ rpm-build \ wget \ - which + which \ + xorg-x11-server-Xvfb if [[ "${TARGETPLATFORM}" == 'linux/amd64' ]]; then dnf -y install intel-mediasdk-devel fi @@ -68,28 +72,34 @@ dnf clean all rm -rf /var/cache/yum _DEPS -# todo - enable cuda once it's supported for gcc 13 and fedora 39 -## install cuda -#WORKDIR /build/cuda -## versions: https://developer.nvidia.com/cuda-toolkit-archive -#ENV CUDA_VERSION="12.0.0" -#ENV CUDA_BUILD="525.60.13" -## hadolint ignore=SC3010 -#RUN <<_INSTALL_CUDA -##!/bin/bash -#set -e -#cuda_prefix="https://developer.download.nvidia.com/compute/cuda/" -#cuda_suffix="" -#if [[ "${TARGETPLATFORM}" == 'linux/arm64' ]]; then -# cuda_suffix="_sbsa" -#fi -#url="${cuda_prefix}${CUDA_VERSION}/local_installers/cuda_${CUDA_VERSION}_${CUDA_BUILD}_linux${cuda_suffix}.run" -#echo "cuda url: ${url}" -#wget "$url" --progress=bar:force:noscroll -q --show-progress -O ./cuda.run -#chmod a+x ./cuda.run -#./cuda.run --silent --toolkit --toolkitpath=/build/cuda --no-opengl-libs --no-man-page --no-drm -#rm ./cuda.run -#_INSTALL_CUDA +# install cuda +WORKDIR /build/cuda +# versions: https://developer.nvidia.com/cuda-toolkit-archive +ENV CUDA_VERSION="12.4.0" +ENV CUDA_BUILD="550.54.14" +# hadolint ignore=SC3010 +RUN <<_INSTALL_CUDA +#!/bin/bash +set -e +cuda_prefix="https://developer.download.nvidia.com/compute/cuda/" +cuda_suffix="" +if [[ "${TARGETPLATFORM}" == 'linux/arm64' ]]; then + cuda_suffix="_sbsa" + + # patch headers https://bugs.launchpad.net/ubuntu/+source/mumax3/+bug/2032624 + sed -i 's/__Float32x4_t/int/g' /usr/include/bits/math-vector.h + sed -i 's/__Float64x2_t/int/g' /usr/include/bits/math-vector.h + sed -i 's/__SVFloat32_t/float/g' /usr/include/bits/math-vector.h + sed -i 's/__SVFloat64_t/float/g' /usr/include/bits/math-vector.h + sed -i 's/__SVBool_t/int/g' /usr/include/bits/math-vector.h +fi +url="${cuda_prefix}${CUDA_VERSION}/local_installers/cuda_${CUDA_VERSION}_${CUDA_BUILD}_linux${cuda_suffix}.run" +echo "cuda url: ${url}" +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/ @@ -99,12 +109,12 @@ COPY --link .. . WORKDIR /build/sunshine/build # cmake and cpack -# todo - add cmake argument back in for cuda support "-DCMAKE_CUDA_COMPILER:PATH=/build/cuda/bin/nvcc \" -# todo - re-enable "DSUNSHINE_ENABLE_CUDA" RUN <<_MAKE #!/bin/bash set -e cmake \ + -DCMAKE_CUDA_COMPILER:PATH=/build/cuda/bin/nvcc \ + -DBUILD_WERROR=ON \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_INSTALL_PREFIX=/usr \ -DSUNSHINE_ASSETS_DIR=share/sunshine \ @@ -112,12 +122,23 @@ cmake \ -DSUNSHINE_ENABLE_WAYLAND=ON \ -DSUNSHINE_ENABLE_X11=ON \ -DSUNSHINE_ENABLE_DRM=ON \ - -DSUNSHINE_ENABLE_CUDA=OFF \ + -DSUNSHINE_ENABLE_CUDA=ON \ /build/sunshine make -j "$(nproc)" cpack -G RPM _MAKE +# run tests +WORKDIR /build/sunshine/build/tests +# hadolint ignore=SC1091 +RUN <<_TEST +#!/bin/bash +set -e +export DISPLAY=:1 +Xvfb ${DISPLAY} -screen 0 1024x768x24 & +./test_sunshine --gtest_color=yes +_TEST + FROM scratch AS artifacts ARG BASE ARG TAG diff --git a/docker/ubuntu-22.04.dockerfile b/docker/ubuntu-22.04.dockerfile index ab6ec096a3b..e02ca1eba91 100644 --- a/docker/ubuntu-22.04.dockerfile +++ b/docker/ubuntu-22.04.dockerfile @@ -33,9 +33,10 @@ apt-get install -y --no-install-recommends \ build-essential \ cmake=3.22.* \ ca-certificates \ + doxygen \ git \ + graphviz \ libayatana-appindicator3-dev \ - libavdevice-dev \ libboost-filesystem-dev=1.74.* \ libboost-locale-dev=1.74.* \ libboost-log-dev=1.74.* \ @@ -60,8 +61,12 @@ apt-get install -y --no-install-recommends \ libxfixes-dev \ libxrandr-dev \ libxtst-dev \ + python3.10 \ + python3.10-venv \ udev \ - wget + wget \ + x11-xserver-utils \ + xvfb if [[ "${TARGETPLATFORM}" == 'linux/amd64' ]]; then apt-get install -y --no-install-recommends \ libmfx-dev @@ -120,6 +125,7 @@ source "$HOME/.nvm/nvm.sh" nvm use 20.9.0 #Actually build cmake \ + -DBUILD_WERROR=ON \ -DCMAKE_CUDA_COMPILER:PATH=/build/cuda/bin/nvcc \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_INSTALL_PREFIX=/usr \ @@ -134,6 +140,17 @@ make -j "$(nproc)" cpack -G DEB _MAKE +# run tests +WORKDIR /build/sunshine/build/tests +# hadolint ignore=SC1091 +RUN <<_TEST +#!/bin/bash +set -e +export DISPLAY=:1 +Xvfb ${DISPLAY} -screen 0 1024x768x24 & +./test_sunshine --gtest_color=yes +_TEST + FROM scratch AS artifacts ARG BASE ARG TAG diff --git a/docker/ubuntu-20.04.dockerfile b/docker/ubuntu-24.04.dockerfile similarity index 77% rename from docker/ubuntu-20.04.dockerfile rename to docker/ubuntu-24.04.dockerfile index d677830db5c..7ef83bfba39 100644 --- a/docker/ubuntu-20.04.dockerfile +++ b/docker/ubuntu-24.04.dockerfile @@ -1,10 +1,10 @@ # syntax=docker/dockerfile:1.4 # artifacts: true -# platforms: linux/amd64,linux/arm64/v8 +# platforms: linux/amd64 # platforms_pr: linux/amd64 # no-cache-filters: sunshine-base,artifacts,sunshine ARG BASE=ubuntu -ARG TAG=20.04 +ARG TAG=24.04 FROM ${BASE}:${TAG} AS sunshine-base ENV DEBIAN_FRONTEND=noninteractive @@ -31,16 +31,18 @@ set -e apt-get update -y apt-get install -y --no-install-recommends \ build-essential \ + cmake=3.28.* \ ca-certificates \ - gcc-10=10.5.* \ - g++-10=10.5.* \ + doxygen \ + gcc-11 \ + g++-11 \ git \ + graphviz \ libayatana-appindicator3-dev \ - libavdevice-dev \ - libboost-filesystem-dev=1.71.* \ - libboost-locale-dev=1.71.* \ - libboost-log-dev=1.71.* \ - libboost-program-options-dev=1.71.* \ + libboost-filesystem-dev=1.83.* \ + libboost-locale-dev=1.83.* \ + libboost-log-dev=1.83.* \ + libboost-program-options-dev=1.83.* \ libcap-dev \ libcurl4-openssl-dev \ libdrm-dev \ @@ -61,8 +63,12 @@ apt-get install -y --no-install-recommends \ libxfixes-dev \ libxrandr-dev \ libxtst-dev \ + python3.12 \ + python3.12-venv \ udev \ - wget + wget \ + x11-xserver-utils \ + xvfb if [[ "${TARGETPLATFORM}" == 'linux/amd64' ]]; then apt-get install -y --no-install-recommends \ libmfx-dev @@ -71,6 +77,7 @@ apt-get clean rm -rf /var/lib/apt/lists/* _DEPS + #Install Node # hadolint ignore=SC1091 RUN <<_INSTALL_NODE @@ -88,35 +95,13 @@ RUN <<_GCC_ALIAS #!/bin/bash set -e update-alternatives --install \ - /usr/bin/gcc gcc /usr/bin/gcc-10 100 \ - --slave /usr/bin/g++ g++ /usr/bin/g++-10 \ - --slave /usr/bin/gcov gcov /usr/bin/gcov-10 \ - --slave /usr/bin/gcc-ar gcc-ar /usr/bin/gcc-ar-10 \ - --slave /usr/bin/gcc-ranlib gcc-ranlib /usr/bin/gcc-ranlib-10 + /usr/bin/gcc gcc /usr/bin/gcc-11 100 \ + --slave /usr/bin/g++ g++ /usr/bin/g++-11 \ + --slave /usr/bin/gcov gcov /usr/bin/gcov-11 \ + --slave /usr/bin/gcc-ar gcc-ar /usr/bin/gcc-ar-11 \ + --slave /usr/bin/gcc-ranlib gcc-ranlib /usr/bin/gcc-ranlib-11 _GCC_ALIAS -# install cmake -# sunshine requires cmake >= 3.18 -WORKDIR /build/cmake -# https://cmake.org/download/ -ENV CMAKE_VERSION="3.25.1" -# hadolint ignore=SC3010 -RUN <<_INSTALL_CMAKE -#!/bin/bash -set -e -cmake_prefix="https://github.com/Kitware/CMake/releases/download/v" -if [[ "${TARGETPLATFORM}" == 'linux/amd64' ]]; then - cmake_arch="x86_64" -elif [[ "${TARGETPLATFORM}" == 'linux/arm64' ]]; then - cmake_arch="aarch64" -fi -url="${cmake_prefix}${CMAKE_VERSION}/cmake-${CMAKE_VERSION}-linux-${cmake_arch}.sh" -echo "cmake url: ${url}" -wget "$url" --progress=bar:force:noscroll -q --show-progress -O ./cmake.sh -sh ./cmake.sh --prefix=/usr/local --skip-license -rm ./cmake.sh -_INSTALL_CMAKE - # install cuda WORKDIR /build/cuda # versions: https://developer.nvidia.com/cuda-toolkit-archive @@ -154,7 +139,9 @@ set -e #Set Node version source "$HOME/.nvm/nvm.sh" nvm use 20.9.0 +#Actually build cmake \ + -DBUILD_WERROR=ON \ -DCMAKE_CUDA_COMPILER:PATH=/build/cuda/bin/nvcc \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_INSTALL_PREFIX=/usr \ @@ -169,6 +156,17 @@ make -j "$(nproc)" cpack -G DEB _MAKE +# run tests +WORKDIR /build/sunshine/build/tests +# hadolint ignore=SC1091 +RUN <<_TEST +#!/bin/bash +set -e +export DISPLAY=:1 +Xvfb ${DISPLAY} -screen 0 1024x768x24 & +./test_sunshine --gtest_color=yes +_TEST + FROM scratch AS artifacts ARG BASE ARG TAG @@ -196,9 +194,9 @@ EXPOSE 48010 EXPOSE 47998-48000/udp # setup user -ARG PGID=1000 +ARG PGID=1001 ENV PGID=${PGID} -ARG PUID=1000 +ARG PUID=1001 ENV PUID=${PUID} ENV TZ="UTC" ARG UNAME=lizard diff --git a/docs/Doxyfile b/docs/Doxyfile index d6e79cd2571..d6aa47edd51 100644 --- a/docs/Doxyfile +++ b/docs/Doxyfile @@ -1,4 +1,4 @@ -# Doxyfile 1.9.6 +# Doxyfile 1.10.0 # This file describes the settings to be used by the documentation system # doxygen (www.doxygen.org) for a project. @@ -42,7 +42,7 @@ DOXYFILE_ENCODING = UTF-8 # title of most generated pages and in a few other places. # The default value is: My Project. -PROJECT_NAME = "Sunshine" +PROJECT_NAME = Sunshine # The PROJECT_NUMBER tag can be used to enter a project or revision number. This # could be handy for archiving the generated documentation or if some version @@ -63,6 +63,12 @@ PROJECT_BRIEF = "Sunshine is a Gamestream host for Moonlight." PROJECT_LOGO = ../sunshine.png +# With the PROJECT_ICON tag one can specify an icon that is included in the tabs +# when the HTML document is shown. Doxygen will copy the logo to the output +# directory. + +PROJECT_ICON = + # The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path # into which the generated documentation will be written. If a relative path is # entered, it will be relative to the location where doxygen was started. If @@ -365,6 +371,17 @@ MARKDOWN_SUPPORT = YES TOC_INCLUDE_HEADINGS = 5 +# The MARKDOWN_ID_STYLE tag can be used to specify the algorithm used to +# generate identifiers for the Markdown headings. Note: Every identifier is +# unique. +# Possible values are: DOXYGEN use a fixed 'autotoc_md' string followed by a +# sequence number starting at 0 and GITHUB use the lower case version of title +# with any whitespace replaced by '-' and punctuation characters removed. +# The default value is: DOXYGEN. +# This tag requires that the tag MARKDOWN_SUPPORT is set to YES. + +MARKDOWN_ID_STYLE = DOXYGEN + # When enabled doxygen tries to link words that correspond to documented # classes, or namespaces to their corresponding documentation. Such a link can # be prevented in individual cases by putting a % sign in front of the word or @@ -489,6 +506,14 @@ LOOKUP_CACHE_SIZE = 0 NUM_PROC_THREADS = 0 +# If the TIMESTAMP tag is set different from NO then each generated page will +# contain the date or date and time when the page was generated. Setting this to +# NO can help when comparing the output of multiple runs. +# Possible values are: YES, NO, DATETIME and DATE. +# The default value is: NO. + +TIMESTAMP = NO + #--------------------------------------------------------------------------- # Build related configuration options #--------------------------------------------------------------------------- @@ -874,7 +899,14 @@ WARN_IF_UNDOC_ENUM_VAL = NO # a warning is encountered. If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS # then doxygen will continue running as if WARN_AS_ERROR tag is set to NO, but # at the end of the doxygen process doxygen will return with a non-zero status. -# Possible values are: NO, YES and FAIL_ON_WARNINGS. +# If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS_PRINT then doxygen behaves +# like FAIL_ON_WARNINGS but in case no WARN_LOGFILE is defined doxygen will not +# write the warning messages in between other messages but write them at the end +# of a run, in case a WARN_LOGFILE is defined the warning messages will be +# besides being in the defined file also be shown at the end of a run, unless +# the WARN_LOGFILE is defined as - i.e. standard output (stdout) in that case +# the behavior will remain as with the setting FAIL_ON_WARNINGS. +# Possible values are: NO, YES, FAIL_ON_WARNINGS and FAIL_ON_WARNINGS_PRINT. # The default value is: NO. WARN_AS_ERROR = NO @@ -953,12 +985,12 @@ INPUT_FILE_ENCODING = # Note the list of default checked file patterns might differ from the list of # default file extension mappings. # -# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cpp, -# *.c++, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h, -# *.hh, *.hxx, *.hpp, *.h++, *.l, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, -# *.inc, *.m, *.markdown, *.md, *.mm, *.dox (to be provided as doxygen C -# comment), *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, *.f18, *.f, *.for, *.vhd, -# *.vhdl, *.ucf, *.qsf and *.ice. +# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cxxm, +# *.cpp, *.cppm, *.ccm, *.c++, *.c++m, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, +# *.idl, *.ddl, *.odl, *.h, *.hh, *.hxx, *.hpp, *.h++, *.ixx, *.l, *.cs, *.d, +# *.php, *.php4, *.php5, *.phtml, *.inc, *.m, *.markdown, *.md, *.mm, *.dox (to +# be provided as doxygen C comment), *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, +# *.f18, *.f, *.for, *.vhd, *.vhdl, *.ucf, *.qsf and *.ice. FILE_PATTERNS = *.c \ *.cc \ @@ -1043,9 +1075,6 @@ EXCLUDE_PATTERNS = # output. The symbol name can be a fully qualified name, a word, or if the # wildcard * is used, a substring. Examples: ANamespace, AClass, # ANamespace::AClass, ANamespace::*Test -# -# Note that the wildcards are matched against the file with absolute path, so to -# exclude all test directories use the pattern */test/* EXCLUDE_SYMBOLS = @@ -1159,7 +1188,8 @@ FORTRAN_COMMENT_AFTER = 72 SOURCE_BROWSER = NO # Setting the INLINE_SOURCES tag to YES will include the body of functions, -# classes and enums directly into the documentation. +# multi-line macros, enums or list initialized variables directly into the +# documentation. # The default value is: NO. INLINE_SOURCES = NO @@ -1428,15 +1458,6 @@ HTML_COLORSTYLE_SAT = 100 HTML_COLORSTYLE_GAMMA = 80 -# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML -# page will contain the date and time when the page was generated. Setting this -# to YES can help to show when doxygen was last run and thus if the -# documentation is up to date. -# The default value is: NO. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_TIMESTAMP = NO - # If the HTML_DYNAMIC_MENUS tag is set to YES then the generated HTML # documentation will contain a main index with vertical navigation menus that # are dynamically created via JavaScript. If disabled, the navigation index will @@ -1456,6 +1477,33 @@ HTML_DYNAMIC_MENUS = YES HTML_DYNAMIC_SECTIONS = NO +# If the HTML_CODE_FOLDING tag is set to YES then classes and functions can be +# dynamically folded and expanded in the generated HTML source code. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_CODE_FOLDING = YES + +# If the HTML_COPY_CLIPBOARD tag is set to YES then doxygen will show an icon in +# the top right corner of code and text fragments that allows the user to copy +# its content to the clipboard. Note this only works if supported by the browser +# and the web page is served via a secure context (see: +# https://www.w3.org/TR/secure-contexts/), i.e. using the https: or file: +# protocol. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COPY_CLIPBOARD = YES + +# Doxygen stores a couple of settings persistently in the browser (via e.g. +# cookies). By default these settings apply to all HTML pages generated by +# doxygen across all projects. The HTML_PROJECT_COOKIE tag can be used to store +# the settings under a project specific key, such that the user preferences will +# be stored separately. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_PROJECT_COOKIE = + # With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries # shown in the various tree structured indices initially; the user can expand # and collapse entries dynamically later on. Doxygen will expand the tree to @@ -1586,6 +1634,16 @@ BINARY_TOC = NO TOC_EXPAND = NO +# The SITEMAP_URL tag is used to specify the full URL of the place where the +# generated documentation will be placed on the server by the user during the +# deployment of the documentation. The generated sitemap is called sitemap.xml +# and placed on the directory specified by HTML_OUTPUT. In case no SITEMAP_URL +# is specified no sitemap is generated. For information about the sitemap +# protocol see https://www.sitemaps.org +# This tag requires that the tag GENERATE_HTML is set to YES. + +SITEMAP_URL = + # If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and # QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that # can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help @@ -2074,9 +2132,16 @@ PDF_HYPERLINKS = YES USE_PDFLATEX = YES -# If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \batchmode -# command to the generated LaTeX files. This will instruct LaTeX to keep running -# if errors occur, instead of asking the user for help. +# The LATEX_BATCHMODE tag signals the behavior of LaTeX in case of an error. +# Possible values are: NO same as ERROR_STOP, YES same as BATCH, BATCH In batch +# mode nothing is printed on the terminal, errors are scrolled as if is +# hit at every error; missing files that TeX tries to input or request from +# keyboard input (\read on a not open input stream) cause the job to abort, +# NON_STOP In nonstop mode the diagnostic message will appear on the terminal, +# but there is no possibility of user interaction just like in batch mode, +# SCROLL In scroll mode, TeX will stop only for missing files to input or if +# keyboard input is necessary and ERROR_STOP In errorstop mode, TeX will stop at +# each error, asking for user intervention. # The default value is: NO. # This tag requires that the tag GENERATE_LATEX is set to YES. @@ -2097,14 +2162,6 @@ LATEX_HIDE_INDICES = NO LATEX_BIB_STYLE = plain -# If the LATEX_TIMESTAMP tag is set to YES then the footer of each generated -# page will contain the date and time when the page was generated. Setting this -# to NO can help when comparing the output of multiple runs. -# The default value is: NO. -# This tag requires that the tag GENERATE_LATEX is set to YES. - -LATEX_TIMESTAMP = NO - # The LATEX_EMOJI_DIRECTORY tag is used to specify the (relative or absolute) # path from which the emoji images will be read. If a relative path is entered, # it will be relative to the LATEX_OUTPUT directory. If left blank the @@ -2270,13 +2327,39 @@ DOCBOOK_OUTPUT = doxydocbook #--------------------------------------------------------------------------- # If the GENERATE_AUTOGEN_DEF tag is set to YES, doxygen will generate an -# AutoGen Definitions (see http://autogen.sourceforge.net/) file that captures +# AutoGen Definitions (see https://autogen.sourceforge.net/) file that captures # the structure of the code including all documentation. Note that this feature # is still experimental and incomplete at the moment. # The default value is: NO. GENERATE_AUTOGEN_DEF = NO +#--------------------------------------------------------------------------- +# Configuration options related to Sqlite3 output +#--------------------------------------------------------------------------- + +# If the GENERATE_SQLITE3 tag is set to YES doxygen will generate a Sqlite3 +# database with symbols found by doxygen stored in tables. +# The default value is: NO. + +GENERATE_SQLITE3 = NO + +# The SQLITE3_OUTPUT tag is used to specify where the Sqlite3 database will be +# put. If a relative path is entered the value of OUTPUT_DIRECTORY will be put +# in front of it. +# The default directory is: sqlite3. +# This tag requires that the tag GENERATE_SQLITE3 is set to YES. + +SQLITE3_OUTPUT = sqlite3 + +# The SQLITE3_RECREATE_DB tag is set to YES, the existing doxygen_sqlite3.db +# database file will be recreated with each doxygen run. If set to NO, doxygen +# will warn if a database file is already found and not modify it. +# The default value is: YES. +# This tag requires that the tag GENERATE_SQLITE3 is set to YES. + +SQLITE3_RECREATE_DB = YES + #--------------------------------------------------------------------------- # Configuration options related to the Perl module output #--------------------------------------------------------------------------- @@ -2419,15 +2502,15 @@ TAGFILES = GENERATE_TAGFILE = -# If the ALLEXTERNALS tag is set to YES, all external class will be listed in -# the class index. If set to NO, only the inherited external classes will be -# listed. +# If the ALLEXTERNALS tag is set to YES, all external classes and namespaces +# will be listed in the class and namespace index. If set to NO, only the +# inherited external classes will be listed. # The default value is: NO. ALLEXTERNALS = NO # If the EXTERNAL_GROUPS tag is set to YES, all external groups will be listed -# in the modules index. If set to NO, only the current project's groups will be +# in the topic index. If set to NO, only the current project's groups will be # listed. # The default value is: YES. @@ -2441,16 +2524,9 @@ EXTERNAL_GROUPS = YES EXTERNAL_PAGES = YES #--------------------------------------------------------------------------- -# Configuration options related to the dot tool +# Configuration options related to diagram generator tools #--------------------------------------------------------------------------- -# You can include diagrams made with dia in doxygen documentation. Doxygen will -# then run dia to produce the diagram and insert it in the documentation. The -# DIA_PATH tag allows you to specify the directory where the dia binary resides. -# If left empty dia is assumed to be found in the default search path. - -DIA_PATH = - # If set to YES the inheritance and collaboration graphs will hide inheritance # and usage relations if the target is undocumented or is not a class. # The default value is: YES. @@ -2459,7 +2535,7 @@ HIDE_UNDOC_RELATIONS = YES # If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is # available from the path. This tool is part of Graphviz (see: -# http://www.graphviz.org/), a graph visualization toolkit from AT&T and Lucent +# https://www.graphviz.org/), a graph visualization toolkit from AT&T and Lucent # Bell Labs. The other options in this section have no effect if this option is # set to NO # The default value is: NO. @@ -2512,13 +2588,19 @@ DOT_NODE_ATTR = "shape=box,height=0.2,width=0.4" DOT_FONTPATH = -# If the CLASS_GRAPH tag is set to YES (or GRAPH) then doxygen will generate a -# graph for each documented class showing the direct and indirect inheritance -# relations. In case HAVE_DOT is set as well dot will be used to draw the graph, -# otherwise the built-in generator will be used. If the CLASS_GRAPH tag is set -# to TEXT the direct and indirect inheritance relations will be shown as texts / -# links. -# Possible values are: NO, YES, TEXT and GRAPH. +# If the CLASS_GRAPH tag is set to YES or GRAPH or BUILTIN then doxygen will +# generate a graph for each documented class showing the direct and indirect +# inheritance relations. In case the CLASS_GRAPH tag is set to YES or GRAPH and +# HAVE_DOT is enabled as well, then dot will be used to draw the graph. In case +# the CLASS_GRAPH tag is set to YES and HAVE_DOT is disabled or if the +# CLASS_GRAPH tag is set to BUILTIN, then the built-in generator will be used. +# If the CLASS_GRAPH tag is set to TEXT the direct and indirect inheritance +# relations will be shown as texts / links. Explicit enabling an inheritance +# graph or choosing a different representation for an inheritance graph of a +# specific class, can be accomplished by means of the command \inheritancegraph. +# Disabling an inheritance graph can be accomplished by means of the command +# \hideinheritancegraph. +# Possible values are: NO, YES, TEXT, GRAPH and BUILTIN. # The default value is: YES. CLASS_GRAPH = YES @@ -2526,15 +2608,21 @@ CLASS_GRAPH = YES # If the COLLABORATION_GRAPH tag is set to YES then doxygen will generate a # graph for each documented class showing the direct and indirect implementation # dependencies (inheritance, containment, and class references variables) of the -# class with other documented classes. +# class with other documented classes. Explicit enabling a collaboration graph, +# when COLLABORATION_GRAPH is set to NO, can be accomplished by means of the +# command \collaborationgraph. Disabling a collaboration graph can be +# accomplished by means of the command \hidecollaborationgraph. # The default value is: YES. # This tag requires that the tag HAVE_DOT is set to YES. COLLABORATION_GRAPH = YES # If the GROUP_GRAPHS tag is set to YES then doxygen will generate a graph for -# groups, showing the direct groups dependencies. See also the chapter Grouping -# in the manual. +# groups, showing the direct groups dependencies. Explicit enabling a group +# dependency graph, when GROUP_GRAPHS is set to NO, can be accomplished by means +# of the command \groupgraph. Disabling a directory graph can be accomplished by +# means of the command \hidegroupgraph. See also the chapter Grouping in the +# manual. # The default value is: YES. # This tag requires that the tag HAVE_DOT is set to YES. @@ -2576,8 +2664,8 @@ DOT_UML_DETAILS = NO # The DOT_WRAP_THRESHOLD tag can be used to set the maximum number of characters # to display on a single line. If the actual line length exceeds this threshold -# significantly it will wrapped across multiple lines. Some heuristics are apply -# to avoid ugly line breaks. +# significantly it will be wrapped across multiple lines. Some heuristics are +# applied to avoid ugly line breaks. # Minimum value: 0, maximum value: 1000, default value: 17. # This tag requires that the tag HAVE_DOT is set to YES. @@ -2594,7 +2682,9 @@ TEMPLATE_RELATIONS = NO # If the INCLUDE_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are set to # YES then doxygen will generate a graph for each documented file showing the # direct and indirect include dependencies of the file with other documented -# files. +# files. Explicit enabling an include graph, when INCLUDE_GRAPH is is set to NO, +# can be accomplished by means of the command \includegraph. Disabling an +# include graph can be accomplished by means of the command \hideincludegraph. # The default value is: YES. # This tag requires that the tag HAVE_DOT is set to YES. @@ -2603,7 +2693,10 @@ INCLUDE_GRAPH = YES # If the INCLUDED_BY_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are # set to YES then doxygen will generate a graph for each documented file showing # the direct and indirect include dependencies of the file with other documented -# files. +# files. Explicit enabling an included by graph, when INCLUDED_BY_GRAPH is set +# to NO, can be accomplished by means of the command \includedbygraph. Disabling +# an included by graph can be accomplished by means of the command +# \hideincludedbygraph. # The default value is: YES. # This tag requires that the tag HAVE_DOT is set to YES. @@ -2643,7 +2736,10 @@ GRAPHICAL_HIERARCHY = YES # If the DIRECTORY_GRAPH tag is set to YES then doxygen will show the # dependencies a directory has on other directories in a graphical way. The # dependency relations are determined by the #include relations between the -# files in the directories. +# files in the directories. Explicit enabling a directory graph, when +# DIRECTORY_GRAPH is set to NO, can be accomplished by means of the command +# \directorygraph. Disabling a directory graph can be accomplished by means of +# the command \hidedirectorygraph. # The default value is: YES. # This tag requires that the tag HAVE_DOT is set to YES. @@ -2659,7 +2755,7 @@ DIR_GRAPH_MAX_DEPTH = 1 # The DOT_IMAGE_FORMAT tag can be used to set the image format of the images # generated by dot. For an explanation of the image formats see the section # output formats in the documentation of the dot tool (Graphviz (see: -# http://www.graphviz.org/)). +# https://www.graphviz.org/)). # Note: If you choose svg you need to set HTML_FILE_EXTENSION to xhtml in order # to make the SVG files visible in IE 9+ (other browsers do not have this # requirement). @@ -2696,11 +2792,12 @@ DOT_PATH = DOTFILE_DIRS = -# The MSCFILE_DIRS tag can be used to specify one or more directories that -# contain msc files that are included in the documentation (see the \mscfile -# command). +# You can include diagrams made with dia in doxygen documentation. Doxygen will +# then run dia to produce the diagram and insert it in the documentation. The +# DIA_PATH tag allows you to specify the directory where the dia binary resides. +# If left empty dia is assumed to be found in the default search path. -MSCFILE_DIRS = +DIA_PATH = # The DIAFILE_DIRS tag can be used to specify one or more directories that # contain dia files that are included in the documentation (see the \diafile @@ -2777,3 +2874,19 @@ GENERATE_LEGEND = YES # The default value is: YES. DOT_CLEANUP = YES + +# You can define message sequence charts within doxygen comments using the \msc +# command. If the MSCGEN_TOOL tag is left empty (the default), then doxygen will +# use a built-in version of mscgen tool to produce the charts. Alternatively, +# the MSCGEN_TOOL tag can also specify the name an external tool. For instance, +# specifying prog as the value, doxygen will call the tool as prog -T +# -o . The external tool should support +# output file formats "png", "eps", "svg", and "ismap". + +MSCGEN_TOOL = + +# The MSCFILE_DIRS tag can be used to specify one or more directories that +# contain msc files that are included in the documentation (see the \mscfile +# command). + +MSCFILE_DIRS = diff --git a/docs/Makefile b/docs/Makefile index d0c3cbf1020..8b6275ab8cc 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -3,7 +3,7 @@ # You can set these variables from the command line, and also # from the environment for the first two. -SPHINXOPTS ?= +SPHINXOPTS ?= -W --keep-going SPHINXBUILD ?= sphinx-build SOURCEDIR = source BUILDDIR = build diff --git a/docs/make.bat b/docs/make.bat index dc1312ab09c..08ca2232081 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -9,6 +9,7 @@ if "%SPHINXBUILD%" == "" ( ) set SOURCEDIR=source set BUILDDIR=build +set "SPHINXOPTS=-W --keep-going" %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( @@ -25,11 +26,11 @@ if errorlevel 9009 ( if "%1" == "" goto help -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% || exit /b %ERRORLEVEL% goto end :help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% || exit /b %ERRORLEVEL% :end popd diff --git a/docs/requirements.txt b/docs/requirements.txt index 688a0a896ce..46e47734df9 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,8 +1,9 @@ breathe==4.35.0 furo==2024.1.29 m2r2==0.3.3.post2 -rstcheck[sphinx]==6.2.0 +rstcheck[sphinx]==6.2.1 rstfmt==0.0.14 +setuptools # required by m2r2, Ubuntu 24.04 doesn't include this Sphinx==7.2.6 sphinx-copybutton==0.5.2 sphinx_inline_tabs==2023.4.21 diff --git a/docs/source/about/advanced_usage.rst b/docs/source/about/advanced_usage.rst index fe603e38174..1c3a0f64c56 100644 --- a/docs/source/about/advanced_usage.rst +++ b/docs/source/about/advanced_usage.rst @@ -47,6 +47,40 @@ editing the `conf` file in a text editor. Use the examples as reference. `General `__ ----------------------------------------------------- +`locale `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Description** + The locale used for Sunshine's user interface. + +**Choices** + +.. table:: + :widths: auto + + ======= =========== + Value Description + ======= =========== + de German + en English + en_GB English (UK) + en_US English (United States) + es Spanish + fr French + it Italian + ru Russian + sv Swedish + zh Chinese (Simplified) + ======= =========== + +**Default** + ``en`` + +**Example** + .. code-block:: text + + locale = en + `sunshine_name `__ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -92,112 +126,37 @@ editing the `conf` file in a text editor. Use the examples as reference. min_log_level = info -`global_prep_cmd `__ -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -**Description** - A list of commands to be run before/after all applications. If any of the prep-commands fail, starting the application is aborted. - -**Default** - ``[]`` - -**Example** - .. code-block:: text - - global_prep_cmd = [{"do":"nircmd.exe setdisplay 1280 720 32 144","undo":"nircmd.exe setdisplay 2560 1440 32 144"}] - -`Files `__ -------------------------------------------------- - -`file_apps `__ -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -**Description** - The application configuration file path. The file contains a json formatted list of applications that can be started - by Moonlight. - -**Default** - OS and package dependent - -**Example** - .. code-block:: text - - file_apps = apps.json - -`credentials_file `__ -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -**Description** - The file where user credentials for the UI are stored. - -**Default** - ``sunshine_state.json`` - -**Example** - .. code-block:: text - - credentials_file = sunshine_state.json - -`log_path `__ +`channels `__ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Description** - The path where the sunshine log is stored. + Sunshine can support multiple clients streaming simultaneously, at the cost of higher CPU and GPU usage. -**Default** - ``sunshine.log`` - -**Example** - .. code-block:: text - - log_path = sunshine.log - -`pkey `__ -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -**Description** - The private key used for the web UI and Moonlight client pairing. For best compatibility, this should be an RSA-2048 private key. - - .. warning:: Not all Moonlight clients support ECDSA keys or RSA key lengths other than 2048 bits. - -**Default** - ``credentials/cakey.pem`` + .. note:: All connected clients share control of the same streaming session. -**Example** - .. code-block:: text - - pkey = /dir/pkey.pem - -`cert `__ -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -**Description** - The certificate used for the web UI and Moonlight client pairing. For best compatibility, this should have an RSA-2048 public key. - - .. warning:: Not all Moonlight clients support ECDSA keys or RSA key lengths other than 2048 bits. + .. warning:: Some hardware encoders may have limitations that reduce performance with multiple streams. **Default** - ``credentials/cacert.pem`` + ``1`` **Example** .. code-block:: text - cert = /dir/cert.pem + channels = 1 -`file_state `__ -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +`global_prep_cmd `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Description** - The file where current state of Sunshine is stored. + A list of commands to be run before/after all applications. If any of the prep-commands fail, starting the application is aborted. **Default** - ``sunshine_state.json`` + ``[]`` **Example** .. code-block:: text - file_state = sunshine_state.json - + global_prep_cmd = [{"do":"nircmd.exe setdisplay 1280 720 32 144","undo":"nircmd.exe setdisplay 2560 1440 32 144"}] `Input `__ ------------------------------------------------- @@ -712,6 +671,32 @@ keybindings `Network `__ ----------------------------------------------------- +`upnp `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Description** + Sunshine will attempt to open ports for streaming over the internet. + +**Choices** + +.. table:: + :widths: auto + + ===== =========== + Value Description + ===== =========== + on enable UPnP + off disable UPnP + ===== =========== + +**Default** + ``disabled`` + +**Example** + .. code-block:: text + + upnp = on + `address_family `__ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -798,32 +783,6 @@ keybindings origin_web_ui_allowed = lan -`upnp `__ -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -**Description** - Sunshine will attempt to open ports for streaming over the internet. - -**Choices** - -.. table:: - :widths: auto - - ===== =========== - Value Description - ===== =========== - on enable UPnP - off disable UPnP - ===== =========== - -**Default** - ``disabled`` - -**Example** - .. code-block:: text - - upnp = on - `external_ip `__ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -910,31 +869,100 @@ keybindings ping_timeout = 10000 -`Advanced `__ -------------------------------------------------------- +`Config Files `__ +-------------------------------------------------------- -`channels `__ +`file_apps `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Description** + The application configuration file path. The file contains a json formatted list of applications that can be started + by Moonlight. + +**Default** + OS and package dependent + +**Example** + .. code-block:: text + + file_apps = apps.json + +`credentials_file `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Description** + The file where user credentials for the UI are stored. + +**Default** + ``sunshine_state.json`` + +**Example** + .. code-block:: text + + credentials_file = sunshine_state.json + +`log_path `__ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Description** - This will generate distinct video streams, unlike simply broadcasting to multiple Clients. + The path where the sunshine log is stored. + +**Default** + ``sunshine.log`` + +**Example** + .. code-block:: text + + log_path = sunshine.log + +`pkey `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Description** + The private key used for the web UI and Moonlight client pairing. For best compatibility, this should be an RSA-2048 private key. - When multicasting, it could be useful to have different configurations for each connected Client. + .. warning:: Not all Moonlight clients support ECDSA keys or RSA key lengths other than 2048 bits. + +**Default** + ``credentials/cakey.pem`` + +**Example** + .. code-block:: text + + pkey = /dir/pkey.pem - For instance: +`cert `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - - Clients connected through WAN and LAN have different bitrate constraints. - - Decoders may require different settings for color. +**Description** + The certificate used for the web UI and Moonlight client pairing. For best compatibility, this should have an RSA-2048 public key. - .. warning:: CPU usage increases for each distinct video stream generated. + .. warning:: Not all Moonlight clients support ECDSA keys or RSA key lengths other than 2048 bits. **Default** - ``1`` + ``credentials/cacert.pem`` **Example** .. code-block:: text - channels = 1 + cert = /dir/cert.pem + +`file_state `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Description** + The file where current state of Sunshine is stored. + +**Default** + ``sunshine_state.json`` + +**Example** + .. code-block:: text + + file_state = sunshine_state.json + +`Advanced `__ +------------------------------------------------------- `fec_percentage `__ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -1424,6 +1452,22 @@ keybindings qsv_coder = auto +`qsv_slow_hevc `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Description** + This options enables use of HEVC on older Intel GPUs that only support low power encoding for H.264. + + .. Caution:: Streaming performance may be significantly reduced when this option is enabled. + +**Default** + ``disabled`` + +**Example** + .. code-block:: text + + qsv_slow_hevc = disabled + `AMD AMF Encoder `__ --------------------------------------------------------------------- @@ -1479,12 +1523,12 @@ keybindings =========== =========== **Default** - ``vbr_latency`` + ``cbr`` **Example** .. code-block:: text - amd_rc = vbr_latency + amd_rc = cbr `amd_usage `__ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -1499,14 +1543,15 @@ keybindings .. table:: :widths: auto - =============== =========== - Value Description - =============== =========== - transcoding transcoding (slowest) - webcam webcam (slow) - lowlatency low latency (fast) - ultralowlatency ultra low latency (fastest) - =============== =========== + ======================= =========== + Value Description + ======================= =========== + transcoding transcoding (slowest) + webcam webcam (slow) + lowlatency_high_quality low latency, high quality (fast) + lowlatency low latency (faster) + ultralowlatency ultra low latency (fastest) + ======================= =========== **Default** ``ultralowlatency`` @@ -1548,6 +1593,22 @@ keybindings amd_vbaq = enabled +`amd_enforce_hrd `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Description** + Enable Hypothetical Reference Decoder (HRD) enforcement to help constrain the target bitrate. + + .. note:: This option only applies when using amdvce `encoder`_. + +**Default** + ``enabled`` + +**Example** + .. code-block:: text + + amd_enforce_hrd = enabled + `amd_coder `__ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/source/about/guides/app_examples.rst b/docs/source/about/guides/app_examples.rst index ca834e4a4f2..8bca2bd89f0 100644 --- a/docs/source/about/guides/app_examples.rst +++ b/docs/source/about/guides/app_examples.rst @@ -6,6 +6,8 @@ and applications to Sunshine. .. attention:: Throughout these examples, any fields not shown are left blank. You can enhance your experience by adding an image or a log file (via the ``Output`` field). +.. note:: When a working directory is not specified, it defaults to the folder where the target application resides. + Common Examples --------------- @@ -24,7 +26,7 @@ Steam Big Picture ^^^^^^^^^^^^^^^^^ .. note:: Steam is launched as a detached command because Steam starts with a process that self updates itself and the original - process is killed. Since the original process ends it will not work as a regular command. + process is killed. .. tab:: Linux @@ -51,7 +53,7 @@ Steam Big Picture +----------------------+-----------------------------+ | Application Name | ``Steam Big Picture`` | +----------------------+-----------------------------+ - | Detached Commands | ``steam://open/bigpicture`` | + | Command | ``steam://open/bigpicture`` | +----------------------+-----------------------------+ | Image | ``steam.png`` | +----------------------+-----------------------------+ @@ -59,8 +61,7 @@ Steam Big Picture Epic Game Store game ^^^^^^^^^^^^^^^^^^^^ -.. note:: Using URI method will be the most consistent between various games, but does not allow a game to be launched - using the "Command" and therefore the stream will not end when the game ends. +.. note:: Using URI method will be the most consistent between various games. URI (Epic) """""""""" @@ -70,7 +71,7 @@ URI (Epic) +----------------------+--------------------------------------------------------------------------------------------------------------------------------------------+ | Application Name | ``Surviving Mars`` | +----------------------+--------------------------------------------------------------------------------------------------------------------------------------------+ - | Detached Commands | ``com.epicgames.launcher://apps/d759128018124dcabb1fbee9bb28e178%3A20729b9176c241f0b617c5723e70ec2d%3AOvenbird?action=launch&silent=true`` | + | Command | ``com.epicgames.launcher://apps/d759128018124dcabb1fbee9bb28e178%3A20729b9176c241f0b617c5723e70ec2d%3AOvenbird?action=launch&silent=true`` | +----------------------+--------------------------------------------------------------------------------------------------------------------------------------------+ Binary (Epic w/ working directory) @@ -81,7 +82,7 @@ Binary (Epic w/ working directory) +----------------------+-----------------------------------------------+ | Application Name | ``Surviving Mars`` | +----------------------+-----------------------------------------------+ - | Command | ``cmd /c "MarsEpic.exe"`` | + | Command | ``MarsEpic.exe`` | +----------------------+-----------------------------------------------+ | Working Directory | ``C:\Program Files\Epic Games\SurvivingMars`` | +----------------------+-----------------------------------------------+ @@ -100,8 +101,7 @@ Binary (Epic w/o working directory) Steam game ^^^^^^^^^^ -.. note:: Using URI method will be the most consistent between various games, but does not allow a game to be launched - using the "Command" and therefore the stream will not end when the game ends. +.. note:: Using URI method will be the most consistent between various games. URI (Steam) """"""""""" @@ -127,7 +127,7 @@ URI (Steam) +----------------------+------------------------------+ | Application Name | ``Surviving Mars`` | +----------------------+------------------------------+ - | Detached Commands | ``steam://rungameid/464920`` | + | Command | ``steam://rungameid/464920`` | +----------------------+------------------------------+ Binary (Steam w/ working directory) diff --git a/docs/source/about/setup.rst b/docs/source/about/setup.rst index 8a280fcf644..5c70c11a004 100644 --- a/docs/source/about/setup.rst +++ b/docs/source/about/setup.rst @@ -38,20 +38,21 @@ Install =========================================== ============== ============== ================================ Package CUDA Version Min Driver CUDA Compute Capabilities =========================================== ============== ============== ================================ - PKGBUILD User dependent User dependent User dependent sunshine.AppImage 11.8.0 450.80.02 35;50;52;60;61;62;70;75;80;86;90 sunshine.pkg.tar.zst 11.8.0 450.80.02 35;50;52;60;61;62;70;75;80;86;90 sunshine_{arch}.flatpak 12.0.0 525.60.13 50;52;60;61;62;70;75;80;86;90 sunshine-debian-bookworm-{arch}.deb 12.0.0 525.60.13 50;52;60;61;62;70;75;80;86;90 sunshine-debian-bullseye-{arch}.deb 11.8.0 450.80.02 35;50;52;60;61;62;70;75;80;86;90 - sunshine-fedora-38-{arch}.rpm unavailable unavailable none - sunshine-fedora-39-{arch}.rpm unavailable unavailable none - sunshine-ubuntu-20.04-{arch}.deb 11.8.0 450.80.02 35;50;52;60;61;62;70;75;80;86;90 + sunshine-fedora-38-{arch}.rpm 12.4.0 525.60.13 50;52;60;61;62;70;75;80;86;90 + sunshine-fedora-39-{arch}.rpm 12.4.0 525.60.13 50;52;60;61;62;70;75;80;86;90 sunshine-ubuntu-22.04-{arch}.deb 11.8.0 450.80.02 35;50;52;60;61;62;70;75;80;86;90 + sunshine-ubuntu-24.04-{arch}.deb 11.8.0 450.80.02 35;50;52;60;61;62;70;75;80;86;90 =========================================== ============== ============== ================================ .. tab:: AppImage + .. caution:: Use distro-specific packages instead of the AppImage if they are available. + According to AppImageLint the supported distro matrix of the AppImage is below. - ✔ Debian bullseye @@ -90,21 +91,7 @@ Install ./sunshine.AppImage --remove - .. tab:: Archlinux PKGBUILD - - #. Open terminal and run the following code. - - .. code-block:: bash - - wget https://github.com/LizardByte/Sunshine/releases/latest/download/PKGBUILD - makepkg -fi - - Uninstall: - .. code-block:: bash - - pacman -R sunshine - - .. tab:: Archlinux pkg + .. tab:: Arch Linux Package #. Open terminal and run the following code. @@ -118,7 +105,7 @@ Install pacman -R sunshine - .. tab:: Debian Package + .. tab:: Debian/Ubuntu Package #. Download ``sunshine-{distro}-{distro-version}-{arch}.deb`` and run the following code. @@ -138,6 +125,8 @@ Install .. tab:: Flatpak Package + .. caution:: Use distro-specific packages instead of the Flatpak if they are available. + .. important:: The instructions provided here are for the version supplied in the `latest release`_, which does not necessarily match the version in the Flathub repository! @@ -205,16 +194,35 @@ Install sudo dnf remove sunshine - The `deb`, `rpm`, `Flatpak` and `AppImage` packages should handle these steps automatically. + The `deb`, `rpm`, `zst`, `Flatpak` and `AppImage` packages should handle these steps automatically. Third party packages may not. Sunshine needs access to `uinput` to create mouse and gamepad events. - #. Create `udev` rules. + #. Create and reload `udev` rules for uinput. .. code-block:: bash echo 'KERNEL=="uinput", SUBSYSTEM=="misc", OPTIONS+="static_node=uinput", TAG+="uaccess"' | \ - sudo tee /etc/udev/rules.d/85-sunshine.rules + sudo tee /etc/udev/rules.d/60-sunshine.rules + sudo udevadm control --reload-rules + sudo udevadm trigger + sudo modprobe uinput + + #. Enable permissions for KMS capture. + .. warning:: Capture of most Wayland-based desktop environments will fail unless this step is performed. + + .. note:: ``cap_sys_admin`` may as well be root, except you don't need to be root to run it. It is necessary to + allow Sunshine to use KMS capture. + + **Enable** + .. code-block:: bash + + sudo setcap cap_sys_admin+p $(readlink -f $(which sunshine)) + + **Disable (for Xorg/X11 only)** + .. code-block:: bash + + sudo setcap -r $(readlink -f $(which sunshine)) #. Optionally, configure autostart service @@ -260,20 +268,6 @@ Install systemctl --user enable sunshine - #. Additional Setup for KMS - .. note:: ``cap_sys_admin`` may as well be root, except you don't need to be root to run it. It is necessary to - allow Sunshine to use KMS. - - **Enable** - .. code-block:: bash - - sudo setcap cap_sys_admin+p $(readlink -f $(which sunshine)) - - **Disable (for Xorg/X11)** - .. code-block:: bash - - sudo setcap -r $(readlink -f $(which sunshine)) - #. Reboot .. code-block:: bash @@ -281,20 +275,17 @@ Install .. tab:: macOS - .. important:: Sunshine on macOS is experimental. Gamepads do not work. Other features may not work as expected. + .. important:: Sunshine on macOS is experimental. Gamepads do not work. - .. tab:: dmg + .. tab:: Homebrew - .. warning:: The `dmg` does not include runtime dependencies. This package is not recommended for most users. - No support will be provided! + #. Install `Homebrew `__ + #. Update the Homebrew sources and install Sunshine. - #. Download the ``sunshine.dmg`` file and install it. - - Uninstall: .. code-block:: bash - cd /etc/sunshine/assets - uninstall_pkg.sh + brew tap LizardByte/homebrew + brew install sunshine .. tab:: Portfile @@ -316,7 +307,7 @@ Install mkdir -p ~/ports/multimedia/sunshine cd ~/ports/multimedia/sunshine - curl -O https://github.com/LizardByte/Sunshine/releases/latest/download/Portfile + curl -OL https://github.com/LizardByte/Sunshine/releases/latest/download/Portfile cd ~/ports portindex sudo port install sunshine diff --git a/docs/source/building/linux.rst b/docs/source/building/linux.rst index 789409e4174..d263a7d07fd 100644 --- a/docs/source/building/linux.rst +++ b/docs/source/building/linux.rst @@ -15,7 +15,6 @@ Install Requirements sudo apt update && sudo apt install \ build-essential \ cmake \ - libavdevice-dev \ libayatana-appindicator3-dev \ libboost-filesystem-dev \ libboost-locale-dev \ @@ -88,9 +87,8 @@ Install Requirements wget \ # necessary for cuda install with `run` file which # necessary for cuda install with `run` file -Ubuntu 20.04 +Ubuntu 22.04 ^^^^^^^^^^^^ -End of Life: April 2030 Install Requirements .. code-block:: bash @@ -98,9 +96,7 @@ Install Requirements sudo apt update && sudo apt install \ build-essential \ cmake \ - g++-10 \ - libayatana-appindicator3-dev \ - libavdevice-dev \ + libappindicator3-dev \ libboost-filesystem-dev \ libboost-locale-dev \ libboost-log-dev \ @@ -117,7 +113,6 @@ Install Requirements libpulse-dev \ libssl-dev \ libva-dev \ # VA-API - libvdpau-dev \ libwayland-dev \ # Wayland libx11-dev \ # X11 libxcb-shm0-dev \ # X11 @@ -128,21 +123,11 @@ Install Requirements libxtst-dev \ # X11 nodejs \ npm \ - wget # necessary for cuda install with `run` file - -Update gcc alias - .. code-block:: bash - - update-alternatives --install \ - /usr/bin/gcc gcc /usr/bin/gcc-10 100 \ - --slave /usr/bin/g++ g++ /usr/bin/g++-10 \ - --slave /usr/bin/gcov gcov /usr/bin/gcov-10 \ - --slave /usr/bin/gcc-ar gcc-ar /usr/bin/gcc-ar-10 \ - --slave /usr/bin/gcc-ranlib gcc-ranlib /usr/bin/gcc-ranlib-10 + nvidia-cuda-dev \ # CUDA, NvFBC + nvidia-cuda-toolkit # CUDA, NvFBC -Ubuntu 22.04 +Ubuntu 24.04 ^^^^^^^^^^^^ -End of Life: April 2027 Install Requirements .. code-block:: bash @@ -150,8 +135,9 @@ Install Requirements sudo apt update && sudo apt install \ build-essential \ cmake \ + gcc-11 \ + g++-11 \ libappindicator3-dev \ - libavdevice-dev \ libboost-filesystem-dev \ libboost-locale-dev \ libboost-log-dev \ @@ -181,11 +167,22 @@ Install Requirements nvidia-cuda-dev \ # CUDA, NvFBC nvidia-cuda-toolkit # CUDA, NvFBC +Update gcc alias + .. code-block:: bash + + update-alternatives --install \ + /usr/bin/gcc gcc /usr/bin/gcc-11 100 \ + --slave /usr/bin/g++ g++ /usr/bin/g++-11 \ + --slave /usr/bin/gcov gcov /usr/bin/gcov-11 \ + --slave /usr/bin/gcc-ar gcc-ar /usr/bin/gcc-ar-11 \ + --slave /usr/bin/gcc-ranlib gcc-ranlib /usr/bin/gcc-ranlib-11 + CUDA ---- If the version of CUDA available from your distro is not adequate, manually install CUDA. .. tip:: The version of CUDA you use will determine compatibility with various GPU generations. + At the time of writing, the recommended version to use is CUDA ~11.8. See `CUDA compatibility `__ for more info. Select the appropriate run file based on your desired CUDA version and architecture according to diff --git a/docs/source/building/macos.rst b/docs/source/building/macos.rst index c14751c28f8..1b874d71a11 100644 --- a/docs/source/building/macos.rst +++ b/docs/source/building/macos.rst @@ -12,17 +12,31 @@ MacPorts Install Requirements .. code-block:: bash - sudo port install avahi boost180 cmake curl libopus miniupnpc npm9 pkgconfig + sudo port install avahi boost180 cmake curl doxygen graphviz libopus miniupnpc npm9 pkgconfig python311 py311-pip Homebrew """""""" Install Requirements .. code-block:: bash - brew install boost cmake miniupnpc node opus pkg-config - # if there are issues with an SSL header that is not found: - cd /usr/local/include - ln -s ../opt/openssl/include/openssl . + brew install boost cmake doxygen graphviz miniupnpc node opus pkg-config python@3.11 + +If there are issues with an SSL header that is not found: + .. tab:: Intel + + .. code-block:: bash + + pushd /usr/local/include + ln -s ../opt/openssl/include/openssl . + popd + + .. tab:: Apple Silicon + + .. code-block:: bash + + pushd /opt/homebrew/include + ln -s ../opt/openssl/include/openssl . + popd Build ----- @@ -31,7 +45,7 @@ Build .. code-block:: bash cmake .. - make -j ${nproc} + make -j $(sysctl -n hw.ncpu) cpack -G DragNDrop # optionally, create a macOS dmg package diff --git a/docs/source/building/windows.rst b/docs/source/building/windows.rst index 8e83c58e99d..90b3d9e3c33 100644 --- a/docs/source/building/windows.rst +++ b/docs/source/building/windows.rst @@ -18,6 +18,7 @@ Install dependencies: base-devel \ cmake \ diffutils \ + doxygen \ gcc \ git \ make \ @@ -25,12 +26,17 @@ Install dependencies: mingw-w64-x86_64-boost \ mingw-w64-x86_64-cmake \ mingw-w64-x86_64-curl \ + mingw-w64-x86_64-graphviz \ mingw-w64-x86_64-miniupnpc \ + mingw-w64-x86_64-nlohmann-json \ mingw-w64-x86_64-nodejs \ mingw-w64-x86_64-onevpl \ mingw-w64-x86_64-openssl \ mingw-w64-x86_64-opus \ - mingw-w64-x86_64-toolchain + mingw-w64-x86_64-rust \ + mingw-w64-x86_64-toolchain \ + python \ + python-pip Build ----- diff --git a/docs/source/conf.py b/docs/source/conf.py index c2b66a56635..42fb91eb78e 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -7,7 +7,6 @@ # standard imports from datetime import datetime import os -import re import subprocess @@ -27,16 +26,8 @@ author = 'ReenigneArcher' # The full version, including alpha/beta/rc tags -with open(os.path.join(root_dir, 'CMakeLists.txt'), 'r') as f: - version = re.search(r"project\(Sunshine VERSION ((\d+)\.(\d+)\.(\d+))", str(f.read())).group(1) -""" -To use cmake method for obtaining version instead of regex, -1. Within CMakeLists.txt add the following line without backticks: - ``configure_file(docs/source/conf.py.in "${CMAKE_CURRENT_SOURCE_DIR}/docs/source/conf.py" @ONLY)`` -2. Rename this file to ``conf.py.in`` -3. Uncomment the next line -""" -# version = '@PROJECT_VERSION@' # use this for cmake configure_file method +# https://docs.readthedocs.io/en/stable/reference/environment-variables.html#envvar-READTHEDOCS_VERSION +version = os.getenv('READTHEDOCS_VERSION', 'dirty') # -- General configuration --------------------------------------------------- @@ -105,6 +96,17 @@ doxy_version = doxy_proc.stdout.decode('utf-8').strip() print('doxygen version: ' + doxy_version) +# create build directories, as doxygen fails to create it in macports and docker +directories = [ + os.path.join(source_dir, 'build'), + os.path.join(source_dir, 'build', 'doxyxml'), +] +for d in directories: + os.makedirs( + name=d, + exist_ok=True, + ) + # run doxygen doxy_proc = subprocess.run('doxygen Doxyfile', shell=True, cwd=source_dir) if doxy_proc.returncode != 0: diff --git a/docs/source/contributing/localization.rst b/docs/source/contributing/localization.rst index 2ca912805e1..21cd330e487 100644 --- a/docs/source/contributing/localization.rst +++ b/docs/source/contributing/localization.rst @@ -30,42 +30,84 @@ localization there. Extraction ---------- -There should be minimal cases where strings need to be extracted from source code; however it may be necessary in some -situations. For example if a system tray icon is added it should be localized as it is user interfacing. -- Wrap the string to be extracted in a function as shown. - .. code-block:: cpp +.. tab:: UI - #include - #include + Sunshine uses `Vue I18n `__ for localizing the UI. + The following is a simple example of how to use it. - std::string msg = boost::locale::translate("Hello world!"); + - Add the string to `src_assets/common/assets/web/public/assets/locale/en.json`, in English. + .. code-block:: json -.. tip:: More examples can be found in the documentation for - `boost locale `__. + { + "index": { + "welcome": "Hello, Sunshine!" + } + } -.. warning:: This is for information only. Contributors should never include manually updated template files, or - manually compiled language files in Pull Requests. + .. note:: The json keys should be sorted alphabetically. You can use `jsonabc `__ + to sort the keys. -Strings are automatically extracted from the code to the `locale/sunshine.po` template file. The generated file is -used by CrowdIn to generate language specific template files. The file is generated using the -`.github/workflows/localize.yml` workflow and is run on any push event into the `nightly` branch. Jobs are only run if -any of the following paths are modified. + .. attention:: Due to the integration with Crowdin, it is important to only add strings to the `en.json` file, + and to not modify any other language files. After the PR is merged, the translations can take place + on `CrowdIn `__. Once the translations are complete, a PR will be made + to merge the translations into Sunshine. -.. code-block:: yaml + - Use the string in a Vue component. + .. code-block:: html - - 'src/**' + -When testing locally it may be desirable to manually extract, initialize, update, and compile strings. Python is -required for this, along with the python dependencies in the `./scripts/requirements.txt` file. Additionally, -`xgettext `__ must be installed. + .. tip:: More formatting examples can be found in the + `Vue I18n guide `__. -**Extract, initialize, and update** - .. code-block:: bash +.. tab:: C++ - python ./scripts/_locale.py --extract --init --update + There should be minimal cases where strings need to be extracted from C++ source code; however it may be necessary in + some situations. For example the system tray icon could be localized as it is user interfacing. -**Compile** - .. code-block:: bash + - Wrap the string to be extracted in a function as shown. + .. code-block:: cpp - python ./scripts/_locale.py --compile + #include + #include + + std::string msg = boost::locale::translate("Hello world!"); + + .. tip:: More examples can be found in the documentation for + `boost locale `__. + + .. warning:: This is for information only. Contributors should never include manually updated template files, or + manually compiled language files in Pull Requests. + + Strings are automatically extracted from the code to the `locale/sunshine.po` template file. The generated file is + used by CrowdIn to generate language specific template files. The file is generated using the + `.github/workflows/localize.yml` workflow and is run on any push event into the `nightly` branch. Jobs are only run if + any of the following paths are modified. + + .. code-block:: yaml + + - 'src/**' + + When testing locally it may be desirable to manually extract, initialize, update, and compile strings. Python is + required for this, along with the python dependencies in the `./scripts/requirements.txt` file. Additionally, + `xgettext `__ must be installed. + + **Extract, initialize, and update** + .. code-block:: bash + + python ./scripts/_locale.py --extract --init --update + + **Compile** + .. code-block:: bash + + python ./scripts/_locale.py --compile + + .. attention:: Due to the integration with Crowdin, it is important to not include any extracted or compiled files in + Pull Requests. The files are automatically generated and updated by the workflow. Once the PR is merged, the + translations can take place on `CrowdIn `__. Once the translations are + complete, a PR will be made to merge the translations into Sunshine. diff --git a/docs/source/contributing/testing.rst b/docs/source/contributing/testing.rst index e4b3ae10502..2d9f6290d3f 100644 --- a/docs/source/contributing/testing.rst +++ b/docs/source/contributing/testing.rst @@ -59,5 +59,81 @@ Format inplace with rstfmt Unit Testing ------------ -.. todo:: Sunshine does not currently have any unit tests. If you would like to help us improve please get in contact - with us, or make a PR with suggested changes. +Sunshine uses `Google Test `__ for unit testing. Google Test is included in the +repo as a submodule. The test sources are located in the `./tests` directory. + +The tests need to be compiled into an executable, and then run. The tests are built using the normal build process, but +can be disabled by setting the `BUILD_TESTS` CMake option to `OFF`. + +To run the tests, execute the following command from the build directory: + +.. tab:: Linux + + .. code-block:: bash + + pushd tests + ./test_sunshine + popd + +.. tab:: macOS + + .. code-block:: bash + + pushd tests + ./test_sunshine + popd + +.. tab:: Windows + + .. code-block:: bash + + pushd tests + test_sunshine.exe + popd + +To see all available options, run the tests with the `--help` option. + +.. tab:: Linux + + .. code-block:: bash + + pushd tests + ./test_sunshine --help + popd + +.. tab:: macOS + + .. code-block:: bash + + pushd tests + ./test_sunshine --help + popd + +.. tab:: Windows + + .. code-block:: bash + + pushd tests + test_sunshine.exe --help + popd + +Some tests rely on Python to run. CMake will search for Python and enable the docs tests if it is found, otherwise +cmake will fail. You can manually disable the tests by setting the `TESTS_ENABLE_PYTHON_TESTS` CMake option to +`OFF`. + +.. tip:: + + See the googletest `FAQ `__ for more information on how to use + Google Test. + +We use `gcovr `__ to generate code coverage reports, +and `Codecov `__ to analyze the reports for all PRs and commits. + +Codecov will fail a PR if the total coverage is reduced too much, or if not enough of the diff is covered by tests. +In some cases, the code cannot be covered when running the tests inside of GitHub runners. For example, any test that +needs access to the GPU will not be able to run. In these cases, the coverage can be omitted by adding comments to the +code. See the `gcovr documentation `__ for +more information. + +Even if your changes cannot be covered in the CI, we still encourage you to write the tests for them. This will allow +maintainers to run the tests locally. diff --git a/docs/source/source_code/src/entry_handler.rst b/docs/source/source_code/src/entry_handler.rst new file mode 100644 index 00000000000..c522b065652 --- /dev/null +++ b/docs/source/source_code/src/entry_handler.rst @@ -0,0 +1,5 @@ +entry_handler +============= + +.. doxygenfile:: entry_handler.h + :allow-dot-graphs: diff --git a/docs/source/source_code/src/globals.rst b/docs/source/source_code/src/globals.rst new file mode 100644 index 00000000000..ed70cecf692 --- /dev/null +++ b/docs/source/source_code/src/globals.rst @@ -0,0 +1,5 @@ +globals +======= + +.. doxygenfile:: globals.h + :allow-dot-graphs: diff --git a/package.json b/package.json index 4a709402bb3..db309cb7ec9 100644 --- a/package.json +++ b/package.json @@ -4,12 +4,13 @@ "dev": "vite build --watch" }, "dependencies": { - "@fortawesome/fontawesome-free": "6.5.1", + "@fortawesome/fontawesome-free": "6.5.2", "@popperjs/core": "2.11.8", "@vitejs/plugin-vue": "4.6.2", - "bootstrap": "5.3.2", + "bootstrap": "5.3.3", "vite": "4.5.2", "vite-plugin-ejs": "1.6.4", - "vue": "3.4.5" + "vue": "3.4.5", + "vue-i18n": "9.11.0" } } diff --git a/packaging/linux/AppImage/AppRun b/packaging/linux/AppImage/AppRun index ddc5fd38455..404704c34d3 100644 --- a/packaging/linux/AppImage/AppRun +++ b/packaging/linux/AppImage/AppRun @@ -46,7 +46,9 @@ echo " function install() { # user input rules # shellcheck disable=SC2002 - cat "$SUNSHINE_SHARE_HERE/udev/rules.d/85-sunshine.rules" | sudo tee /etc/udev/rules.d/85-sunshine.rules + cat "$SUNSHINE_SHARE_HERE/udev/rules.d/60-sunshine.rules" | sudo tee /etc/udev/rules.d/60-sunshine.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --property-match=DEVNAME=/dev/uinput # sunshine service mkdir -p ~/.config/systemd/user @@ -56,30 +58,11 @@ function install() { # setcap sudo setcap cap_sys_admin+p "$(readlink -f "$SUNSHINE_BIN_HERE")" - - while true - do - read -r -p "This installation requires a reboot. Do you want to reboot NOW? [y/n] " input - - case $input in - [yY][eE][sS]|[yY]) - echo "Yes" - sudo reboot now - ;; - [nN][oO]|[nN]) - echo "No" - break - ;; - *) - echo "Invalid input..." - ;; - esac - done } function remove() { # remove input rules - sudo rm -f /etc/udev/rules.d/85-sunshine.rules + sudo rm -f /etc/udev/rules.d/60-sunshine.rules # remove service sudo rm -f ~/.config/systemd/user/sunshine.service diff --git a/packaging/linux/Arch/PKGBUILD b/packaging/linux/Arch/PKGBUILD index a0ceb6b5684..6fde7fe5a90 100644 --- a/packaging/linux/Arch/PKGBUILD +++ b/packaging/linux/Arch/PKGBUILD @@ -7,12 +7,15 @@ pkgrel=1 pkgdesc="@PROJECT_DESCRIPTION@" arch=('x86_64' 'aarch64') url=@PROJECT_HOMEPAGE_URL@ -license=('GPL3') +license=('GPL-3.0-only') +install=sunshine.install depends=('avahi' 'boost-libs' 'curl' 'libayatana-appindicator' + 'libcap' + 'libdrm' 'libevdev' 'libmfx' 'libnotify' @@ -28,16 +31,21 @@ depends=('avahi' 'numactl' 'openssl' 'opus' + 'python' 'udev') +checkdepends=('doxygen' + 'graphviz') makedepends=('boost' 'cmake' + 'gcc12' 'git' 'make' 'nodejs' 'npm') -optdepends=('cuda: NvFBC capture support' - 'libcap' - 'libdrm') +optdepends=('cuda: Nvidia GPU encoding support' + 'libva-mesa-driver: AMD GPU encoding support' + 'intel-media-driver: Intel GPU encoding support' + 'xorg-server-xvfb: Virtual X server for headless testing') provides=('sunshine') @@ -54,6 +62,9 @@ build() { export BUILD_VERSION="@GITHUB_BUILD_VERSION@" export COMMIT="@GITHUB_COMMIT@" + export CC=gcc-12 + export CXX=g++-12 + export CFLAGS="${CFLAGS/-Werror=format-security/}" export CXXFLAGS="${CXXFLAGS/-Werror=format-security/}" @@ -61,6 +72,7 @@ build() { -S "$pkgname" \ -B build \ -Wno-dev \ + -D BUILD_WERROR=ON \ -D CMAKE_INSTALL_PREFIX=/usr \ -D SUNSHINE_EXECUTABLE_PATH=/usr/bin/sunshine \ -D SUNSHINE_ASSETS_DIR="share/sunshine" @@ -68,6 +80,14 @@ build() { make -C build } +check() { + export CC=gcc-12 + export CXX=g++-12 + + cd "${srcdir}/build/tests" + ./test_sunshine --gtest_color=yes +} + package() { make -C build install DESTDIR="$pkgdir" } diff --git a/packaging/linux/Arch/sunshine.install b/packaging/linux/Arch/sunshine.install new file mode 100644 index 00000000000..a8a700f1f1c --- /dev/null +++ b/packaging/linux/Arch/sunshine.install @@ -0,0 +1,20 @@ +do_setcap() { + setcap cap_sys_admin+p $(readlink -f $(which sunshine)) +} + +do_udev_reload() { + udevadm control --reload-rules + udevadm trigger --property-match=DEVNAME=/dev/uinput + modprobe uinput || true +} + +post_install() { + do_setcap + do_udev_reload +} + +post_upgrade() { + do_setcap + do_udev_reload +} + diff --git a/packaging/linux/flatpak/deps/org.flatpak.Builder.BaseApp b/packaging/linux/flatpak/deps/org.flatpak.Builder.BaseApp new file mode 160000 index 00000000000..6e295e63074 --- /dev/null +++ b/packaging/linux/flatpak/deps/org.flatpak.Builder.BaseApp @@ -0,0 +1 @@ +Subproject commit 6e295e630740ae8ef82c6291724e709b36477042 diff --git a/packaging/linux/flatpak/deps/shared-modules b/packaging/linux/flatpak/deps/shared-modules new file mode 160000 index 00000000000..d0229951ac2 --- /dev/null +++ b/packaging/linux/flatpak/deps/shared-modules @@ -0,0 +1 @@ +Subproject commit d0229951ac23967c4f5697bd7b5c1bd7e641b8c3 diff --git a/packaging/linux/flatpak/dev.lizardbyte.sunshine.yml b/packaging/linux/flatpak/dev.lizardbyte.sunshine.yml index b62f0e1d667..0e891dacf0e 100644 --- a/packaging/linux/flatpak/dev.lizardbyte.sunshine.yml +++ b/packaging/linux/flatpak/dev.lizardbyte.sunshine.yml @@ -11,6 +11,7 @@ separate-locales: false finish-args: - --device=all # access all devices - --env=PULSE_PROP_media.category=Manager # allow sunshine to manage audio sinks + - --env=SUNSHINE_MIGRATE_CONFIG=1 # migrate config files to the new location - --filesystem=home # need to save files in user's home directory - --share=ipc # required for X11 shared memory extension - --share=network # access network @@ -33,6 +34,8 @@ build-options: prepend-ld-library-path: /usr/lib/sdk/vala/lib modules: + - "org.flatpak.Builder.BaseApp/xvfb.json" + - name: boost disabled: false buildsystem: simple @@ -325,11 +328,11 @@ modules: append-path: /usr/lib/sdk/node18/bin build-args: - --share=network - cxxflags: -I${C_INCLUDE_PATH}/libevdev-1.0 env: npm_config_nodedir: /usr/lib/sdk/node18 NPM_CONFIG_LOGLEVEL: info config-opts: + - -DBUILD_WERROR=ON - -DCMAKE_BUILD_TYPE=Release - -DCMAKE_INSTALL_PREFIX=/app - -DCMAKE_CUDA_COMPILER=/app/cuda/bin/nvcc @@ -340,6 +343,7 @@ modules: - -DSUNSHINE_ENABLE_DRM=ON - -DSUNSHINE_ENABLE_CUDA=ON - -DSUNSHINE_BUILD_FLATPAK=ON + - -DTESTS_ENABLE_PYTHON_TESTS=OFF sources: - type: git url: "@GITHUB_CLONE_URL@" @@ -357,3 +361,7 @@ modules: 's%/app/bin/sunshine%flatpak run dev.lizardbyte.sunshine\nExecStop=flatpak kill dev.lizardbyte.sunshine%g' /app/share/sunshine/systemd/user/sunshine.service - install -D $FLATPAK_BUILDER_BUILDDIR/packaging/linux/flatpak/scripts/* /app/bin + run-tests: true + test-rule: "" # empty to disable + test-commands: + - xvfb-run tests/test_sunshine --gtest_color=yes diff --git a/packaging/linux/flatpak/scripts/additional-install.sh b/packaging/linux/flatpak/scripts/additional-install.sh index 8a905b53810..a27db4e09ba 100644 --- a/packaging/linux/flatpak/scripts/additional-install.sh +++ b/packaging/linux/flatpak/scripts/additional-install.sh @@ -7,7 +7,7 @@ echo Sunshine User Service has been installed. echo Use [systemctl --user enable sunshine] once to autostart Sunshine on login. # Udev rule -UDEV=$(cat /app/share/sunshine/udev/rules.d/85-sunshine.rules) +UDEV=$(cat /app/share/sunshine/udev/rules.d/60-sunshine.rules) echo Configuring mouse permission. -flatpak-spawn --host pkexec sh -c "echo '$UDEV' > /etc/udev/rules.d/85-sunshine.rules" +flatpak-spawn --host pkexec sh -c "echo '$UDEV' > /etc/udev/rules.d/60-sunshine.rules" echo Restart computer for mouse permission to take effect. diff --git a/packaging/linux/flatpak/scripts/remove-additional-install.sh b/packaging/linux/flatpak/scripts/remove-additional-install.sh index 6148f62ea1e..0d13baeb62c 100644 --- a/packaging/linux/flatpak/scripts/remove-additional-install.sh +++ b/packaging/linux/flatpak/scripts/remove-additional-install.sh @@ -7,5 +7,5 @@ systemctl --user daemon-reload echo Sunshine User Service has been removed. # Udev rule -flatpak-spawn --host pkexec sh -c "rm /etc/udev/rules.d/85-sunshine.rules" +flatpak-spawn --host pkexec sh -c "rm /etc/udev/rules.d/60-sunshine.rules" echo Mouse permission removed. Restart computer to take effect. diff --git a/packaging/linux/flatpak/sunshine.desktop b/packaging/linux/flatpak/sunshine.desktop index be702701e08..1c5fe13a409 100644 --- a/packaging/linux/flatpak/sunshine.desktop +++ b/packaging/linux/flatpak/sunshine.desktop @@ -12,9 +12,9 @@ Actions=RunInTerminal;KMS; [Desktop Action RunInTerminal] Name=Run in Terminal Icon=application-x-executable -Exec=gio launch @CMAKE_INSTALL_DATAROOTDIR@/applications/sunshine_terminal.desktop +Exec=gio launch @CMAKE_INSTALL_FULL_DATAROOTDIR@/applications/sunshine_terminal.desktop [Desktop Action KMS] Name=Run in Terminal (KMS) Icon=application-x-executable -Exec=gio launch @CMAKE_INSTALL_DATAROOTDIR@/applications/sunshine_kms.desktop +Exec=gio launch @CMAKE_INSTALL_FULL_DATAROOTDIR@/applications/sunshine_kms.desktop diff --git a/packaging/linux/sunshine.desktop b/packaging/linux/sunshine.desktop index b0f2ce327ec..719555301d5 100644 --- a/packaging/linux/sunshine.desktop +++ b/packaging/linux/sunshine.desktop @@ -12,4 +12,4 @@ Actions=RunInTerminal; [Desktop Action RunInTerminal] Name=Run in Terminal Icon=application-x-executable -Exec=gio launch @CMAKE_INSTALL_DATAROOTDIR@/applications/sunshine_terminal.desktop +Exec=gio launch @CMAKE_INSTALL_FULL_DATAROOTDIR@/applications/sunshine_terminal.desktop diff --git a/packaging/macos/Portfile b/packaging/macos/Portfile index 19aeccf2dbd..aacc3209fff 100644 --- a/packaging/macos/Portfile +++ b/packaging/macos/Portfile @@ -31,16 +31,24 @@ post-fetch { system -W ${worksrcpath} "${git.cmd} submodule update --init --recursive" } +# https://guide.macports.org/chunked/reference.dependencies.html +depends_build-append port:npm9 \ + port:pkgconfig + depends_lib port:avahi \ port:curl \ port:libopus \ port:miniupnpc \ - port:npm9 \ - port:pkgconfig + port:python311 \ + port:py311-pip + +depends_test port:doxygen \ + port:graphviz boost.version 1.81 -configure.args -DCMAKE_INSTALL_PREFIX=${prefix} \ +configure.args -DBUILD_WERROR=ON \ + -DCMAKE_INSTALL_PREFIX=${prefix} \ -DSUNSHINE_ASSETS_DIR=etc/sunshine/assets startupitem.create yes @@ -60,3 +68,9 @@ notes-append "Run @PROJECT_NAME@ by executing 'sunshine ', notes-append "The config file will be created if it doesn't exist." notes-append "It is recommended to set a location for the apps file in the config." notes-append "See our documentation at 'https://docs.lizardbyte.dev/projects/sunshine/en/v@PROJECT_VERSION@/' for further info." + +test.run yes +test.dir ${build.dir}/tests +test.target "" +test.cmd ./test_sunshine +test.args --gtest_color=yes diff --git a/packaging/macos/sunshine.rb b/packaging/macos/sunshine.rb new file mode 100644 index 00000000000..1853c0c8c75 --- /dev/null +++ b/packaging/macos/sunshine.rb @@ -0,0 +1,65 @@ +require "language/node" + +class @PROJECT_NAME@ < Formula + desc "@PROJECT_DESCRIPTION@" + homepage "@PROJECT_HOMEPAGE_URL@" + url "@GITHUB_CLONE_URL@", + tag: "@GITHUB_BRANCH@" + version "@PROJECT_VERSION@" + license all_of: ["GPL-3.0-only"] + head "@GITHUB_CLONE_URL@", branch: "@GITHUB_DEFAULT_BRANCH@" + + depends_on "boost" => :build + depends_on "cmake" => :build + depends_on "node" => :build + depends_on "pkg-config" => :build + depends_on "curl" + depends_on "miniupnpc" + depends_on "openssl" + depends_on "opus" + + def install + args = %W[ + -DBUILD_WERROR=ON + -DCMAKE_INSTALL_PREFIX=#{prefix} + -DOPENSSL_ROOT_DIR=#{Formula["openssl"].opt_prefix} + -DSUNSHINE_ASSETS_DIR=sunshine/assets + -DSUNSHINE_BUILD_HOMEBREW=ON + -DTESTS_ENABLE_PYTHON_TESTS=OFF + ] + system "cmake", "-S", ".", "-B", "build", *std_cmake_args, *args + + cd "build" do + system "make", "-j" + system "make", "install" + bin.install "tests/test_sunshine" + end + end + + service do + run [opt_bin/"sunshine", "~/.config/sunshine/sunshine.conf"] + end + + def caveats + <<~EOS + Thanks for installing @PROJECT_NAME@! + + To get started, review the documentation at: + https://docs.lizardbyte.dev/projects/sunshine/en/latest/ + + Sunshine can only access microphones on macOS due to system limitations. + To stream system audio use "Soundflower" or "BlackHole". + + Gamepads are not currently supported on macOS. + EOS + end + + test do + # test that the binary runs at all + system "#{bin}/sunshine", "--version" + + # run the test suite + # cannot build tests with python tests because homebrew destroys the source directory + system "#{bin}/test_sunshine", "--gtest_color=yes" + end +end diff --git a/scripts/_locale.py b/scripts/_locale.py index d967974e3f5..884805702e5 100644 --- a/scripts/_locale.py +++ b/scripts/_locale.py @@ -22,16 +22,19 @@ year = datetime.datetime.now().year -# retroarcher target locales +# target locales target_locales = [ - 'de', # Deutsch + 'de', # German 'en', # English 'en_GB', # English (United Kingdom) 'en_US', # English (United States) - 'es', # español - 'fr', # français - 'it', # italiano - 'ru', # русский + 'es', # Spanish + 'fr', # French + 'it', # Italian + 'ja', # Japanese + 'ru', # Russian + 'sv', # Swedish + 'zh', # Chinese ] diff --git a/scripts/update_clang_format.py b/scripts/update_clang_format.py index 8cf9b9f0d97..9e0dacda847 100644 --- a/scripts/update_clang_format.py +++ b/scripts/update_clang_format.py @@ -5,6 +5,7 @@ # variables directories = [ 'src', + 'tests', 'tools', os.path.join('third-party', 'glad'), os.path.join('third-party', 'nvfbc'), diff --git a/src/audio.cpp b/src/audio.cpp index a3555eaa080..1995e380ea7 100644 --- a/src/audio.cpp +++ b/src/audio.cpp @@ -10,8 +10,8 @@ #include "audio.h" #include "config.h" +#include "globals.h" #include "logging.h" -#include "main.h" #include "thread_safe.h" #include "utility.h" diff --git a/src/config.cpp b/src/config.cpp index 5cd08cee94e..ab6dbfb5c37 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -15,9 +16,9 @@ #include #include "config.h" +#include "entry_handler.h" #include "file_handler.h" #include "logging.h" -#include "main.h" #include "nvhttp.h" #include "rtsp.h" #include "utility.h" @@ -84,14 +85,17 @@ namespace config { #define AMF_VIDEO_ENCODER_AV1_USAGE_LOW_LATENCY 1 #define AMF_VIDEO_ENCODER_AV1_USAGE_ULTRA_LOW_LATENCY 2 #define AMF_VIDEO_ENCODER_AV1_USAGE_WEBCAM 3 - #define AMF_VIDEO_ENCODER_HEVC_USAGE_TRANSCONDING 0 + #define AMF_VIDEO_ENCODER_AV1_USAGE_LOW_LATENCY_HIGH_QUALITY 5 + #define AMF_VIDEO_ENCODER_HEVC_USAGE_TRANSCODING 0 #define AMF_VIDEO_ENCODER_HEVC_USAGE_ULTRA_LOW_LATENCY 1 #define AMF_VIDEO_ENCODER_HEVC_USAGE_LOW_LATENCY 2 #define AMF_VIDEO_ENCODER_HEVC_USAGE_WEBCAM 3 - #define AMF_VIDEO_ENCODER_USAGE_TRANSCONDING 0 + #define AMF_VIDEO_ENCODER_HEVC_USAGE_LOW_LATENCY_HIGH_QUALITY 5 + #define AMF_VIDEO_ENCODER_USAGE_TRANSCODING 0 #define AMF_VIDEO_ENCODER_USAGE_ULTRA_LOW_LATENCY 1 #define AMF_VIDEO_ENCODER_USAGE_LOW_LATENCY 2 #define AMF_VIDEO_ENCODER_USAGE_WEBCAM 3 + #define AMF_VIDEO_ENCODER_USAGE_LOW_LATENCY_HIGH_QUALITY 5 #define AMF_VIDEO_ENCODER_UNDEFINED 0 #define AMF_VIDEO_ENCODER_CABAC 1 #define AMF_VIDEO_ENCODER_CALV 2 @@ -143,20 +147,23 @@ namespace config { enum class usage_av1_e : int { transcoding = AMF_VIDEO_ENCODER_AV1_USAGE_TRANSCODING, webcam = AMF_VIDEO_ENCODER_AV1_USAGE_WEBCAM, + lowlatency_high_quality = AMF_VIDEO_ENCODER_AV1_USAGE_LOW_LATENCY_HIGH_QUALITY, lowlatency = AMF_VIDEO_ENCODER_AV1_USAGE_LOW_LATENCY, ultralowlatency = AMF_VIDEO_ENCODER_AV1_USAGE_ULTRA_LOW_LATENCY }; enum class usage_hevc_e : int { - transcoding = AMF_VIDEO_ENCODER_HEVC_USAGE_TRANSCONDING, + transcoding = AMF_VIDEO_ENCODER_HEVC_USAGE_TRANSCODING, webcam = AMF_VIDEO_ENCODER_HEVC_USAGE_WEBCAM, + lowlatency_high_quality = AMF_VIDEO_ENCODER_HEVC_USAGE_LOW_LATENCY_HIGH_QUALITY, lowlatency = AMF_VIDEO_ENCODER_HEVC_USAGE_LOW_LATENCY, ultralowlatency = AMF_VIDEO_ENCODER_HEVC_USAGE_ULTRA_LOW_LATENCY }; enum class usage_h264_e : int { - transcoding = AMF_VIDEO_ENCODER_USAGE_TRANSCONDING, + transcoding = AMF_VIDEO_ENCODER_USAGE_TRANSCODING, webcam = AMF_VIDEO_ENCODER_USAGE_WEBCAM, + lowlatency_high_quality = AMF_VIDEO_ENCODER_USAGE_LOW_LATENCY_HIGH_QUALITY, lowlatency = AMF_VIDEO_ENCODER_USAGE_LOW_LATENCY, ultralowlatency = AMF_VIDEO_ENCODER_USAGE_ULTRA_LOW_LATENCY }; @@ -194,12 +201,13 @@ namespace config { template std::optional - usage_from_view(const std::string_view &rc) { + usage_from_view(const std::string_view &usage) { #define _CONVERT_(x) \ - if (rc == #x##sv) return (int) T::x + if (usage == #x##sv) return (int) T::x _CONVERT_(transcoding); _CONVERT_(webcam); _CONVERT_(lowlatency); + _CONVERT_(lowlatency_high_quality); _CONVERT_(ultralowlatency); #undef _CONVERT_ return std::nullopt; @@ -338,20 +346,22 @@ namespace config { { qsv::medium, // preset qsv::_auto, // cavlc + false, // slow_hevc }, // qsv { (int) amd::quality_h264_e::balanced, // quality (h264) (int) amd::quality_hevc_e::balanced, // quality (hevc) (int) amd::quality_av1_e::balanced, // quality (av1) - (int) amd::rc_h264_e::vbr_latency, // rate control (h264) - (int) amd::rc_hevc_e::vbr_latency, // rate control (hevc) - (int) amd::rc_av1_e::vbr_latency, // rate control (av1) + (int) amd::rc_h264_e::cbr, // rate control (h264) + (int) amd::rc_hevc_e::cbr, // rate control (hevc) + (int) amd::rc_av1_e::cbr, // rate control (av1) (int) amd::usage_h264_e::ultralowlatency, // usage (h264) (int) amd::usage_hevc_e::ultralowlatency, // usage (hevc) (int) amd::usage_av1_e::ultralowlatency, // usage (av1) 0, // preanalysis 1, // vbaq + 1, // enforce_hrd (int) amd::coder_e::_auto, // coder }, // amd @@ -439,6 +449,7 @@ namespace config { }; sunshine_t sunshine { + "en", // locale 2, // min_log_level 0, // flags {}, // User file @@ -847,6 +858,11 @@ namespace config { std::vector list; list_string_f(vars, name, list); + // check if list is empty, i.e. when the value doesn't exist in the config file + if (list.empty()) { + return; + } + // The framerate list must be cleared before adding values from the file configuration. // If the list is not cleared, then the specified parameters do not affect the behavior of the sunshine server. // That is, if you set only 30 fps in the configuration file, it will not work because by default, during initialization the list includes 10, 30, 60, 90 and 120 fps. @@ -961,6 +977,7 @@ namespace config { int_f(vars, "qsv_preset", video.qsv.qsv_preset, qsv::preset_from_view); int_f(vars, "qsv_coder", video.qsv.qsv_cavlc, qsv::coder_from_view); + bool_f(vars, "qsv_slow_hevc", video.qsv.qsv_slow_hevc); std::string quality; string_f(vars, "amd_quality", quality); @@ -982,13 +999,14 @@ namespace config { std::string usage; string_f(vars, "amd_usage", usage); if (!usage.empty()) { - video.amd.amd_usage_h264 = amd::usage_from_view(rc); - video.amd.amd_usage_hevc = amd::usage_from_view(rc); - video.amd.amd_usage_av1 = amd::usage_from_view(rc); + video.amd.amd_usage_h264 = amd::usage_from_view(usage); + video.amd.amd_usage_hevc = amd::usage_from_view(usage); + video.amd.amd_usage_av1 = amd::usage_from_view(usage); } bool_f(vars, "amd_preanalysis", (bool &) video.amd.amd_preanalysis); bool_f(vars, "amd_vbaq", (bool &) video.amd.amd_vbaq); + bool_f(vars, "amd_enforce_hrd", (bool &) video.amd.amd_enforce_hrd); int_f(vars, "vt_coder", video.vt.vt_coder, vt::coder_from_view); int_f(vars, "vt_software", video.vt.vt_allow_sw, vt::allow_software_from_view); @@ -1093,6 +1111,20 @@ namespace config { config::sunshine.flags[config::flag::UPNP].flip(); } + string_restricted_f(vars, "locale", config::sunshine.locale, { + "de"sv, // German + "en"sv, // English + "en_GB"sv, // English (UK) + "en_US"sv, // English (US) + "es"sv, // Spanish + "fr"sv, // French + "it"sv, // Italian + "ja"sv, // Japanese + "ru"sv, // Russian + "sv"sv, // Swedish + "zh"sv, // Chinese + }); + std::string log_level_string; string_f(vars, "min_log_level", log_level_string); @@ -1153,7 +1185,7 @@ namespace config { auto line = argv[x]; if (line == "--help"sv) { - print_help(*argv); + logging::print_help(*argv); return 1; } #ifdef _WIN32 @@ -1173,7 +1205,7 @@ namespace config { break; } if (apply_flags(line + 1)) { - print_help(*argv); + logging::print_help(*argv); return -1; } } @@ -1187,7 +1219,7 @@ namespace config { else { TUPLE_EL(var, 1, parse_option(line, line_end)); if (!var) { - print_help(*argv); + logging::print_help(*argv); return -1; } diff --git a/src/config.h b/src/config.h index e08a87f3c4d..2c85096bac6 100644 --- a/src/config.h +++ b/src/config.h @@ -44,6 +44,7 @@ namespace config { struct { std::optional qsv_preset; std::optional qsv_cavlc; + bool qsv_slow_hevc; } qsv; struct { @@ -58,6 +59,7 @@ namespace config { std::optional amd_usage_av1; std::optional amd_preanalysis; std::optional amd_vbaq; + std::optional amd_enforce_hrd; int amd_coder; } amd; @@ -159,6 +161,7 @@ namespace config { bool elevated; }; struct sunshine_t { + std::string locale; int min_log_level; std::bitset flags; std::string credentials_file; diff --git a/src/confighttp.cpp b/src/confighttp.cpp index 9328e27a15a..4ef0af75f6f 100644 --- a/src/confighttp.cpp +++ b/src/confighttp.cpp @@ -30,9 +30,9 @@ #include "confighttp.h" #include "crypto.h" #include "file_handler.h" +#include "globals.h" #include "httpcommon.h" #include "logging.h" -#include "main.h" #include "network.h" #include "nvhttp.h" #include "platform/common.h" @@ -550,6 +550,24 @@ namespace confighttp { } } + void + getLocale(resp_https_t response, req_https_t request) { + // we need to return the locale whether authenticated or not + + print_req(request); + + pt::ptree outputTree; + auto g = util::fail_guard([&]() { + std::ostringstream data; + + pt::write_json(data, outputTree); + response->write(data.str()); + }); + + outputTree.put("status", "true"); + outputTree.put("locale", config::sunshine.locale); + } + void saveConfig(resp_https_t response, req_https_t request) { if (!authenticate(response, request)) return; @@ -797,6 +815,7 @@ namespace confighttp { server.resource["^/api/apps$"]["POST"] = saveApp; server.resource["^/api/config$"]["GET"] = getConfig; server.resource["^/api/config$"]["POST"] = saveConfig; + server.resource["^/api/configLocale$"]["GET"] = getLocale; server.resource["^/api/restart$"]["POST"] = restart; server.resource["^/api/password$"]["POST"] = savePassword; server.resource["^/api/apps/([0-9]+)$"]["DELETE"] = deleteApp; diff --git a/src/crypto.cpp b/src/crypto.cpp index e92e6e9e7db..9a5ef5a474e 100644 --- a/src/crypto.cpp +++ b/src/crypto.cpp @@ -17,6 +17,10 @@ namespace crypto { X509_STORE_add_cert(x509_store.get(), cert.get()); _certs.emplace_back(std::make_pair(std::move(cert), std::move(x509_store))); } + void + cert_chain_t::clear() { + _certs.clear(); + } static int openssl_verify_cb(int ok, X509_STORE_CTX *ctx) { diff --git a/src/crypto.h b/src/crypto.h index eb355f576c8..410d3c802a5 100644 --- a/src/crypto.h +++ b/src/crypto.h @@ -73,6 +73,9 @@ namespace crypto { void add(x509_t &&cert); + void + clear(); + const char * verify(x509_t::element_type *cert); diff --git a/src/entry_handler.cpp b/src/entry_handler.cpp new file mode 100644 index 00000000000..8d17b7d270a --- /dev/null +++ b/src/entry_handler.cpp @@ -0,0 +1,396 @@ +/** + * @file entry_handler.cpp + * @brief Entry point related functions. + */ + +// standard includes +#include +#include +#include + +// local includes +#include "config.h" +#include "confighttp.h" +#include "entry_handler.h" +#include "globals.h" +#include "httpcommon.h" +#include "logging.h" +#include "network.h" +#include "platform/common.h" +#include "version.h" + +extern "C" { +#ifdef _WIN32 + #include +#endif +} + +using namespace std::literals; + +/** + * @brief Launch the Web UI. + * + * EXAMPLES: + * ```cpp + * launch_ui(); + * ``` + */ +void +launch_ui() { + std::string url = "https://localhost:" + std::to_string(net::map_port(confighttp::PORT_HTTPS)); + platf::open_url(url); +} + +/** + * @brief Launch the Web UI at a specific endpoint. + * + * EXAMPLES: + * ```cpp + * launch_ui_with_path("/pin"); + * ``` + */ +void +launch_ui_with_path(std::string path) { + std::string url = "https://localhost:" + std::to_string(net::map_port(confighttp::PORT_HTTPS)) + path; + platf::open_url(url); +} + +namespace args { + /** + * @brief Reset the user credentials. + * + * @param name The name of the program. + * @param argc The number of arguments. + * @param argv The arguments. + * + * EXAMPLES: + * ```cpp + * creds("sunshine", 2, {"new_username", "new_password"}); + * ``` + */ + int + creds(const char *name, int argc, char *argv[]) { + if (argc < 2 || argv[0] == "help"sv || argv[1] == "help"sv) { + help(name, argc, argv); + } + + http::save_user_creds(config::sunshine.credentials_file, argv[0], argv[1]); + + return 0; + } + + /** + * @brief Print help to stdout, then exit. + * @param name The name of the program. + * @param argc The number of arguments. (Unused) + * @param argv The arguments. (Unused) + * + * EXAMPLES: + * ```cpp + * help("sunshine", 0, nullptr); + * ``` + */ + int + help(const char *name, int argc, char *argv[]) { + logging::print_help(name); + return 0; + } + + /** + * @brief Print the version to stdout, then exit. + * @param name The name of the program. (Unused) + * @param argc The number of arguments. (Unused) + * @param argv The arguments. (Unused) + * + * EXAMPLES: + * ```cpp + * version("sunshine", 0, nullptr); + * ``` + */ + int + version(const char *name, int argc, char *argv[]) { + // version was already logged at startup + return 0; + } + +#ifdef _WIN32 + /** + * @brief Restore global NVIDIA control panel settings. + * + * If Sunshine was improperly terminated, this function restores + * the global NVIDIA control panel settings to the undo file left + * by Sunshine. This function is typically called by the uninstaller. + * + * @param name The name of the program. (Unused) + * @param argc The number of arguments. (Unused) + * @param argv The arguments. (Unused) + * + * EXAMPLES: + * ```cpp + * restore_nvprefs_undo("sunshine", 0, nullptr); + * ``` + */ + int + restore_nvprefs_undo(const char *name, int argc, char *argv[]) { + if (nvprefs_instance.load()) { + nvprefs_instance.restore_from_and_delete_undo_file_if_exists(); + nvprefs_instance.unload(); + } + return 0; + } +#endif +} // namespace args + +namespace lifetime { + char **argv; + std::atomic_int desired_exit_code; + + /** + * @brief Terminates Sunshine gracefully with the provided exit code. + * @param exit_code The exit code to return from main(). + * @param async Specifies whether our termination will be non-blocking. + */ + void + exit_sunshine(int exit_code, bool async) { + // Store the exit code of the first exit_sunshine() call + int zero = 0; + desired_exit_code.compare_exchange_strong(zero, exit_code); + + // Raise SIGINT to start termination + std::raise(SIGINT); + + // Termination will happen asynchronously, but the caller may + // have wanted synchronous behavior. + while (!async) { + std::this_thread::sleep_for(1s); + } + } + + /** + * @brief Breaks into the debugger or terminates Sunshine if no debugger is attached. + */ + void + debug_trap() { +#ifdef _WIN32 + DebugBreak(); +#else + std::raise(SIGTRAP); +#endif + } + + /** + * @brief Gets the argv array passed to main(). + */ + char ** + get_argv() { + return argv; + } +} // namespace lifetime + +#ifdef _WIN32 +/** + * @brief Check if NVIDIA's GameStream software is running. + * @return `true` if GameStream is enabled, `false` otherwise. + */ +bool +is_gamestream_enabled() { + DWORD enabled; + DWORD size = sizeof(enabled); + return RegGetValueW( + HKEY_LOCAL_MACHINE, + L"SOFTWARE\\NVIDIA Corporation\\NvStream", + L"EnableStreaming", + RRF_RT_REG_DWORD, + nullptr, + &enabled, + &size) == ERROR_SUCCESS && + enabled != 0; +} + +namespace service_ctrl { + class service_controller { + public: + /** + * @brief Constructor for service_controller class. + * @param service_desired_access SERVICE_* desired access flags. + */ + service_controller(DWORD service_desired_access) { + scm_handle = OpenSCManagerA(nullptr, nullptr, SC_MANAGER_CONNECT); + if (!scm_handle) { + auto winerr = GetLastError(); + BOOST_LOG(error) << "OpenSCManager() failed: "sv << winerr; + return; + } + + service_handle = OpenServiceA(scm_handle, "SunshineService", service_desired_access); + if (!service_handle) { + auto winerr = GetLastError(); + BOOST_LOG(error) << "OpenService() failed: "sv << winerr; + return; + } + } + + ~service_controller() { + if (service_handle) { + CloseServiceHandle(service_handle); + } + + if (scm_handle) { + CloseServiceHandle(scm_handle); + } + } + + /** + * @brief Asynchronously starts the Sunshine service. + */ + bool + start_service() { + if (!service_handle) { + return false; + } + + if (!StartServiceA(service_handle, 0, nullptr)) { + auto winerr = GetLastError(); + if (winerr != ERROR_SERVICE_ALREADY_RUNNING) { + BOOST_LOG(error) << "StartService() failed: "sv << winerr; + return false; + } + } + + return true; + } + + /** + * @brief Query the service status. + * @param status The SERVICE_STATUS struct to populate. + */ + bool + query_service_status(SERVICE_STATUS &status) { + if (!service_handle) { + return false; + } + + if (!QueryServiceStatus(service_handle, &status)) { + auto winerr = GetLastError(); + BOOST_LOG(error) << "QueryServiceStatus() failed: "sv << winerr; + return false; + } + + return true; + } + + private: + SC_HANDLE scm_handle = NULL; + SC_HANDLE service_handle = NULL; + }; + + /** + * @brief Check if the service is running. + * + * EXAMPLES: + * ```cpp + * is_service_running(); + * ``` + */ + bool + is_service_running() { + service_controller sc { SERVICE_QUERY_STATUS }; + + SERVICE_STATUS status; + if (!sc.query_service_status(status)) { + return false; + } + + return status.dwCurrentState == SERVICE_RUNNING; + } + + /** + * @brief Start the service and wait for startup to complete. + * + * EXAMPLES: + * ```cpp + * start_service(); + * ``` + */ + bool + start_service() { + service_controller sc { SERVICE_QUERY_STATUS | SERVICE_START }; + + std::cout << "Starting Sunshine..."sv; + + // This operation is asynchronous, so we must wait for it to complete + if (!sc.start_service()) { + return false; + } + + SERVICE_STATUS status; + do { + Sleep(1000); + std::cout << '.'; + } while (sc.query_service_status(status) && status.dwCurrentState == SERVICE_START_PENDING); + + if (status.dwCurrentState != SERVICE_RUNNING) { + BOOST_LOG(error) << SERVICE_NAME " failed to start: "sv << status.dwWin32ExitCode; + return false; + } + + std::cout << std::endl; + return true; + } + + /** + * @brief Wait for the UI to be ready after Sunshine startup. + * + * EXAMPLES: + * ```cpp + * wait_for_ui_ready(); + * ``` + */ + bool + wait_for_ui_ready() { + std::cout << "Waiting for Web UI to be ready..."; + + // Wait up to 30 seconds for the web UI to start + for (int i = 0; i < 30; i++) { + PMIB_TCPTABLE tcp_table = nullptr; + ULONG table_size = 0; + ULONG err; + + auto fg = util::fail_guard([&tcp_table]() { + free(tcp_table); + }); + + do { + // Query all open TCP sockets to look for our web UI port + err = GetTcpTable(tcp_table, &table_size, false); + if (err == ERROR_INSUFFICIENT_BUFFER) { + free(tcp_table); + tcp_table = (PMIB_TCPTABLE) malloc(table_size); + } + } while (err == ERROR_INSUFFICIENT_BUFFER); + + if (err != NO_ERROR) { + BOOST_LOG(error) << "Failed to query TCP table: "sv << err; + return false; + } + + uint16_t port_nbo = htons(net::map_port(confighttp::PORT_HTTPS)); + for (DWORD i = 0; i < tcp_table->dwNumEntries; i++) { + auto &entry = tcp_table->table[i]; + + // Look for our port in the listening state + if (entry.dwLocalPort == port_nbo && entry.dwState == MIB_TCP_STATE_LISTEN) { + std::cout << std::endl; + return true; + } + } + + Sleep(1000); + std::cout << '.'; + } + + std::cout << "timed out"sv << std::endl; + return false; + } +} // namespace service_ctrl +#endif diff --git a/src/entry_handler.h b/src/entry_handler.h new file mode 100644 index 00000000000..bdab361cf0c --- /dev/null +++ b/src/entry_handler.h @@ -0,0 +1,62 @@ +/** + * @file entry_handler.h + * @brief Header file for entry point functions. + */ +#pragma once + +// standard includes +#include +#include + +// local includes +#include "thread_pool.h" +#include "thread_safe.h" + +// functions +void +launch_ui(); +void +launch_ui_with_path(std::string path); + +#ifdef _WIN32 +// windows only functions +bool +is_gamestream_enabled(); +#endif + +namespace args { + int + creds(const char *name, int argc, char *argv[]); + int + help(const char *name, int argc, char *argv[]); + int + version(const char *name, int argc, char *argv[]); +#ifdef _WIN32 + int + restore_nvprefs_undo(const char *name, int argc, char *argv[]); +#endif +} // namespace args + +namespace lifetime { + extern char **argv; + extern std::atomic_int desired_exit_code; + void + exit_sunshine(int exit_code, bool async); + void + debug_trap(); + char ** + get_argv(); +} // namespace lifetime + +#ifdef _WIN32 +namespace service_ctrl { + bool + is_service_running(); + + bool + start_service(); + + bool + wait_for_ui_ready(); +} // namespace service_ctrl +#endif diff --git a/src/globals.cpp b/src/globals.cpp new file mode 100644 index 00000000000..ae6c7544360 --- /dev/null +++ b/src/globals.cpp @@ -0,0 +1,27 @@ +/** + * @file globals.cpp + * @brief Implementation for globally accessible variables and functions. + */ +#include "globals.h" + +/** + * @brief A process-wide communication mechanism. + */ +safe::mail_t mail::man; + +/** + * @brief A thread pool for processing tasks. + */ +thread_pool_util::ThreadPool task_pool; + +/** + * @brief A boolean flag to indicate whether the cursor should be displayed. + */ +bool display_cursor = true; + +#ifdef _WIN32 +/** + * @brief A global singleton used for NVIDIA control panel modifications. + */ +nvprefs::nvprefs_interface nvprefs_instance; +#endif diff --git a/src/globals.h b/src/globals.h new file mode 100644 index 00000000000..a137bc9c4d5 --- /dev/null +++ b/src/globals.h @@ -0,0 +1,42 @@ +/** + * @file globals.h + * @brief Header for globally accessible variables and functions. + */ +#pragma once + +#include "entry_handler.h" +#include "thread_pool.h" + +extern thread_pool_util::ThreadPool task_pool; +extern bool display_cursor; + +#ifdef _WIN32 + // Declare global singleton used for NVIDIA control panel modifications + #include "platform/windows/nvprefs/nvprefs_interface.h" +extern nvprefs::nvprefs_interface nvprefs_instance; +#endif + +namespace mail { +#define MAIL(x) \ + constexpr auto x = std::string_view { \ + #x \ + } + + extern safe::mail_t man; + + // Global mail + MAIL(shutdown); + MAIL(broadcast_shutdown); + MAIL(video_packets); + MAIL(audio_packets); + MAIL(switch_display); + + // Local mail + MAIL(touch_port); + MAIL(idr); + MAIL(invalidate_ref_frames); + MAIL(gamepad_feedback); + MAIL(hdr); +#undef MAIL + +} // namespace mail diff --git a/src/input.cpp b/src/input.cpp index b7416fffac2..2e26d5b00a8 100644 --- a/src/input.cpp +++ b/src/input.cpp @@ -17,9 +17,9 @@ extern "C" { #include #include "config.h" +#include "globals.h" #include "input.h" #include "logging.h" -#include "main.h" #include "platform/common.h" #include "thread_pool.h" #include "utility.h" @@ -469,15 +469,19 @@ namespace input { * @param input The input context. * @param val The cartesian coordinate pair to convert. * @param size The size of the client's surface containing the value. - * @return The host-relative coordinate pair. + * @return The host-relative coordinate pair if a touchport is available. */ - std::pair + std::optional> client_to_touchport(std::shared_ptr &input, const std::pair &val, const std::pair &size) { auto &touch_port_event = input->touch_port_event; auto &touch_port = input->touch_port; if (touch_port_event->peek()) { touch_port = *touch_port_event->pop(); } + if (!touch_port) { + BOOST_LOG(verbose) << "Ignoring early absolute input without a touch port"sv; + return std::nullopt; + } auto scalarX = touch_port.width / size.first; auto scalarY = touch_port.height / size.second; @@ -491,7 +495,7 @@ namespace input { x = std::clamp(x, offsetX, (size.first * scalarX) - offsetX); y = std::clamp(y, offsetY, (size.second * scalarY) - offsetY); - return { (x - offsetX) * touch_port.scalar_inv, (y - offsetY) * touch_port.scalar_inv }; + return std::pair { (x - offsetX) * touch_port.scalar_inv, (y - offsetY) * touch_port.scalar_inv }; } /** @@ -561,6 +565,9 @@ namespace input { auto height = (float) util::endian::big(packet->height); auto tpcoords = client_to_touchport(input, { x, y }, { width, height }); + if (!tpcoords) { + return; + } auto &touch_port = input->touch_port; platf::touch_port_t abs_port { @@ -568,7 +575,7 @@ namespace input { touch_port.env_width, touch_port.env_height }; - platf::abs_mouse(platf_input, abs_port, tpcoords.first, tpcoords.second); + platf::abs_mouse(platf_input, abs_port, tpcoords->first, tpcoords->second); } void @@ -918,6 +925,9 @@ namespace input { { from_clamped_netfloat(packet->x, 0.0f, 1.0f) * 65535.f, from_clamped_netfloat(packet->y, 0.0f, 1.0f) * 65535.f }, { 65535.f, 65535.f }); + if (!coords) { + return; + } auto &touch_port = input->touch_port; platf::touch_port_t abs_port { @@ -926,8 +936,8 @@ namespace input { }; // Renormalize the coordinates - coords.first /= abs_port.width; - coords.second /= abs_port.height; + coords->first /= abs_port.width; + coords->second /= abs_port.height; // Normalize rotation value to 0-359 degree range auto rotation = util::endian::little(packet->rotation); @@ -946,8 +956,8 @@ namespace input { packet->eventType, rotation, util::endian::little(packet->pointerId), - coords.first, - coords.second, + coords->first, + coords->second, from_clamped_netfloat(packet->pressureOrDistance, 0.0f, 1.0f), contact_area.first, contact_area.second, @@ -972,6 +982,9 @@ namespace input { { from_clamped_netfloat(packet->x, 0.0f, 1.0f) * 65535.f, from_clamped_netfloat(packet->y, 0.0f, 1.0f) * 65535.f }, { 65535.f, 65535.f }); + if (!coords) { + return; + } auto &touch_port = input->touch_port; platf::touch_port_t abs_port { @@ -980,8 +993,8 @@ namespace input { }; // Renormalize the coordinates - coords.first /= abs_port.width; - coords.second /= abs_port.height; + coords->first /= abs_port.width; + coords->second /= abs_port.height; // Normalize rotation value to 0-359 degree range auto rotation = util::endian::little(packet->rotation); @@ -1002,8 +1015,8 @@ namespace input { packet->penButtons, packet->tilt, rotation, - coords.first, - coords.second, + coords->first, + coords->second, from_clamped_netfloat(packet->pressureOrDistance, 0.0f, 1.0f), contact_area.first, contact_area.second, diff --git a/src/input.h b/src/input.h index bc9fea8479f..33a9ee42741 100644 --- a/src/input.h +++ b/src/input.h @@ -32,6 +32,11 @@ namespace input { float client_offsetX, client_offsetY; float scalar_inv; + + explicit + operator bool() const { + return width != 0 && height != 0 && env_width != 0 && env_height != 0; + } }; std::pair diff --git a/src/logging.cpp b/src/logging.cpp index 70a2ae82a00..e03bcbf5134 100644 --- a/src/logging.cpp +++ b/src/logging.cpp @@ -4,9 +4,11 @@ */ // standard includes +#include #include // lib includes +#include #include #include #include @@ -16,6 +18,10 @@ // local includes #include "logging.h" +extern "C" { +#include +} + using namespace std::literals; namespace bl = boost::log; @@ -29,45 +35,182 @@ bl::sources::severity_logger warning(3); // Strange events bl::sources::severity_logger error(4); // Recoverable errors bl::sources::severity_logger fatal(5); // Unrecoverable errors -/** - * @brief Flush the log. - * - * EXAMPLES: - * ```cpp - * log_flush(); - * ``` - */ -void -log_flush() { - sink->flush(); -} +BOOST_LOG_ATTRIBUTE_KEYWORD(severity, "Severity", int) -/** - * @brief Print help to stdout. - * @param name The name of the program. - * - * EXAMPLES: - * ```cpp - * print_help("sunshine"); - * ``` - */ -void -print_help(const char *name) { - std::cout - << "Usage: "sv << name << " [options] [/path/to/configuration_file] [--cmd]"sv << std::endl - << " Any configurable option can be overwritten with: \"name=value\""sv << std::endl - << std::endl - << " Note: The configuration will be created if it doesn't exist."sv << std::endl - << std::endl - << " --help | print help"sv << std::endl - << " --creds username password | set user credentials for the Web manager"sv << std::endl - << " --version | print the version of sunshine"sv << std::endl - << std::endl - << " flags"sv << std::endl - << " -0 | Read PIN from stdin"sv << std::endl - << " -1 | Do not load previously saved state and do retain any state after shutdown"sv << std::endl - << " | Effectively starting as if for the first time without overwriting any pairings with your devices"sv << std::endl - << " -2 | Force replacement of headers in video stream"sv << std::endl - << " -p | Enable/Disable UPnP"sv << std::endl - << std::endl; -} +namespace logging { + /** + * @brief A destructor that restores the initial state. + */ + deinit_t::~deinit_t() { + deinit(); + } + + /** + * @brief Deinitialize the logging system. + * + * EXAMPLES: + * ```cpp + * deinit(); + * ``` + */ + void + deinit() { + log_flush(); + bl::core::get()->remove_sink(sink); + sink.reset(); + } + + /** + * @brief Initialize the logging system. + * @param min_log_level The minimum log level to output. + * @param log_file The log file to write to. + * @returns A deinit_t object that will deinitialize the logging system when it goes out of scope. + * + * EXAMPLES: + * ```cpp + * log_init(2, "sunshine.log"); + * ``` + */ + [[nodiscard]] std::unique_ptr + init(int min_log_level, const std::string &log_file) { + if (sink) { + // Deinitialize the logging system before reinitializing it. This can probably only ever be hit in tests. + deinit(); + } + + setup_av_logging(min_log_level); + + sink = boost::make_shared(); + + boost::shared_ptr stream { &std::cout, boost::null_deleter() }; + sink->locked_backend()->add_stream(stream); + sink->locked_backend()->add_stream(boost::make_shared(log_file)); + sink->set_filter(severity >= min_log_level); + + sink->set_formatter([](const bl::record_view &view, bl::formatting_ostream &os) { + constexpr const char *message = "Message"; + constexpr const char *severity = "Severity"; + constexpr int DATE_BUFFER_SIZE = 21 + 2 + 1; // Full string plus ": \0" + + auto log_level = view.attribute_values()[severity].extract().get(); + + std::string_view log_type; + switch (log_level) { + case 0: + log_type = "Verbose: "sv; + break; + case 1: + log_type = "Debug: "sv; + break; + case 2: + log_type = "Info: "sv; + break; + case 3: + log_type = "Warning: "sv; + break; + case 4: + log_type = "Error: "sv; + break; + case 5: + log_type = "Fatal: "sv; + break; + }; + + char _date[DATE_BUFFER_SIZE]; + std::time_t t = std::time(nullptr); + strftime(_date, DATE_BUFFER_SIZE, "[%Y:%m:%d:%H:%M:%S]: ", std::localtime(&t)); + + os << _date << log_type << view.attribute_values()[message].extract(); + }); + + // Flush after each log record to ensure log file contents on disk isn't stale. + // This is particularly important when running from a Windows service. + sink->locked_backend()->auto_flush(true); + + bl::core::get()->add_sink(sink); + return std::make_unique(); + } + + /** + * @brief Setup AV logging. + * @param min_log_level The log level. + */ + void + setup_av_logging(int min_log_level) { + if (min_log_level >= 1) { + av_log_set_level(AV_LOG_QUIET); + } + else { + av_log_set_level(AV_LOG_DEBUG); + } + av_log_set_callback([](void *ptr, int level, const char *fmt, va_list vl) { + static int print_prefix = 1; + char buffer[1024]; + + av_log_format_line(ptr, level, fmt, vl, buffer, sizeof(buffer), &print_prefix); + if (level <= AV_LOG_ERROR) { + // We print AV_LOG_FATAL at the error level. FFmpeg prints things as fatal that + // are expected in some cases, such as lack of codec support or similar things. + BOOST_LOG(error) << buffer; + } + else if (level <= AV_LOG_WARNING) { + BOOST_LOG(warning) << buffer; + } + else if (level <= AV_LOG_INFO) { + BOOST_LOG(info) << buffer; + } + else if (level <= AV_LOG_VERBOSE) { + // AV_LOG_VERBOSE is less verbose than AV_LOG_DEBUG + BOOST_LOG(debug) << buffer; + } + else { + BOOST_LOG(verbose) << buffer; + } + }); + } + + /** + * @brief Flush the log. + * + * EXAMPLES: + * ```cpp + * log_flush(); + * ``` + */ + void + log_flush() { + if (sink) { + sink->flush(); + } + } + + /** + * @brief Print help to stdout. + * @param name The name of the program. + * + * EXAMPLES: + * ```cpp + * print_help("sunshine"); + * ``` + */ + void + print_help(const char *name) { + std::cout + << "Usage: "sv << name << " [options] [/path/to/configuration_file] [--cmd]"sv << std::endl + << " Any configurable option can be overwritten with: \"name=value\""sv << std::endl + << std::endl + << " Note: The configuration will be created if it doesn't exist."sv << std::endl + << std::endl + << " --help | print help"sv << std::endl + << " --creds username password | set user credentials for the Web manager"sv << std::endl + << " --version | print the version of sunshine"sv << std::endl + << std::endl + << " flags"sv << std::endl + << " -0 | Read PIN from stdin"sv << std::endl + << " -1 | Do not load previously saved state and do retain any state after shutdown"sv << std::endl + << " | Effectively starting as if for the first time without overwriting any pairings with your devices"sv << std::endl + << " -2 | Force replacement of headers in video stream"sv << std::endl + << " -p | Enable/Disable UPnP"sv << std::endl + << std::endl; + } +} // namespace logging diff --git a/src/logging.h b/src/logging.h index 47a08555a0b..24f9d169082 100644 --- a/src/logging.h +++ b/src/logging.h @@ -10,7 +10,6 @@ #include #include -extern boost::shared_ptr> sink; using text_sink = boost::log::sinks::asynchronous_sink; extern boost::log::sources::severity_logger verbose; @@ -20,8 +19,20 @@ extern boost::log::sources::severity_logger warning; extern boost::log::sources::severity_logger error; extern boost::log::sources::severity_logger fatal; -// functions -void -log_flush(); -void -print_help(const char *name); +namespace logging { + class deinit_t { + public: + ~deinit_t(); + }; + + void + deinit(); + [[nodiscard]] std::unique_ptr + init(int min_log_level, const std::string &log_file); + void + setup_av_logging(int min_log_level); + void + log_flush(); + void + print_help(const char *name); +} // namespace logging diff --git a/src/main.cpp b/src/main.cpp index 1d1bb305398..208951d48fc 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -4,368 +4,30 @@ */ // standard includes +#include #include -#include #include #include -#include - -// lib includes -#include -#include -#include -#include -#include // local includes -#include "config.h" #include "confighttp.h" +#include "entry_handler.h" +#include "globals.h" #include "httpcommon.h" #include "logging.h" #include "main.h" -#include "network.h" #include "nvhttp.h" -#include "platform/common.h" #include "process.h" -#include "rtsp.h" #include "system_tray.h" -#include "thread_pool.h" #include "upnp.h" #include "version.h" #include "video.h" extern "C" { -#include #include - -#ifdef _WIN32 - #include -#endif } -safe::mail_t mail::man; - using namespace std::literals; -namespace bl = boost::log; - -#ifdef _WIN32 -// Define global singleton used for NVIDIA control panel modifications -nvprefs::nvprefs_interface nvprefs_instance; -#endif - -thread_pool_util::ThreadPool task_pool; - -bool display_cursor = true; - -struct NoDelete { - void - operator()(void *) {} -}; - -BOOST_LOG_ATTRIBUTE_KEYWORD(severity, "Severity", int) - -namespace help { - int - entry(const char *name, int argc, char *argv[]) { - print_help(name); - return 0; - } -} // namespace help - -namespace version { - int - entry(const char *name, int argc, char *argv[]) { - std::cout << PROJECT_NAME << " version: v" << PROJECT_VER << std::endl; - return 0; - } -} // namespace version - -#ifdef _WIN32 -namespace restore_nvprefs_undo { - int - entry(const char *name, int argc, char *argv[]) { - // Restore global NVIDIA control panel settings to the undo file - // left by improper termination of sunshine.exe, if it exists. - // This entry point is typically called by the uninstaller. - if (nvprefs_instance.load()) { - nvprefs_instance.restore_from_and_delete_undo_file_if_exists(); - nvprefs_instance.unload(); - } - return 0; - } -} // namespace restore_nvprefs_undo -#endif - -namespace lifetime { - static char **argv; - static std::atomic_int desired_exit_code; - - /** - * @brief Terminates Sunshine gracefully with the provided exit code. - * @param exit_code The exit code to return from main(). - * @param async Specifies whether our termination will be non-blocking. - */ - void - exit_sunshine(int exit_code, bool async) { - // Store the exit code of the first exit_sunshine() call - int zero = 0; - desired_exit_code.compare_exchange_strong(zero, exit_code); - - // Raise SIGINT to start termination - std::raise(SIGINT); - - // Termination will happen asynchronously, but the caller may - // have wanted synchronous behavior. - while (!async) { - std::this_thread::sleep_for(1s); - } - } - - /** - * @brief Gets the argv array passed to main(). - */ - char ** - get_argv() { - return argv; - } -} // namespace lifetime - -#ifdef _WIN32 -namespace service_ctrl { - class service_controller { - public: - /** - * @brief Constructor for service_controller class. - * @param service_desired_access SERVICE_* desired access flags. - */ - service_controller(DWORD service_desired_access) { - scm_handle = OpenSCManagerA(nullptr, nullptr, SC_MANAGER_CONNECT); - if (!scm_handle) { - auto winerr = GetLastError(); - BOOST_LOG(error) << "OpenSCManager() failed: "sv << winerr; - return; - } - - service_handle = OpenServiceA(scm_handle, "SunshineService", service_desired_access); - if (!service_handle) { - auto winerr = GetLastError(); - BOOST_LOG(error) << "OpenService() failed: "sv << winerr; - return; - } - } - - ~service_controller() { - if (service_handle) { - CloseServiceHandle(service_handle); - } - - if (scm_handle) { - CloseServiceHandle(scm_handle); - } - } - - /** - * @brief Asynchronously starts the Sunshine service. - */ - bool - start_service() { - if (!service_handle) { - return false; - } - - if (!StartServiceA(service_handle, 0, nullptr)) { - auto winerr = GetLastError(); - if (winerr != ERROR_SERVICE_ALREADY_RUNNING) { - BOOST_LOG(error) << "StartService() failed: "sv << winerr; - return false; - } - } - - return true; - } - - /** - * @brief Query the service status. - * @param status The SERVICE_STATUS struct to populate. - */ - bool - query_service_status(SERVICE_STATUS &status) { - if (!service_handle) { - return false; - } - - if (!QueryServiceStatus(service_handle, &status)) { - auto winerr = GetLastError(); - BOOST_LOG(error) << "QueryServiceStatus() failed: "sv << winerr; - return false; - } - - return true; - } - - private: - SC_HANDLE scm_handle = NULL; - SC_HANDLE service_handle = NULL; - }; - - /** - * @brief Check if the service is running. - * - * EXAMPLES: - * ```cpp - * is_service_running(); - * ``` - */ - bool - is_service_running() { - service_controller sc { SERVICE_QUERY_STATUS }; - - SERVICE_STATUS status; - if (!sc.query_service_status(status)) { - return false; - } - - return status.dwCurrentState == SERVICE_RUNNING; - } - - /** - * @brief Start the service and wait for startup to complete. - * - * EXAMPLES: - * ```cpp - * start_service(); - * ``` - */ - bool - start_service() { - service_controller sc { SERVICE_QUERY_STATUS | SERVICE_START }; - - std::cout << "Starting Sunshine..."sv; - - // This operation is asynchronous, so we must wait for it to complete - if (!sc.start_service()) { - return false; - } - - SERVICE_STATUS status; - do { - Sleep(1000); - std::cout << '.'; - } while (sc.query_service_status(status) && status.dwCurrentState == SERVICE_START_PENDING); - - if (status.dwCurrentState != SERVICE_RUNNING) { - BOOST_LOG(error) << SERVICE_NAME " failed to start: "sv << status.dwWin32ExitCode; - return false; - } - - std::cout << std::endl; - return true; - } - - /** - * @brief Wait for the UI to be ready after Sunshine startup. - * - * EXAMPLES: - * ```cpp - * wait_for_ui_ready(); - * ``` - */ - bool - wait_for_ui_ready() { - std::cout << "Waiting for Web UI to be ready..."; - - // Wait up to 30 seconds for the web UI to start - for (int i = 0; i < 30; i++) { - PMIB_TCPTABLE tcp_table = nullptr; - ULONG table_size = 0; - ULONG err; - - auto fg = util::fail_guard([&tcp_table]() { - free(tcp_table); - }); - - do { - // Query all open TCP sockets to look for our web UI port - err = GetTcpTable(tcp_table, &table_size, false); - if (err == ERROR_INSUFFICIENT_BUFFER) { - free(tcp_table); - tcp_table = (PMIB_TCPTABLE) malloc(table_size); - } - } while (err == ERROR_INSUFFICIENT_BUFFER); - - if (err != NO_ERROR) { - BOOST_LOG(error) << "Failed to query TCP table: "sv << err; - return false; - } - - uint16_t port_nbo = htons(net::map_port(confighttp::PORT_HTTPS)); - for (DWORD i = 0; i < tcp_table->dwNumEntries; i++) { - auto &entry = tcp_table->table[i]; - - // Look for our port in the listening state - if (entry.dwLocalPort == port_nbo && entry.dwState == MIB_TCP_STATE_LISTEN) { - std::cout << std::endl; - return true; - } - } - - Sleep(1000); - std::cout << '.'; - } - - std::cout << "timed out"sv << std::endl; - return false; - } -} // namespace service_ctrl - -/** - * @brief Checks if NVIDIA's GameStream software is running. - * @return `true` if GameStream is enabled. - */ -bool -is_gamestream_enabled() { - DWORD enabled; - DWORD size = sizeof(enabled); - return RegGetValueW( - HKEY_LOCAL_MACHINE, - L"SOFTWARE\\NVIDIA Corporation\\NvStream", - L"EnableStreaming", - RRF_RT_REG_DWORD, - nullptr, - &enabled, - &size) == ERROR_SUCCESS && - enabled != 0; -} - -#endif - -/** - * @brief Launch the Web UI. - * - * EXAMPLES: - * ```cpp - * launch_ui(); - * ``` - */ -void -launch_ui() { - std::string url = "https://localhost:" + std::to_string(net::map_port(confighttp::PORT_HTTPS)); - platf::open_url(url); -} - -/** - * @brief Launch the Web UI at a specific endpoint. - * - * EXAMPLES: - * ```cpp - * launch_ui_with_path("/pin"); - * ``` - */ -void -launch_ui_with_path(std::string path) { - std::string url = "https://localhost:" + std::to_string(net::map_port(confighttp::PORT_HTTPS)) + path; - platf::open_url(url); -} std::map> signal_handlers; void @@ -381,26 +43,12 @@ on_signal(int sig, FN &&fn) { std::signal(sig, on_signal_forwarder); } -namespace gen_creds { - int - entry(const char *name, int argc, char *argv[]) { - if (argc < 2 || argv[0] == "help"sv || argv[1] == "help"sv) { - print_help(name); - return 0; - } - - http::save_user_creds(config::sunshine.credentials_file, argv[0], argv[1]); - - return 0; - } -} // namespace gen_creds - std::map> cmd_to_func { - { "creds"sv, gen_creds::entry }, - { "help"sv, help::entry }, - { "version"sv, version::entry }, + { "creds"sv, args::creds }, + { "help"sv, args::help }, + { "version"sv, args::version }, #ifdef _WIN32 - { "restore-nvprefs-undo"sv, restore_nvprefs_undo::entry }, + { "restore-nvprefs-undo"sv, args::restore_nvprefs_undo }, #endif }; @@ -438,9 +86,6 @@ SessionMonitorWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { */ int main(int argc, char *argv[]) { - // the version should be printed to the log before anything else - BOOST_LOG(info) << PROJECT_NAME << " version: " << PROJECT_VER; - lifetime::argv = argv; task_pool_util::TaskPool::task_id_t force_shutdown = nullptr; @@ -450,8 +95,11 @@ main(int argc, char *argv[]) { setlocale(LC_ALL, ".UTF-8"); #endif +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" // Use UTF-8 conversion for the default C++ locale (used by boost::log) std::locale::global(std::locale(std::locale(), new std::codecvt_utf8)); +#pragma GCC diagnostic pop mail::man = std::make_shared(); @@ -459,84 +107,15 @@ main(int argc, char *argv[]) { return 0; } - if (config::sunshine.min_log_level >= 1) { - av_log_set_level(AV_LOG_QUIET); - } - else { - av_log_set_level(AV_LOG_DEBUG); + auto log_deinit_guard = logging::init(config::sunshine.min_log_level, config::sunshine.log_file); + if (!log_deinit_guard) { + BOOST_LOG(error) << "Logging failed to initialize"sv; } - av_log_set_callback([](void *ptr, int level, const char *fmt, va_list vl) { - static int print_prefix = 1; - char buffer[1024]; - - av_log_format_line(ptr, level, fmt, vl, buffer, sizeof(buffer), &print_prefix); - if (level <= AV_LOG_ERROR) { - // We print AV_LOG_FATAL at the error level. FFmpeg prints things as fatal that - // are expected in some cases, such as lack of codec support or similar things. - BOOST_LOG(error) << buffer; - } - else if (level <= AV_LOG_WARNING) { - BOOST_LOG(warning) << buffer; - } - else if (level <= AV_LOG_INFO) { - BOOST_LOG(info) << buffer; - } - else if (level <= AV_LOG_VERBOSE) { - // AV_LOG_VERBOSE is less verbose than AV_LOG_DEBUG - BOOST_LOG(debug) << buffer; - } - else { - BOOST_LOG(verbose) << buffer; - } - }); - sink = boost::make_shared(); - - boost::shared_ptr stream { &std::cout, NoDelete {} }; - sink->locked_backend()->add_stream(stream); - sink->locked_backend()->add_stream(boost::make_shared(config::sunshine.log_file)); - sink->set_filter(severity >= config::sunshine.min_log_level); - - sink->set_formatter([message = "Message"s, severity = "Severity"s](const bl::record_view &view, bl::formatting_ostream &os) { - constexpr int DATE_BUFFER_SIZE = 21 + 2 + 1; // Full string plus ": \0" - - auto log_level = view.attribute_values()[severity].extract().get(); - - std::string_view log_type; - switch (log_level) { - case 0: - log_type = "Verbose: "sv; - break; - case 1: - log_type = "Debug: "sv; - break; - case 2: - log_type = "Info: "sv; - break; - case 3: - log_type = "Warning: "sv; - break; - case 4: - log_type = "Error: "sv; - break; - case 5: - log_type = "Fatal: "sv; - break; - }; - - char _date[DATE_BUFFER_SIZE]; - std::time_t t = std::time(nullptr); - strftime(_date, DATE_BUFFER_SIZE, "[%Y:%m:%d:%H:%M:%S]: ", std::localtime(&t)); - - os << _date << log_type << view.attribute_values()[message].extract(); - }); - - // Flush after each log record to ensure log file contents on disk isn't stale. - // This is particularly important when running from a Windows service. - sink->locked_backend()->auto_flush(true); - - bl::core::get()->add_sink(sink); - auto fg = util::fail_guard(log_flush); + // logging can begin at this point + // if anything is logged prior to this point, it will appear in stdout, but not in the log viewer in the UI + // the version should be printed to the log before anything else + BOOST_LOG(info) << PROJECT_NAME << " version: " << PROJECT_VER; if (!config::sunshine.cmd.name.empty()) { auto fn = cmd_to_func.find(config::sunshine.cmd.name); @@ -563,7 +142,7 @@ main(int argc, char *argv[]) { nvprefs_instance.modify_application_profile(); // Modify global settings, undo file is produced in the process to restore after improper termination nvprefs_instance.modify_global_profile(); - // Unload dynamic library to survive driver reinstallation + // Unload dynamic library to survive driver re-installation nvprefs_instance.unload(); } @@ -656,8 +235,8 @@ main(int argc, char *argv[]) { auto task = []() { BOOST_LOG(fatal) << "10 seconds passed, yet Sunshine's still running: Forcing shutdown"sv; - log_flush(); - std::abort(); + logging::log_flush(); + lifetime::debug_trap(); }; force_shutdown = task_pool.pushDelayed(task, 10s).task_id; @@ -669,8 +248,8 @@ main(int argc, char *argv[]) { auto task = []() { BOOST_LOG(fatal) << "10 seconds passed, yet Sunshine's still running: Forcing shutdown"sv; - log_flush(); - std::abort(); + logging::log_flush(); + lifetime::debug_trap(); }; force_shutdown = task_pool.pushDelayed(task, 10s).task_id; @@ -682,8 +261,8 @@ main(int argc, char *argv[]) { // If any of the following fail, we log an error and continue event though sunshine will not function correctly. // This allows access to the UI to fix configuration problems or view the logs. - auto deinit_guard = platf::init(); - if (!deinit_guard) { + auto platf_deinit_guard = platf::init(); + if (!platf_deinit_guard) { BOOST_LOG(error) << "Platform failed to initialize"sv; } diff --git a/src/main.h b/src/main.h index 02a21fd321e..f34ca6cda66 100644 --- a/src/main.h +++ b/src/main.h @@ -6,73 +6,6 @@ // macros #pragma once -// standard includes -#include -#include - -// local includes -#include "thread_pool.h" -#include "thread_safe.h" - -#ifdef _WIN32 - // Declare global singleton used for NVIDIA control panel modifications - #include "platform/windows/nvprefs/nvprefs_interface.h" -extern nvprefs::nvprefs_interface nvprefs_instance; -#endif - -extern thread_pool_util::ThreadPool task_pool; -extern bool display_cursor; - // functions int main(int argc, char *argv[]); -void -launch_ui(); -void -launch_ui_with_path(std::string path); - -// namespaces -namespace mail { -#define MAIL(x) \ - constexpr auto x = std::string_view { \ - #x \ - } - - extern safe::mail_t man; - - // Global mail - MAIL(shutdown); - MAIL(broadcast_shutdown); - MAIL(video_packets); - MAIL(audio_packets); - MAIL(switch_display); - - // Local mail - MAIL(touch_port); - MAIL(idr); - MAIL(invalidate_ref_frames); - MAIL(gamepad_feedback); - MAIL(hdr); -#undef MAIL - -} // namespace mail - -namespace lifetime { - void - exit_sunshine(int exit_code, bool async); - char ** - get_argv(); -} // namespace lifetime - -#ifdef _WIN32 -namespace service_ctrl { - bool - is_service_running(); - - bool - start_service(); - - bool - wait_for_ui_ready(); -} // namespace service_ctrl -#endif diff --git a/src/nvenc/nvenc_base.cpp b/src/nvenc/nvenc_base.cpp index 63107324f37..b9eba5a04df 100644 --- a/src/nvenc/nvenc_base.cpp +++ b/src/nvenc/nvenc_base.cpp @@ -4,6 +4,17 @@ #include "src/logging.h" #include "src/utility.h" +#define MAKE_NVENC_VER(major, minor) ((major) | ((minor) << 24)) + +// Make sure we check backwards compatibility when bumping the Video Codec SDK version +// Things to look out for: +// - NV_ENC_*_VER definitions where the value inside NVENCAPI_STRUCT_VERSION() was increased +// - Incompatible struct changes in nvEncodeAPI.h (fields removed, semantics changed, etc.) +// - Test both old and new drivers with all supported codecs +#if NVENCAPI_VERSION != MAKE_NVENC_VER(12U, 0U) + #error Check and update NVENC code for backwards compatibility! +#endif + namespace { GUID @@ -81,6 +92,10 @@ namespace nvenc { bool nvenc_base::create_encoder(const nvenc_config &config, const video::config_t &client_config, const nvenc_colorspace_t &colorspace, NV_ENC_BUFFER_FORMAT buffer_format) { + // Pick the minimum NvEncode API version required to support the specified codec + // to maximize driver compatibility. AV1 was introduced in SDK v12.0. + minimum_api_version = (client_config.videoFormat <= 1) ? MAKE_NVENC_VER(11U, 0U) : MAKE_NVENC_VER(12U, 0U); + if (!nvenc && !init_library()) return false; if (encoder) destroy_encoder(); @@ -91,12 +106,12 @@ namespace nvenc { encoder_params.buffer_format = buffer_format; encoder_params.rfi = true; - NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS session_params = { NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS_VER }; + NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS session_params = { min_struct_version(NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS_VER) }; session_params.device = device; session_params.deviceType = device_type; - session_params.apiVersion = NVENCAPI_VERSION; + session_params.apiVersion = minimum_api_version; if (nvenc_failed(nvenc->nvEncOpenEncodeSessionEx(&session_params, &encoder))) { - BOOST_LOG(error) << "NvEncOpenEncodeSessionEx failed"; + BOOST_LOG(error) << "NvEncOpenEncodeSessionEx failed: " << last_error_string; return false; } @@ -112,7 +127,7 @@ namespace nvenc { return false; } - NV_ENC_INITIALIZE_PARAMS init_params = { NV_ENC_INITIALIZE_PARAMS_VER }; + NV_ENC_INITIALIZE_PARAMS init_params = { min_struct_version(NV_ENC_INITIALIZE_PARAMS_VER) }; switch (client_config.videoFormat) { case 0: @@ -146,7 +161,7 @@ namespace nvenc { } auto get_encoder_cap = [&](NV_ENC_CAPS cap) { - NV_ENC_CAPS_PARAM param = { NV_ENC_CAPS_PARAM_VER, cap }; + NV_ENC_CAPS_PARAM param = { min_struct_version(NV_ENC_CAPS_PARAM_VER), cap }; int value = 0; nvenc->nvEncGetEncodeCaps(encoder, init_params.encodeGUID, ¶m, &value); return value; @@ -199,7 +214,7 @@ namespace nvenc { init_params.frameRateNum = client_config.framerate; init_params.frameRateDen = 1; - NV_ENC_PRESET_CONFIG preset_config = { NV_ENC_PRESET_CONFIG_VER, { NV_ENC_CONFIG_VER } }; + NV_ENC_PRESET_CONFIG preset_config = { min_struct_version(NV_ENC_PRESET_CONFIG_VER), { min_struct_version(NV_ENC_CONFIG_VER, 7, 8) } }; if (nvenc_failed(nvenc->nvEncGetEncodePresetConfigEx(encoder, init_params.encodeGUID, init_params.presetGUID, init_params.tuningInfo, &preset_config))) { BOOST_LOG(error) << "NvEncGetEncodePresetConfigEx failed: " << last_error_string; return false; @@ -344,7 +359,7 @@ namespace nvenc { } if (async_event_handle) { - NV_ENC_EVENT_PARAMS event_params = { NV_ENC_EVENT_PARAMS_VER }; + NV_ENC_EVENT_PARAMS event_params = { min_struct_version(NV_ENC_EVENT_PARAMS_VER) }; event_params.completionEvent = async_event_handle; if (nvenc_failed(nvenc->nvEncRegisterAsyncEvent(encoder, &event_params))) { BOOST_LOG(error) << "NvEncRegisterAsyncEvent failed: " << last_error_string; @@ -352,7 +367,7 @@ namespace nvenc { } } - NV_ENC_CREATE_BITSTREAM_BUFFER create_bitstream_buffer = { NV_ENC_CREATE_BITSTREAM_BUFFER_VER }; + NV_ENC_CREATE_BITSTREAM_BUFFER create_bitstream_buffer = { min_struct_version(NV_ENC_CREATE_BITSTREAM_BUFFER_VER) }; if (nvenc_failed(nvenc->nvEncCreateBitstreamBuffer(encoder, &create_bitstream_buffer))) { BOOST_LOG(error) << "NvEncCreateBitstreamBuffer failed: " << last_error_string; return false; @@ -394,7 +409,7 @@ namespace nvenc { output_bitstream = nullptr; } if (encoder && async_event_handle) { - NV_ENC_EVENT_PARAMS event_params = { NV_ENC_EVENT_PARAMS_VER }; + NV_ENC_EVENT_PARAMS event_params = { min_struct_version(NV_ENC_EVENT_PARAMS_VER) }; event_params.completionEvent = async_event_handle; nvenc->nvEncUnregisterAsyncEvent(encoder, &event_params); } @@ -420,7 +435,7 @@ namespace nvenc { assert(registered_input_buffer); assert(output_bitstream); - NV_ENC_MAP_INPUT_RESOURCE mapped_input_buffer = { NV_ENC_MAP_INPUT_RESOURCE_VER }; + NV_ENC_MAP_INPUT_RESOURCE mapped_input_buffer = { min_struct_version(NV_ENC_MAP_INPUT_RESOURCE_VER) }; mapped_input_buffer.registeredResource = registered_input_buffer; if (nvenc_failed(nvenc->nvEncMapInputResource(encoder, &mapped_input_buffer))) { @@ -429,7 +444,7 @@ namespace nvenc { } auto unmap_guard = util::fail_guard([&] { nvenc->nvEncUnmapInputResource(encoder, &mapped_input_buffer); }); - NV_ENC_PIC_PARAMS pic_params = { NV_ENC_PIC_PARAMS_VER }; + NV_ENC_PIC_PARAMS pic_params = { min_struct_version(NV_ENC_PIC_PARAMS_VER, 4, 6) }; pic_params.inputWidth = encoder_params.width; pic_params.inputHeight = encoder_params.height; pic_params.encodePicFlags = force_idr ? NV_ENC_PIC_FLAG_FORCEIDR : 0; @@ -445,7 +460,7 @@ namespace nvenc { return {}; } - NV_ENC_LOCK_BITSTREAM lock_bitstream = { NV_ENC_LOCK_BITSTREAM_VER }; + NV_ENC_LOCK_BITSTREAM lock_bitstream = { min_struct_version(NV_ENC_LOCK_BITSTREAM_VER, 1, 2) }; lock_bitstream.outputBitstream = output_bitstream; lock_bitstream.doNotWait = 0; @@ -585,4 +600,28 @@ namespace nvenc { return false; } + /** + * @brief This function returns the corresponding struct version for the minimum API required by the codec. + * @details Reducing the struct versions maximizes driver compatibility by avoiding needless API breaks. + * @param version The raw structure version from `NVENCAPI_STRUCT_VERSION()`. + * @param v11_struct_version Optionally specifies the struct version to use with v11 SDK major versions. + * @param v12_struct_version Optionally specifies the struct version to use with v12 SDK major versions. + * @return A suitable struct version for the active codec. + */ + uint32_t + nvenc_base::min_struct_version(uint32_t version, uint32_t v11_struct_version, uint32_t v12_struct_version) { + assert(minimum_api_version); + + // Mask off and replace the original NVENCAPI_VERSION + version &= ~NVENCAPI_VERSION; + version |= minimum_api_version; + + // If there's a struct version override, apply that too + if (v11_struct_version || v12_struct_version) { + version &= ~(0xFFu << 16); + version |= (((minimum_api_version & 0xFF) >= 12) ? v12_struct_version : v11_struct_version) << 16; + } + + return version; + } } // namespace nvenc diff --git a/src/nvenc/nvenc_base.h b/src/nvenc/nvenc_base.h index aa02fbef620..2d012ef8da8 100644 --- a/src/nvenc/nvenc_base.h +++ b/src/nvenc/nvenc_base.h @@ -45,6 +45,17 @@ namespace nvenc { bool nvenc_failed(NVENCSTATUS status); + /** + * @brief This function returns the corresponding struct version for the minimum API required by the codec. + * @details Reducing the struct versions maximizes driver compatibility by avoiding needless API breaks. + * @param version The raw structure version from `NVENCAPI_STRUCT_VERSION()`. + * @param v11_struct_version Optionally specifies the struct version to use with v11 SDK major versions. + * @param v12_struct_version Optionally specifies the struct version to use with v12 SDK major versions. + * @return A suitable struct version for the active codec. + */ + uint32_t + min_struct_version(uint32_t version, uint32_t v11_struct_version = 0, uint32_t v12_struct_version = 0); + const NV_ENC_DEVICE_TYPE device_type; void *const device; @@ -68,6 +79,7 @@ namespace nvenc { private: NV_ENC_OUTPUT_PTR output_bitstream = nullptr; + uint32_t minimum_api_version = 0; struct { uint64_t last_encoded_frame_index = 0; diff --git a/src/nvenc/nvenc_d3d11.cpp b/src/nvenc/nvenc_d3d11.cpp index 7db6fdd2fa4..cb33a1801af 100644 --- a/src/nvenc/nvenc_d3d11.cpp +++ b/src/nvenc/nvenc_d3d11.cpp @@ -39,7 +39,7 @@ namespace nvenc { if ((dll = LoadLibraryEx(dll_name, NULL, LOAD_LIBRARY_SEARCH_SYSTEM32))) { if (auto create_instance = (decltype(NvEncodeAPICreateInstance) *) GetProcAddress(dll, "NvEncodeAPICreateInstance")) { auto new_nvenc = std::make_unique(); - new_nvenc->version = NV_ENCODE_API_FUNCTION_LIST_VER; + new_nvenc->version = min_struct_version(NV_ENCODE_API_FUNCTION_LIST_VER); if (nvenc_failed(create_instance(new_nvenc.get()))) { BOOST_LOG(error) << "NvEncodeAPICreateInstance failed: " << last_error_string; } @@ -83,7 +83,7 @@ namespace nvenc { } if (!registered_input_buffer) { - NV_ENC_REGISTER_RESOURCE register_resource = { NV_ENC_REGISTER_RESOURCE_VER }; + NV_ENC_REGISTER_RESOURCE register_resource = { min_struct_version(NV_ENC_REGISTER_RESOURCE_VER, 3, 4) }; register_resource.resourceType = NV_ENC_INPUT_RESOURCE_TYPE_DIRECTX; register_resource.width = encoder_params.width; register_resource.height = encoder_params.height; diff --git a/src/nvhttp.cpp b/src/nvhttp.cpp index 8e976029225..7bff07c6115 100644 --- a/src/nvhttp.cpp +++ b/src/nvhttp.cpp @@ -23,9 +23,9 @@ #include "config.h" #include "crypto.h" #include "file_handler.h" +#include "globals.h" #include "httpcommon.h" #include "logging.h" -#include "main.h" #include "network.h" #include "nvhttp.h" #include "platform/common.h" @@ -42,6 +42,8 @@ namespace nvhttp { namespace fs = std::filesystem; namespace pt = boost::property_tree; + crypto::cert_chain_t cert_chain; + class SunshineHttpsServer: public SimpleWeb::Server { public: SunshineHttpsServer(const std::string &certification_file, const std::string &private_key_file): @@ -1061,7 +1063,6 @@ namespace nvhttp { conf_intern.pkey = file_handler::read_file(config::nvhttp.pkey.c_str()); conf_intern.servercert = file_handler::read_file(config::nvhttp.cert.c_str()); - crypto::cert_chain_t cert_chain; client_t &client = client_root; for (auto &cert : client.certs) { cert_chain.add(crypto::x509(cert)); @@ -1072,15 +1073,15 @@ namespace nvhttp { auto add_cert = std::make_shared>(30); - // /resume doesn't always get the parameter "localAudioPlayMode" - // /launch will store it in host_audio + // resume doesn't always get the parameter "localAudioPlayMode" + // launch will store it in host_audio bool host_audio {}; https_server_t https_server { config::nvhttp.cert, config::nvhttp.pkey }; http_server_t http_server; // Verify certificates after establishing connection - https_server.verify = [&cert_chain, add_cert](SSL *ssl) { + https_server.verify = [add_cert](SSL *ssl) { crypto::x509_t x509 { SSL_get_peer_certificate(ssl) }; if (!x509) { BOOST_LOG(info) << "unknown -- denied"sv; @@ -1195,6 +1196,7 @@ namespace nvhttp { erase_all_clients() { client_t client; client_root = client; + cert_chain.clear(); save_state(); } diff --git a/src/platform/common.h b/src/platform/common.h index 7a4102ad5ba..007f7ece61b 100644 --- a/src/platform/common.h +++ b/src/platform/common.h @@ -10,7 +10,9 @@ #include #include +#include "src/config.h" #include "src/logging.h" +#include "src/stat_trackers.h" #include "src/thread_safe.h" #include "src/utility.h" #include "src/video_colorspace.h" @@ -19,6 +21,8 @@ extern "C" { #include } +using namespace std::literals; + struct sockaddr; struct AVFrame; struct AVBufferRef; @@ -499,6 +503,22 @@ namespace platf { int env_width, env_height; int width, height; + + protected: + // collect capture timing data (at loglevel debug) + stat_trackers::min_max_avg_tracker sleep_overshoot_tracker; + void + log_sleep_overshoot(std::chrono::nanoseconds overshoot_ns) { + if (config::sunshine.min_log_level <= 1) { + // Print sleep overshoot stats to debug log every 20 seconds + auto print_info = [&](double min_overshoot, double max_overshoot, double avg_overshoot) { + auto f = stat_trackers::one_digit_after_decimal(); + BOOST_LOG(debug) << "Sleep overshoot (min/max/avg): " << f % min_overshoot << "ms/" << f % max_overshoot << "ms/" << f % avg_overshoot << "ms"; + }; + // std::chrono::nanoseconds overshoot_ns = std::chrono::steady_clock::now() - next_frame; + sleep_overshoot_tracker.collect_and_callback_on_interval(overshoot_ns.count() / 1000000., print_info, 20s); + } + } }; class mic_t { diff --git a/src/platform/linux/audio.cpp b/src/platform/linux/audio.cpp index 577287b77ef..e663c811ba2 100644 --- a/src/platform/linux/audio.cpp +++ b/src/platform/linux/audio.cpp @@ -4,6 +4,7 @@ */ #include #include +#include #include @@ -15,7 +16,6 @@ #include "src/config.h" #include "src/logging.h" -#include "src/main.h" #include "src/thread_safe.h" namespace platf { diff --git a/src/platform/linux/cuda.cpp b/src/platform/linux/cuda.cpp index 856fecc657a..ee535d21d2a 100644 --- a/src/platform/linux/cuda.cpp +++ b/src/platform/linux/cuda.cpp @@ -3,10 +3,9 @@ * @brief todo */ #include - #include - #include +#include #include #include @@ -20,7 +19,6 @@ extern "C" { #include "cuda.h" #include "graphics.h" #include "src/logging.h" -#include "src/main.h" #include "src/utility.h" #include "src/video.h" #include "wayland.h" @@ -249,29 +247,38 @@ namespace cuda { // There's no way to directly go from CUDA to a DRM device, so we'll // use sysfs to look up the DRM device name from the PCI ID. - char pci_bus_id[13]; - CU_CHECK(cdf->cuDeviceGetPCIBusId(pci_bus_id, sizeof(pci_bus_id), device), "Couldn't get CUDA device PCI bus ID"); - BOOST_LOG(debug) << "Found CUDA device with PCI bus ID: "sv << pci_bus_id; + std::array pci_bus_id; + CU_CHECK(cdf->cuDeviceGetPCIBusId(pci_bus_id.data(), pci_bus_id.size(), device), "Couldn't get CUDA device PCI bus ID"); + BOOST_LOG(debug) << "Found CUDA device with PCI bus ID: "sv << pci_bus_id.data(); + + // Linux uses lowercase hexadecimal while CUDA uses uppercase + std::transform(pci_bus_id.begin(), pci_bus_id.end(), pci_bus_id.begin(), + [](char c) { return std::tolower(c); }); // Look for the name of the primary node in sysfs - char sysfs_path[PATH_MAX]; - std::snprintf(sysfs_path, sizeof(sysfs_path), "/sys/bus/pci/devices/%s/drm", pci_bus_id); - fs::path sysfs_dir { sysfs_path }; - for (auto &entry : fs::directory_iterator { sysfs_dir }) { - auto file = entry.path().filename(); - auto filestring = file.generic_u8string(); - if (std::string_view { filestring }.substr(0, 4) != "card"sv) { - continue; - } + try { + char sysfs_path[PATH_MAX]; + std::snprintf(sysfs_path, sizeof(sysfs_path), "/sys/bus/pci/devices/%s/drm", pci_bus_id.data()); + fs::path sysfs_dir { sysfs_path }; + for (auto &entry : fs::directory_iterator { sysfs_dir }) { + auto file = entry.path().filename(); + auto filestring = file.generic_u8string(); + if (std::string_view { filestring }.substr(0, 4) != "card"sv) { + continue; + } - BOOST_LOG(debug) << "Found DRM primary node: "sv << filestring; + BOOST_LOG(debug) << "Found DRM primary node: "sv << filestring; - fs::path dri_path { "/dev/dri"sv }; - auto device_path = dri_path / file; - return open(device_path.c_str(), O_RDWR); + fs::path dri_path { "/dev/dri"sv }; + auto device_path = dri_path / file; + return open(device_path.c_str(), O_RDWR); + } + } + catch (const std::filesystem::filesystem_error &err) { + BOOST_LOG(error) << "Failed to read sysfs: "sv << err.what(); } - BOOST_LOG(error) << "Unable to find DRM device with PCI bus ID: "sv << pci_bus_id; + BOOST_LOG(error) << "Unable to find DRM device with PCI bus ID: "sv << pci_bus_id.data(); return -1; } @@ -793,16 +800,21 @@ namespace cuda { handle.reset(); }); + sleep_overshoot_tracker.reset(); + while (true) { auto now = std::chrono::steady_clock::now(); if (next_frame > now) { - std::this_thread::sleep_for((next_frame - now) / 3 * 2); + std::this_thread::sleep_for(next_frame - now); } - while (next_frame > now) { - std::this_thread::sleep_for(1ns); - now = std::chrono::steady_clock::now(); + now = std::chrono::steady_clock::now(); + std::chrono::nanoseconds overshoot_ns = now - next_frame; + log_sleep_overshoot(overshoot_ns); + + next_frame += delay; + if (next_frame < now) { // some major slowdown happened; we couldn't keep up + next_frame = now + delay; } - next_frame = now + delay; std::shared_ptr img_out; auto status = snapshot(pull_free_image_cb, img_out, 150ms, *cursor); diff --git a/src/platform/linux/cuda.cu b/src/platform/linux/cuda.cu index 863e3f944fe..8fb1a5ee2d6 100644 --- a/src/platform/linux/cuda.cu +++ b/src/platform/linux/cuda.cu @@ -222,7 +222,7 @@ std::optional tex_t::make(int height, int pitch) { return tex; } -tex_t::tex_t() : array {}, texture { INVALID_TEXTURE } {} +tex_t::tex_t() : array {}, texture { INVALID_TEXTURE, INVALID_TEXTURE } {} tex_t::tex_t(tex_t &&other) : array { other.array }, texture { other.texture } { other.array = 0; other.texture.point = INVALID_TEXTURE; diff --git a/src/platform/linux/graphics.cpp b/src/platform/linux/graphics.cpp index b4129889562..2cd81dd451e 100644 --- a/src/platform/linux/graphics.cpp +++ b/src/platform/linux/graphics.cpp @@ -22,7 +22,9 @@ extern "C" { #define fourcc_mod_code(vendor, val) ((((uint64_t) vendor) << 56) | ((val) &0x00ffffffffffffffULL)) #define DRM_FORMAT_MOD_INVALID fourcc_mod_code(0, ((1ULL << 56) - 1)) -#define SUNSHINE_SHADERS_DIR SUNSHINE_ASSETS_DIR "/shaders/opengl" +#if !defined(SUNSHINE_SHADERS_DIR) // for testing this needs to be defined in cmake as we don't do an install + #define SUNSHINE_SHADERS_DIR SUNSHINE_ASSETS_DIR "/shaders/opengl" +#endif using namespace std::literals; namespace gl { @@ -37,7 +39,7 @@ namespace gl { } tex_t::~tex_t() { - if (!size() == 0) { + if (size() != 0) { ctx.DeleteTextures(size(), begin()); } } diff --git a/src/platform/linux/input.cpp b/src/platform/linux/input.cpp index 43433e58a92..74d683048f8 100644 --- a/src/platform/linux/input.cpp +++ b/src/platform/linux/input.cpp @@ -6,8 +6,10 @@ #include #include +extern "C" { #include #include +} #ifdef SUNSHINE_BUILD_X11 #include @@ -20,11 +22,11 @@ #include #include #include +#include #include "src/config.h" #include "src/input.h" #include "src/logging.h" -#include "src/main.h" #include "src/platform/common.h" #include "src/utility.h" @@ -585,8 +587,8 @@ namespace platf { weak_strong += data.rumble(tp); } - std::clamp(weak_strong.first, 0, 0xFFFF); - std::clamp(weak_strong.second, 0, 0xFFFF); + weak_strong.first = std::clamp(weak_strong.first, 0, 0xFFFF); + weak_strong.second = std::clamp(weak_strong.second, 0, 0xFFFF); old_rumble = weak_strong * gain / 0xFFFF; return old_rumble; diff --git a/src/platform/linux/kmsgrab.cpp b/src/platform/linux/kmsgrab.cpp index a18fc31a823..30a2470f2d3 100644 --- a/src/platform/linux/kmsgrab.cpp +++ b/src/platform/linux/kmsgrab.cpp @@ -13,9 +13,10 @@ #include #include +#include +#include "src/config.h" #include "src/logging.h" -#include "src/main.h" #include "src/platform/common.h" #include "src/round_robin.h" #include "src/utility.h" @@ -108,6 +109,7 @@ namespace platf { using obj_prop_t = util::safe_ptr; using prop_t = util::safe_ptr; using prop_blob_t = util::safe_ptr; + using version_t = util::safe_ptr; using conn_type_count_t = std::map; @@ -145,10 +147,13 @@ namespace platf { }; struct monitor_t { + // Connector attributes std::uint32_t type; - std::uint32_t index; + // Monitor index in the global list + std::uint32_t monitor_index; + platf::touch_port_t viewport; }; @@ -294,6 +299,9 @@ namespace platf { return -1; } + version_t ver { drmGetVersion(fd.el) }; + BOOST_LOG(info) << path << " -> "sv << ((ver && ver->name) ? ver->name : "UNKNOWN"); + // Open the render node for this card to share with libva. // If it fails, we'll just share the primary node instead. char *rendernode_path = drmGetRenderDeviceNameFromFd(fd.el); @@ -312,12 +320,21 @@ namespace platf { } if (drmSetClientCap(fd.el, DRM_CLIENT_CAP_UNIVERSAL_PLANES, 1)) { - BOOST_LOG(error) << "Couldn't expose some/all drm planes for card: "sv << path; + BOOST_LOG(error) << "GPU driver doesn't support universal planes: "sv << path; return -1; } if (drmSetClientCap(fd.el, DRM_CLIENT_CAP_ATOMIC, 1)) { - BOOST_LOG(warning) << "Couldn't expose some properties for card: "sv << path; + BOOST_LOG(warning) << "GPU driver doesn't support atomic mode-setting: "sv << path; +#if defined(SUNSHINE_BUILD_X11) + // We won't be able to capture the mouse cursor with KMS on non-atomic drivers, + // so fall back to X11 if it's available and the user didn't explicitly force KMS. + if (window_system == window_system_e::X11 && config::video.capture != "kms") { + BOOST_LOG(info) << "Avoiding KMS capture under X11 due to lack of atomic mode-setting"sv; + return -1; + } +#endif + BOOST_LOG(warning) << "Cursor capture may fail without atomic mode-setting support!"sv; } plane_res.reset(drmModeGetPlaneResources(fd.el)); @@ -361,6 +378,12 @@ namespace platf { return drmModeGetResources(fd.el); } + bool + is_nvidia() { + version_t ver { drmGetVersion(fd.el) }; + return ver && ver->name && strncmp(ver->name, "nvidia-drm", 10) == 0; + } + bool is_cursor(std::uint32_t plane_id) { auto props = plane_props(plane_id); @@ -601,6 +624,15 @@ namespace platf { continue; } + // Skip non-Nvidia cards if we're looking for CUDA devices + // unless NVENC is selected manually by the user + if (mem_type == mem_type_e::cuda && !card.is_nvidia()) { + BOOST_LOG(debug) << file << " is not a CUDA device"sv; + if (config::video.encoder != "nvenc") { + continue; + } + } + auto end = std::end(card); for (auto plane = std::begin(card); plane != end; ++plane) { // Skip unused planes @@ -624,8 +656,7 @@ namespace platf { } if (!fb->handles[0]) { - BOOST_LOG(error) - << "Couldn't get handle for DRM Framebuffer ["sv << plane->fb_id << "]: Possibly not permitted: do [sudo setcap cap_sys_admin+p sunshine]"sv; + BOOST_LOG(error) << "Couldn't get handle for DRM Framebuffer ["sv << plane->fb_id << "]: Probably not permitted"sv; return -1; } @@ -893,12 +924,14 @@ namespace platf { if (!prop_crtc_w || !prop_crtc_h || !prop_crtc_x || !prop_crtc_y) { BOOST_LOG(error) << "Cursor plane is missing required plane CRTC properties!"sv; + BOOST_LOG(error) << "Atomic mode-setting must be enabled to capture the cursor!"sv; cursor_plane_id = -1; captured_cursor.visible = false; return; } if (!prop_src_x || !prop_src_y || !prop_src_w || !prop_src_h) { BOOST_LOG(error) << "Cursor plane is missing required plane SRC properties!"sv; + BOOST_LOG(error) << "Atomic mode-setting must be enabled to capture the cursor!"sv; cursor_plane_id = -1; captured_cursor.visible = false; return; @@ -1036,7 +1069,7 @@ namespace platf { } inline capture_e - refresh(file_t *file, egl::surface_descriptor_t *sd) { + refresh(file_t *file, egl::surface_descriptor_t *sd, std::optional &frame_timestamp) { // Check for a change in HDR metadata if (connector_id) { auto connector_props = card.connector_props(*connector_id); @@ -1047,6 +1080,7 @@ namespace platf { } plane_t plane = drmModeGetPlane(card.fd.el, plane_id); + frame_timestamp = std::chrono::steady_clock::now(); auto fb = card.fb(plane.get()); if (!fb) { @@ -1056,8 +1090,7 @@ namespace platf { } if (!fb->handles[0]) { - BOOST_LOG(error) - << "Couldn't get handle for DRM Framebuffer ["sv << plane->fb_id << "]: Possibly not permitted: do [sudo setcap cap_sys_admin+p sunshine]"sv; + BOOST_LOG(error) << "Couldn't get handle for DRM Framebuffer ["sv << plane->fb_id << "]: Probably not permitted"sv; return capture_e::error; } @@ -1160,17 +1193,22 @@ namespace platf { capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override { auto next_frame = std::chrono::steady_clock::now(); + sleep_overshoot_tracker.reset(); + while (true) { auto now = std::chrono::steady_clock::now(); if (next_frame > now) { - std::this_thread::sleep_for((next_frame - now) / 3 * 2); + std::this_thread::sleep_for(next_frame - now); } - while (next_frame > now) { - std::this_thread::sleep_for(1ns); - now = std::chrono::steady_clock::now(); + now = std::chrono::steady_clock::now(); + std::chrono::nanoseconds overshoot_ns = now - next_frame; + log_sleep_overshoot(overshoot_ns); + + next_frame += delay; + if (next_frame < now) { // some major slowdown happened; we couldn't keep up + next_frame = now + delay; } - next_frame = now + delay; std::shared_ptr img_out; auto status = snapshot(pull_free_image_cb, img_out, 1000ms, *cursor); @@ -1237,8 +1275,13 @@ namespace platf { auto delta_width = std::min(captured_cursor.src_w, std::max(0, screen_width - cursor_x)) - cursor_delta_x; for (auto y = 0; y < delta_height; ++y) { // Offset into the cursor image to skip drawing the parts of the cursor image that are off screen - auto cursor_begin = (uint32_t *) &captured_cursor.pixels[((y + cursor_delta_y) * captured_cursor.src_w + cursor_delta_x) * 4]; - auto cursor_end = (uint32_t *) &captured_cursor.pixels[((y + cursor_delta_y) * captured_cursor.src_w + delta_width + cursor_delta_x) * 4]; + // + // NB: We must access the elements via the data() function because cursor_end may point to the + // the first element beyond the valid range of the vector. Using vector's [] operator in that + // manner is undefined behavior (and triggers errors when using debug libc++), while doing the + // same with an array is fine. + auto cursor_begin = (uint32_t *) &captured_cursor.pixels.data()[((y + cursor_delta_y) * captured_cursor.src_w + cursor_delta_x) * 4]; + auto cursor_end = (uint32_t *) &captured_cursor.pixels.data()[((y + cursor_delta_y) * captured_cursor.src_w + delta_width + cursor_delta_x) * 4]; auto pixels_begin = &pixels[(y + cursor_y) * (img.row_pitch / img.pixel_pitch) + cursor_x]; @@ -1266,7 +1309,8 @@ namespace platf { egl::surface_descriptor_t sd; - auto status = refresh(fb_fd, &sd); + std::optional frame_timestamp; + auto status = refresh(fb_fd, &sd, frame_timestamp); if (status != capture_e::ok) { return status; } @@ -1293,6 +1337,8 @@ namespace platf { gl::ctx.GetTextureSubImage(rgb->tex[0], 0, img_offset_x, img_offset_y, 0, width, height, 1, GL_BGRA, GL_UNSIGNED_BYTE, img_out->height * img_out->row_pitch, img_out->data); + img_out->frame_timestamp = frame_timestamp; + if (cursor && captured_cursor.visible) { blend_cursor(*img_out); } @@ -1371,17 +1417,22 @@ namespace platf { capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) { auto next_frame = std::chrono::steady_clock::now(); + sleep_overshoot_tracker.reset(); + while (true) { auto now = std::chrono::steady_clock::now(); if (next_frame > now) { - std::this_thread::sleep_for((next_frame - now) / 3 * 2); + std::this_thread::sleep_for(next_frame - now); } - while (next_frame > now) { - std::this_thread::sleep_for(1ns); - now = std::chrono::steady_clock::now(); + now = std::chrono::steady_clock::now(); + std::chrono::nanoseconds overshoot_ns = now - next_frame; + log_sleep_overshoot(overshoot_ns); + + next_frame += delay; + if (next_frame < now) { // some major slowdown happened; we couldn't keep up + next_frame = now + delay; } - next_frame = now + delay; std::shared_ptr img_out; auto status = snapshot(pull_free_image_cb, img_out, 1000ms, *cursor); @@ -1419,7 +1470,7 @@ namespace platf { auto img = (egl::img_descriptor_t *) img_out.get(); img->reset(); - auto status = refresh(fb_fd, &img->sd); + auto status = refresh(fb_fd, &img->sd, img->frame_timestamp); if (status != capture_e::ok) { return status; } @@ -1516,11 +1567,11 @@ namespace platf { correlate_to_wayland(std::vector &cds) { auto monitors = wl::monitors(); + BOOST_LOG(info) << "-------- Start of KMS monitor list --------"sv; + for (auto &monitor : monitors) { std::string_view name = monitor->name; - BOOST_LOG(info) << name << ": "sv << monitor->description; - // Try to convert names in the format: // {type}-{index} // {index} is n'th occurrence of {type} @@ -1553,6 +1604,7 @@ namespace platf { << monitor->viewport.width << 'x' << monitor->viewport.height; } + BOOST_LOG(info) << "Monitor " << monitor_descriptor.monitor_index << " is "sv << name << ": "sv << monitor->description; goto break_for_loop; } } @@ -1561,11 +1613,13 @@ namespace platf { BOOST_LOG(verbose) << "Reduced to name: "sv << name << ": "sv << index; } + + BOOST_LOG(info) << "--------- End of KMS monitor list ---------"sv; } // A list of names of displays accepted as display_name std::vector - kms_display_names() { + kms_display_names(mem_type_e hwdevice_type) { int count = 0; if (!fs::exists("/dev/dri")) { @@ -1597,6 +1651,18 @@ namespace platf { continue; } + // Skip non-Nvidia cards if we're looking for CUDA devices + // unless NVENC is selected manually by the user + if (hwdevice_type == mem_type_e::cuda && !card.is_nvidia()) { + BOOST_LOG(debug) << file << " is not a CUDA device"sv; + if (config::video.encoder == "nvenc") { + BOOST_LOG(warning) << "Using NVENC with your display connected to a different GPU may not work properly!"sv; + } + else { + continue; + } + } + auto crtc_to_monitor = kms::map_crtc_to_monitor(card.monitors(conn_type_count)); auto end = std::end(card); @@ -1617,8 +1683,9 @@ namespace platf { } if (!fb->handles[0]) { - BOOST_LOG(error) - << "Couldn't get handle for DRM Framebuffer ["sv << plane->fb_id << "]: Possibly not permitted: do [sudo setcap cap_sys_admin+p sunshine]"sv; + BOOST_LOG(error) << "Couldn't get handle for DRM Framebuffer ["sv << plane->fb_id << "]: Probably not permitted"sv; + BOOST_LOG((window_system != window_system_e::X11 || config::video.capture == "kms") ? fatal : error) + << "You must run [sudo setcap cap_sys_admin+p $(readlink -f $(which sunshine))] for KMS display capture to work!"sv; break; } @@ -1637,6 +1704,7 @@ namespace platf { (int) crtc->width, (int) crtc->height, }; + it->second.monitor_index = count; } kms::env_width = std::max(kms::env_width, (int) (crtc->x + crtc->width)); diff --git a/src/platform/linux/misc.cpp b/src/platform/linux/misc.cpp index 8ead76b0715..980c0804858 100644 --- a/src/platform/linux/misc.cpp +++ b/src/platform/linux/misc.cpp @@ -10,6 +10,7 @@ // standard includes #include +#include // lib includes #include @@ -26,8 +27,8 @@ #include "graphics.h" #include "misc.h" #include "src/config.h" +#include "src/entry_handler.h" #include "src/logging.h" -#include "src/main.h" #include "src/platform/common.h" #include "vaapi.h" @@ -98,24 +99,89 @@ namespace platf { return ifaddr_t { p }; } + /** + * @brief Performs migration if necessary, then returns the appdata directory. + * @details This is used for the log directory, so it cannot invoke Boost logging! + * @return The path of the appdata directory that should be used. + */ fs::path appdata() { - const char *dir; + static std::once_flag migration_flag; + static fs::path config_path; + + // Ensure migration is only attempted once + std::call_once(migration_flag, []() { + bool found = false; + bool migrate_config = true; + const char *dir; + const char *homedir; + const char *migrate_envvar; + + // Get the home directory + if ((homedir = getenv("HOME")) == nullptr || strlen(homedir) == 0) { + // If HOME is empty or not set, use the current user's home directory + homedir = getpwuid(geteuid())->pw_dir; + } - // May be set if running under a systemd service with the ConfigurationDirectory= option set. - if ((dir = getenv("CONFIGURATION_DIRECTORY")) != nullptr) { - return fs::path { dir } / "sunshine"sv; - } - // Otherwise, follow the XDG base directory specification: - // https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html - if ((dir = getenv("XDG_CONFIG_HOME")) != nullptr) { - return fs::path { dir } / "sunshine"sv; - } - if ((dir = getenv("HOME")) == nullptr) { - dir = getpwuid(geteuid())->pw_dir; - } + // May be set if running under a systemd service with the ConfigurationDirectory= option set. + if ((dir = getenv("CONFIGURATION_DIRECTORY")) != nullptr && strlen(dir) > 0) { + found = true; + config_path = fs::path(dir) / "sunshine"sv; + } + // Otherwise, follow the XDG base directory specification: + // https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html + if (!found && (dir = getenv("XDG_CONFIG_HOME")) != nullptr && strlen(dir) > 0) { + found = true; + config_path = fs::path(dir) / "sunshine"sv; + } + // As a last resort, use the home directory + if (!found) { + migrate_config = false; + config_path = fs::path(homedir) / ".config/sunshine"sv; + } + + // migrate from the old config location if necessary + migrate_envvar = getenv("SUNSHINE_MIGRATE_CONFIG"); + if (migrate_config && found && migrate_envvar && strcmp(migrate_envvar, "1") == 0) { + std::error_code ec; + fs::path old_config_path = fs::path(homedir) / ".config/sunshine"sv; + if (old_config_path != config_path && fs::exists(old_config_path, ec)) { + if (!fs::exists(config_path, ec)) { + std::cout << "Migrating config from "sv << old_config_path << " to "sv << config_path << std::endl; + if (!ec) { + // Create the new directory tree if it doesn't already exist + fs::create_directories(config_path, ec); + } + if (!ec) { + // Copy the old directory into the new location + // NB: We use a copy instead of a move so that cross-volume migrations work + fs::copy(old_config_path, config_path, fs::copy_options::recursive | fs::copy_options::copy_symlinks, ec); + } + if (!ec) { + // If the copy was successful, delete the original directory + fs::remove_all(old_config_path, ec); + if (ec) { + std::cerr << "Failed to clean up old config directory: " << ec.message() << std::endl; + + // This is not fatal. Next time we start, we'll warn the user to delete the old one. + ec.clear(); + } + } + if (ec) { + std::cerr << "Migration failed: " << ec.message() << std::endl; + config_path = old_config_path; + } + } + else { + // We cannot use Boost logging because it hasn't been initialized yet! + std::cerr << "Config exists in both "sv << old_config_path << " and "sv << config_path << ". Using "sv << config_path << " for config" << std::endl; + std::cerr << "It is recommended to remove "sv << old_config_path << std::endl; + } + } + } + }); - return fs::path { dir } / ".config/sunshine"sv; + return config_path; } std::string @@ -734,13 +800,13 @@ namespace platf { #ifdef SUNSHINE_BUILD_DRM std::vector - kms_display_names(); + kms_display_names(mem_type_e hwdevice_type); std::shared_ptr kms_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config); bool verify_kms() { - return !kms_display_names().empty(); + return !kms_display_names(mem_type_e::unknown).empty(); } #endif @@ -766,7 +832,7 @@ namespace platf { if (sources[source::WAYLAND]) return wl_display_names(); #endif #ifdef SUNSHINE_BUILD_DRM - if (sources[source::KMS]) return kms_display_names(); + if (sources[source::KMS]) return kms_display_names(hwdevice_type); #endif #ifdef SUNSHINE_BUILD_X11 if (sources[source::X11]) return x11_display_names(); @@ -836,27 +902,29 @@ namespace platf { #endif #ifdef SUNSHINE_BUILD_CUDA - if (config::video.capture.empty() || config::video.capture == "nvfbc") { + if ((config::video.capture.empty() && sources.none()) || config::video.capture == "nvfbc") { if (verify_nvfbc()) { sources[source::NVFBC] = true; } } #endif #ifdef SUNSHINE_BUILD_WAYLAND - if (config::video.capture.empty() || config::video.capture == "wlr") { + if ((config::video.capture.empty() && sources.none()) || config::video.capture == "wlr") { if (verify_wl()) { sources[source::WAYLAND] = true; } } #endif #ifdef SUNSHINE_BUILD_DRM - if (config::video.capture.empty() || config::video.capture == "kms") { + if ((config::video.capture.empty() && sources.none()) || config::video.capture == "kms") { if (verify_kms()) { sources[source::KMS] = true; } } #endif #ifdef SUNSHINE_BUILD_X11 + // We enumerate this capture backend regardless of other suitable sources, + // since it may be needed as a NvFBC fallback for software encoding on X11. if (config::video.capture.empty() || config::video.capture == "x11") { if (verify_x11()) { sources[source::X11] = true; diff --git a/src/platform/linux/wlgrab.cpp b/src/platform/linux/wlgrab.cpp index 6acde691479..a6ac4adbb96 100644 --- a/src/platform/linux/wlgrab.cpp +++ b/src/platform/linux/wlgrab.cpp @@ -2,10 +2,11 @@ * @file src/platform/linux/wlgrab.cpp * @brief todo */ +#include + #include "src/platform/common.h" #include "src/logging.h" -#include "src/main.h" #include "src/video.h" #include "cuda.h" @@ -128,16 +129,22 @@ namespace wl { capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override { auto next_frame = std::chrono::steady_clock::now(); + sleep_overshoot_tracker.reset(); + while (true) { auto now = std::chrono::steady_clock::now(); if (next_frame > now) { - std::this_thread::sleep_for((next_frame - now) / 3 * 2); + std::this_thread::sleep_for(next_frame - now); } - while (next_frame > now) { - now = std::chrono::steady_clock::now(); + now = std::chrono::steady_clock::now(); + std::chrono::nanoseconds overshoot_ns = now - next_frame; + log_sleep_overshoot(overshoot_ns); + + next_frame += delay; + if (next_frame < now) { // some major slowdown happened; we couldn't keep up + next_frame = now + delay; } - next_frame = now + delay; std::shared_ptr img_out; auto status = snapshot(pull_free_image_cb, img_out, 1000ms, *cursor); @@ -258,16 +265,22 @@ namespace wl { capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override { auto next_frame = std::chrono::steady_clock::now(); + sleep_overshoot_tracker.reset(); + while (true) { auto now = std::chrono::steady_clock::now(); if (next_frame > now) { - std::this_thread::sleep_for((next_frame - now) / 3 * 2); + std::this_thread::sleep_for(next_frame - now); } - while (next_frame > now) { - now = std::chrono::steady_clock::now(); + now = std::chrono::steady_clock::now(); + std::chrono::nanoseconds overshoot_ns = now - next_frame; + log_sleep_overshoot(overshoot_ns); + + next_frame += delay; + if (next_frame < now) { // some major slowdown happened; we couldn't keep up + next_frame = now + delay; } - next_frame = now + delay; std::shared_ptr img_out; auto status = snapshot(pull_free_image_cb, img_out, 1000ms, *cursor); @@ -423,15 +436,21 @@ namespace platf { display.roundtrip(); + BOOST_LOG(info) << "-------- Start of Wayland monitor list --------"sv; + for (int x = 0; x < interface.monitors.size(); ++x) { auto monitor = interface.monitors[x].get(); wl::env_width = std::max(wl::env_width, (int) (monitor->viewport.offset_x + monitor->viewport.width)); wl::env_height = std::max(wl::env_height, (int) (monitor->viewport.offset_y + monitor->viewport.height)); + BOOST_LOG(info) << "Monitor " << x << " is "sv << monitor->name << ": "sv << monitor->description; + display_names.emplace_back(std::to_string(x)); } + BOOST_LOG(info) << "--------- End of Wayland monitor list ---------"sv; + return display_names; } diff --git a/src/platform/linux/x11grab.cpp b/src/platform/linux/x11grab.cpp index 0c1583002b9..0d4c3d38c30 100644 --- a/src/platform/linux/x11grab.cpp +++ b/src/platform/linux/x11grab.cpp @@ -5,6 +5,7 @@ #include "src/platform/common.h" #include +#include #include #include @@ -17,8 +18,8 @@ #include #include "src/config.h" +#include "src/globals.h" #include "src/logging.h" -#include "src/main.h" #include "src/task_pool.h" #include "src/video.h" @@ -480,17 +481,22 @@ namespace platf { capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override { auto next_frame = std::chrono::steady_clock::now(); + sleep_overshoot_tracker.reset(); + while (true) { auto now = std::chrono::steady_clock::now(); if (next_frame > now) { - std::this_thread::sleep_for((next_frame - now) / 3 * 2); + std::this_thread::sleep_for(next_frame - now); } - while (next_frame > now) { - std::this_thread::sleep_for(1ns); - now = std::chrono::steady_clock::now(); + now = std::chrono::steady_clock::now(); + std::chrono::nanoseconds overshoot_ns = now - next_frame; + log_sleep_overshoot(overshoot_ns); + + next_frame += delay; + if (next_frame < now) { // some major slowdown happened; we couldn't keep up + next_frame = now + delay; } - next_frame = now + delay; std::shared_ptr img_out; auto status = snapshot(pull_free_image_cb, img_out, 1000ms, *cursor); @@ -534,6 +540,7 @@ namespace platf { auto img = (x11_img_t *) img_out.get(); XImage *x_img { x11::GetImage(xdisplay.get(), xwindow, offset_x, offset_y, width, height, AllPlanes, ZPixmap) }; + img->frame_timestamp = std::chrono::steady_clock::now(); img->width = x_img->width; img->height = x_img->height; @@ -620,17 +627,22 @@ namespace platf { capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override { auto next_frame = std::chrono::steady_clock::now(); + sleep_overshoot_tracker.reset(); + while (true) { auto now = std::chrono::steady_clock::now(); if (next_frame > now) { - std::this_thread::sleep_for((next_frame - now) / 3 * 2); + std::this_thread::sleep_for(next_frame - now); } - while (next_frame > now) { - std::this_thread::sleep_for(1ns); - now = std::chrono::steady_clock::now(); + now = std::chrono::steady_clock::now(); + std::chrono::nanoseconds overshoot_ns = now - next_frame; + log_sleep_overshoot(overshoot_ns); + + next_frame += delay; + if (next_frame < now) { // some major slowdown happened; we couldn't keep up + next_frame = now + delay; } - next_frame = now + delay; std::shared_ptr img_out; auto status = snapshot(pull_free_image_cb, img_out, 1000ms, *cursor); diff --git a/src/platform/macos/misc.mm b/src/platform/macos/misc.mm index 0a8bf1b78ae..20c2247e049 100644 --- a/src/platform/macos/misc.mm +++ b/src/platform/macos/misc.mm @@ -18,8 +18,8 @@ #include #include "misc.h" +#include "src/entry_handler.h" #include "src/logging.h" -#include "src/main.h" #include "src/platform/common.h" #include diff --git a/src/platform/windows/audio.cpp b/src/platform/windows/audio.cpp index 296112e1ad3..3e9267b32aa 100644 --- a/src/platform/windows/audio.cpp +++ b/src/platform/windows/audio.cpp @@ -7,8 +7,6 @@ #include #include -#include - #include #include @@ -19,6 +17,8 @@ #include "src/logging.h" #include "src/platform/common.h" +#include "misc.h" + // Must be the last included file // clang-format off #include "PolicyConfig.h" @@ -89,7 +89,6 @@ namespace platf::audio { PROPVARIANT prop; }; - static std::wstring_convert, wchar_t> converter; struct format_t { enum type_e : int { none, @@ -613,7 +612,7 @@ namespace platf::audio { audio::wstring_t wstring; device->GetId(&wstring); - sink.host = converter.to_bytes(wstring.get()); + sink.host = to_utf8(wstring.get()); collection_t collection; auto status = device_enum->EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE, &collection); @@ -627,7 +626,7 @@ namespace platf::audio { collection->GetCount(&count); // If the sink isn't a device name, we'll assume it's a device ID - auto virtual_device_id = find_device_id_by_name(config::audio.virtual_sink).value_or(converter.from_bytes(config::audio.virtual_sink)); + auto virtual_device_id = find_device_id_by_name(config::audio.virtual_sink).value_or(from_utf8(config::audio.virtual_sink)); auto virtual_device_found = false; for (auto x = 0; x < count; ++x) { @@ -674,7 +673,7 @@ namespace platf::audio { } if (virtual_device_found) { - auto name_suffix = converter.to_bytes(virtual_device_id); + auto name_suffix = to_utf8(virtual_device_id); sink.null = std::make_optional(sink_t::null_t { "virtual-"s.append(formats[format_t::stereo - 1].name) + name_suffix, "virtual-"s.append(formats[format_t::surr51 - 1].name) + name_suffix, @@ -749,7 +748,7 @@ namespace platf::audio { auto sink_info = get_sink_info(sink); // If the sink isn't a device name, we'll assume it's a device ID - auto wstring_device_id = find_device_id_by_name(sink).value_or(converter.from_bytes(sink_info.second.data())); + auto wstring_device_id = find_device_id_by_name(sink).value_or(from_utf8(sink_info.second.data())); if (sink_info.first == format_t::none) { // wstring_device_id does not contain virtual-(format name) @@ -839,7 +838,7 @@ namespace platf::audio { UINT count; collection->GetCount(&count); - auto wstring_name = converter.from_bytes(name.data()); + auto wstring_name = from_utf8(name.data()); for (auto x = 0; x < count; ++x) { audio::device_t device; diff --git a/src/platform/windows/display_base.cpp b/src/platform/windows/display_base.cpp index 3ae6e337d26..227efe7628f 100644 --- a/src/platform/windows/display_base.cpp +++ b/src/platform/windows/display_base.cpp @@ -3,8 +3,8 @@ * @brief todo */ #include -#include #include +#include #include @@ -16,7 +16,6 @@ typedef long NTSTATUS; #include "misc.h" #include "src/config.h" #include "src/logging.h" -#include "src/main.h" #include "src/platform/common.h" #include "src/stat_trackers.h" #include "src/video.h" @@ -155,7 +154,7 @@ namespace platf::dxgi { SetThreadExecutionState(ES_CONTINUOUS); }); - stat_trackers::min_max_avg_tracker sleep_overshoot_tracker; + sleep_overshoot_tracker.reset(); while (true) { // This will return false if the HDR state changes or for any number of other @@ -185,16 +184,8 @@ namespace platf::dxgi { } else { high_precision_sleep(sleep_period); - - if (config::sunshine.min_log_level <= 1) { - // Print sleep overshoot stats to debug log every 20 seconds - auto print_info = [&](double min_overshoot, double max_overshoot, double avg_overshoot) { - auto f = stat_trackers::one_digit_after_decimal(); - BOOST_LOG(debug) << "Sleep overshoot (min/max/avg): " << f % min_overshoot << "ms/" << f % max_overshoot << "ms/" << f % avg_overshoot << "ms"; - }; - std::chrono::nanoseconds overshoot_ns = std::chrono::steady_clock::now() - sleep_target; - sleep_overshoot_tracker.collect_and_callback_on_interval(overshoot_ns.count() / 1000000., print_info, 20s); - } + std::chrono::nanoseconds overshoot_ns = std::chrono::steady_clock::now() - sleep_target; + log_sleep_overshoot(overshoot_ns); status = snapshot(pull_free_image_cb, img_out, 0ms, *cursor); @@ -359,8 +350,15 @@ namespace platf::dxgi { return false; } + /** + * @brief Tests to determine if the Desktop Duplication API can capture the given output. + * @details When testing for enumeration only, we avoid resyncing the thread desktop. + * @param adapter The DXGI adapter to use for capture. + * @param output The DXGI output to capture. + * @param enumeration_only Specifies whether this test is occurring for display enumeration. + */ bool - test_dxgi_duplication(adapter_t &adapter, output_t &output) { + test_dxgi_duplication(adapter_t &adapter, output_t &output, bool enumeration_only) { D3D_FEATURE_LEVEL featureLevels[] { D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0, @@ -398,14 +396,26 @@ namespace platf::dxgi { for (int x = 0; x < 2; ++x) { dup_t dup; - // Ensure we can duplicate the current display - syncThreadDesktop(); + // Only resynchronize the thread desktop when not enumerating displays. + // During enumeration, the caller will do this only once to ensure + // a consistent view of available outputs. + if (!enumeration_only) { + syncThreadDesktop(); + } status = output1->DuplicateOutput((IUnknown *) device.get(), &dup); if (SUCCEEDED(status)) { return true; } - std::this_thread::sleep_for(200ms); + + // If we're not resyncing the thread desktop and we don't have permission to + // capture the current desktop, just bail immediately. Retrying won't help. + if (enumeration_only && status == E_ACCESSDENIED) { + break; + } + else { + std::this_thread::sleep_for(200ms); + } } BOOST_LOG(error) << "DuplicateOutput() test failed [0x"sv << util::hex(status).to_string_view() << ']'; @@ -447,10 +457,8 @@ namespace platf::dxgi { return -1; } - std::wstring_convert, wchar_t> converter; - - auto adapter_name = converter.from_bytes(config::video.adapter_name); - auto output_name = converter.from_bytes(display_name); + auto adapter_name = from_utf8(config::video.adapter_name); + auto output_name = from_utf8(display_name); adapter_t::pointer adapter_p; for (int tries = 0; tries < 2; ++tries) { @@ -475,7 +483,7 @@ namespace platf::dxgi { continue; } - if (desc.AttachedToDesktop && test_dxgi_duplication(adapter_tmp, output_tmp)) { + if (desc.AttachedToDesktop && test_dxgi_duplication(adapter_tmp, output_tmp, false)) { output = std::move(output_tmp); offset_x = desc.DesktopCoordinates.left; @@ -561,7 +569,7 @@ namespace platf::dxgi { DXGI_ADAPTER_DESC adapter_desc; adapter->GetDesc(&adapter_desc); - auto description = converter.to_bytes(adapter_desc.Description); + auto description = to_utf8(adapter_desc.Description); BOOST_LOG(info) << std::endl << "Device Description : " << description << std::endl @@ -1066,13 +1074,18 @@ namespace platf { BOOST_LOG(debug) << "Detecting monitors..."sv; - std::wstring_convert, wchar_t> converter; - // We must set the GPU preference before calling any DXGI APIs! if (!dxgi::probe_for_gpu_preference(config::video.output_name)) { BOOST_LOG(warning) << "Failed to set GPU preference. Capture may not work!"sv; } + // We sync the thread desktop once before we start the enumeration process + // to ensure test_dxgi_duplication() returns consistent results for all GPUs + // even if the current desktop changes during our enumeration process. + // It is critical that we either fully succeed in enumeration or fully fail, + // otherwise it can lead to the capture code switching monitors unexpectedly. + syncThreadDesktop(); + dxgi::factory1_t factory; status = CreateDXGIFactory1(IID_IDXGIFactory1, (void **) &factory); if (FAILED(status)) { @@ -1088,7 +1101,7 @@ namespace platf { BOOST_LOG(debug) << std::endl << "====== ADAPTER ====="sv << std::endl - << "Device Name : "sv << converter.to_bytes(adapter_desc.Description) << std::endl + << "Device Name : "sv << to_utf8(adapter_desc.Description) << std::endl << "Device Vendor ID : 0x"sv << util::hex(adapter_desc.VendorId).to_string_view() << std::endl << "Device Device ID : 0x"sv << util::hex(adapter_desc.DeviceId).to_string_view() << std::endl << "Device Video Mem : "sv << adapter_desc.DedicatedVideoMemory / 1048576 << " MiB"sv << std::endl @@ -1104,7 +1117,7 @@ namespace platf { DXGI_OUTPUT_DESC desc; output->GetDesc(&desc); - auto device_name = converter.to_bytes(desc.DeviceName); + auto device_name = to_utf8(desc.DeviceName); auto width = desc.DesktopCoordinates.right - desc.DesktopCoordinates.left; auto height = desc.DesktopCoordinates.bottom - desc.DesktopCoordinates.top; @@ -1116,7 +1129,7 @@ namespace platf { << std::endl; // Don't include the display in the list if we can't actually capture it - if (desc.AttachedToDesktop && dxgi::test_dxgi_duplication(adapter, output)) { + if (desc.AttachedToDesktop && dxgi::test_dxgi_duplication(adapter, output, true)) { display_names.emplace_back(std::move(device_name)); } } diff --git a/src/platform/windows/display_vram.cpp b/src/platform/windows/display_vram.cpp index 74b0bab7fbf..4aa1800ba45 100644 --- a/src/platform/windows/display_vram.cpp +++ b/src/platform/windows/display_vram.cpp @@ -4,8 +4,6 @@ */ #include -#include - #include #include @@ -27,7 +25,9 @@ extern "C" { #include -#define SUNSHINE_SHADERS_DIR SUNSHINE_ASSETS_DIR "/shaders/directx" +#if !defined(SUNSHINE_SHADERS_DIR) // for testing this needs to be defined in cmake as we don't do an install + #define SUNSHINE_SHADERS_DIR SUNSHINE_ASSETS_DIR "/shaders/directx" +#endif namespace platf { using namespace std::literals; } @@ -127,6 +127,9 @@ namespace platf::dxgi { // the first successful capture of a desktop frame bool dummy = false; + // Set to true if the image is blank (contains no content at all, including a cursor) + bool blank = true; + // Unique identifier for this image uint32_t id = 0; @@ -233,7 +236,7 @@ namespace platf::dxgi { auto xor_mask = std::begin(img_data) + bytes; for (auto x = 0; x < bytes; ++x) { - for (auto c = 7; c >= 0; --c) { + for (auto c = 7; c >= 0 && ((std::uint8_t *) pixel_data) != std::end(cursor_img); --c) { auto bit = 1 << c; auto color_type = ((*and_mask & bit) ? 1 : 0) + ((*xor_mask & bit) ? 2 : 0); @@ -306,7 +309,7 @@ namespace platf::dxgi { auto xor_mask = std::begin(img_data) + bytes; for (auto x = 0; x < bytes; ++x) { - for (auto c = 7; c >= 0; --c) { + for (auto c = 7; c >= 0 && ((std::uint8_t *) pixel_data) != std::end(cursor_img); --c) { auto bit = 1 << c; auto color_type = ((*and_mask & bit) ? 1 : 0) + ((*xor_mask & bit) ? 2 : 0); @@ -342,9 +345,8 @@ namespace platf::dxgi { #ifndef NDEBUG flags |= D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION; #endif - std::wstring_convert, wchar_t> converter; - auto wFile = converter.from_bytes(file); + auto wFile = from_utf8(file); auto status = D3DCompileFromFile(wFile.c_str(), nullptr, D3D_COMPILE_STANDARD_FILE_INCLUDE, entrypoint, shader_model, flags, 0, &compiled_p, &msg_p); if (msg_p) { @@ -385,38 +387,40 @@ namespace platf::dxgi { } auto &img = (img_d3d_t &) img_base; - auto &img_ctx = img_ctx_map[img.id]; + if (!img.blank) { + auto &img_ctx = img_ctx_map[img.id]; - // Open the shared capture texture with our ID3D11Device - if (initialize_image_context(img, img_ctx)) { - return -1; - } + // Open the shared capture texture with our ID3D11Device + if (initialize_image_context(img, img_ctx)) { + return -1; + } - // Acquire encoder mutex to synchronize with capture code - auto status = img_ctx.encoder_mutex->AcquireSync(0, INFINITE); - if (status != S_OK) { - BOOST_LOG(error) << "Failed to acquire encoder mutex [0x"sv << util::hex(status).to_string_view() << ']'; - return -1; - } + // Acquire encoder mutex to synchronize with capture code + auto status = img_ctx.encoder_mutex->AcquireSync(0, INFINITE); + if (status != S_OK) { + BOOST_LOG(error) << "Failed to acquire encoder mutex [0x"sv << util::hex(status).to_string_view() << ']'; + return -1; + } - device_ctx->OMSetRenderTargets(1, &nv12_Y_rt, nullptr); - device_ctx->VSSetShader(scene_vs.get(), nullptr, 0); - device_ctx->PSSetShader(img.format == DXGI_FORMAT_R16G16B16A16_FLOAT ? convert_Y_fp16_ps.get() : convert_Y_ps.get(), nullptr, 0); - device_ctx->RSSetViewports(1, &outY_view); - device_ctx->PSSetShaderResources(0, 1, &img_ctx.encoder_input_res); - device_ctx->Draw(3, 0); + device_ctx->OMSetRenderTargets(1, &nv12_Y_rt, nullptr); + device_ctx->VSSetShader(scene_vs.get(), nullptr, 0); + device_ctx->PSSetShader(img.format == DXGI_FORMAT_R16G16B16A16_FLOAT ? convert_Y_fp16_ps.get() : convert_Y_ps.get(), nullptr, 0); + device_ctx->RSSetViewports(1, &outY_view); + device_ctx->PSSetShaderResources(0, 1, &img_ctx.encoder_input_res); + device_ctx->Draw(3, 0); - device_ctx->OMSetRenderTargets(1, &nv12_UV_rt, nullptr); - device_ctx->VSSetShader(convert_UV_vs.get(), nullptr, 0); - device_ctx->PSSetShader(img.format == DXGI_FORMAT_R16G16B16A16_FLOAT ? convert_UV_fp16_ps.get() : convert_UV_ps.get(), nullptr, 0); - device_ctx->RSSetViewports(1, &outUV_view); - device_ctx->Draw(3, 0); + device_ctx->OMSetRenderTargets(1, &nv12_UV_rt, nullptr); + device_ctx->VSSetShader(convert_UV_vs.get(), nullptr, 0); + device_ctx->PSSetShader(img.format == DXGI_FORMAT_R16G16B16A16_FLOAT ? convert_UV_fp16_ps.get() : convert_UV_ps.get(), nullptr, 0); + device_ctx->RSSetViewports(1, &outUV_view); + device_ctx->Draw(3, 0); - // Release encoder mutex to allow capture code to reuse this image - img_ctx.encoder_mutex->ReleaseSync(0); + // Release encoder mutex to allow capture code to reuse this image + img_ctx.encoder_mutex->ReleaseSync(0); - ID3D11ShaderResourceView *emptyShaderResourceView = nullptr; - device_ctx->PSSetShaderResources(0, 1, &emptyShaderResourceView); + ID3D11ShaderResourceView *emptyShaderResourceView = nullptr; + device_ctx->PSSetShaderResources(0, 1, &emptyShaderResourceView); + } return 0; } @@ -1147,6 +1151,9 @@ namespace platf::dxgi { return { nullptr, nullptr }; } + // Clear the blank flag now that we're ready to capture into the image + d3d_img->blank = false; + return { std::move(d3d_img), std::move(lock_helper) }; }; @@ -1292,15 +1299,14 @@ namespace platf::dxgi { // Clear the image if it has been used as a dummy. // It can have the mouse cursor blended onto it. auto old_d3d_img = (img_d3d_t *) img_out.get(); - bool reclear_dummy = old_d3d_img->dummy && old_d3d_img->capture_texture; + bool reclear_dummy = !old_d3d_img->blank && old_d3d_img->capture_texture; auto [d3d_img, lock] = get_locked_d3d_img(img_out, true); if (!d3d_img) return capture_e::error; if (reclear_dummy) { - auto dummy_data = std::make_unique(d3d_img->row_pitch * d3d_img->height); - std::fill_n(dummy_data.get(), d3d_img->row_pitch * d3d_img->height, 0); - device_ctx->UpdateSubresource(d3d_img->capture_texture.get(), 0, nullptr, dummy_data.get(), d3d_img->row_pitch, 0); + const float rgb_black[] = { 0.0f, 0.0f, 0.0f, 0.0f }; + device_ctx->ClearRenderTargetView(d3d_img->capture_rt.get(), rgb_black); } if (blend_mouse_cursor_flag) { @@ -1412,6 +1418,7 @@ namespace platf::dxgi { img->width = width_before_rotation; img->height = height_before_rotation; img->id = next_image_id++; + img->blank = true; return img; } @@ -1459,20 +1466,7 @@ namespace platf::dxgi { t.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET; t.MiscFlags = D3D11_RESOURCE_MISC_SHARED_NTHANDLE | D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX; - HRESULT status; - if (dummy) { - auto dummy_data = std::make_unique(img->row_pitch * img->height); - std::fill_n(dummy_data.get(), img->row_pitch * img->height, 0); - D3D11_SUBRESOURCE_DATA initial_data { - dummy_data.get(), - (UINT) img->row_pitch, - 0 - }; - status = device->CreateTexture2D(&t, &initial_data, &img->capture_texture); - } - else { - status = device->CreateTexture2D(&t, nullptr, &img->capture_texture); - } + auto status = device->CreateTexture2D(&t, nullptr, &img->capture_texture); if (FAILED(status)) { BOOST_LOG(error) << "Failed to create img buf texture [0x"sv << util::hex(status).to_string_view() << ']'; return -1; diff --git a/src/platform/windows/input.cpp b/src/platform/windows/input.cpp index 144dac1999a..5056c2c7246 100644 --- a/src/platform/windows/input.cpp +++ b/src/platform/windows/input.cpp @@ -6,14 +6,15 @@ #include #include +#include #include #include "keylayout.h" #include "misc.h" #include "src/config.h" +#include "src/globals.h" #include "src/logging.h" -#include "src/main.h" #include "src/platform/common.h" #ifdef __MINGW32__ diff --git a/src/platform/windows/misc.cpp b/src/platform/windows/misc.cpp index 1bbfe113a38..e7bb64e52b8 100644 --- a/src/platform/windows/misc.cpp +++ b/src/platform/windows/misc.cpp @@ -2,7 +2,6 @@ * @file src/platform/windows/misc.cpp * @brief todo */ -#include #include #include #include @@ -35,8 +34,11 @@ #define NTDDI_VERSION NTDDI_WIN10 #include +#include "misc.h" + +#include "src/entry_handler.h" +#include "src/globals.h" #include "src/logging.h" -#include "src/main.h" #include "src/platform/common.h" #include "src/utility.h" #include @@ -65,8 +67,6 @@ using namespace std::literals; namespace platf { using adapteraddrs_t = util::c_ptr; - static std::wstring_convert, wchar_t> converter; - bool enabled_mouse_keys = false; MOUSEKEYS previous_mouse_keys_state; @@ -296,7 +296,7 @@ namespace platf { // Parse the environment block and populate env for (auto c = (PWCHAR) env_block; *c != UNICODE_NULL; c += wcslen(c) + 1) { // Environment variable entries end with a null-terminator, so std::wstring() will get an entire entry. - std::string env_tuple = converter.to_bytes(std::wstring { c }); + std::string env_tuple = to_utf8(std::wstring { c }); std::string env_name = env_tuple.substr(0, env_tuple.find('=')); std::string env_val = env_tuple.substr(env_tuple.find('=') + 1); @@ -370,7 +370,7 @@ namespace platf { for (const auto &entry : env) { auto name = entry.get_name(); auto value = entry.to_string(); - size += converter.from_bytes(name).length() + 1 /* L'=' */ + converter.from_bytes(value).length() + 1 /* L'\0' */; + size += from_utf8(name).length() + 1 /* L'=' */ + from_utf8(value).length() + 1 /* L'\0' */; } size += 1 /* L'\0' */; @@ -382,9 +382,9 @@ namespace platf { auto value = entry.to_string(); // Construct the NAME=VAL\0 string - append_string_to_environment_block(env_block, offset, converter.from_bytes(name)); + append_string_to_environment_block(env_block, offset, from_utf8(name)); env_block[offset++] = L'='; - append_string_to_environment_block(env_block, offset, converter.from_bytes(value)); + append_string_to_environment_block(env_block, offset, from_utf8(value)); env_block[offset++] = L'\0'; } @@ -489,7 +489,7 @@ namespace platf { auto winerror = GetLastError(); // Log the failure of reverting to self and its error code BOOST_LOG(fatal) << "Failed to revert to self after impersonation: "sv << winerror; - std::abort(); + DebugBreak(); } return ec; @@ -614,24 +614,86 @@ namespace platf { return true; } + /** + * @brief This function quotes/escapes an argument according to the Windows parsing convention. + * @param argument The raw argument to process. + * @return An argument string suitable for use by CreateProcess(). + */ + std::wstring + escape_argument(const std::wstring &argument) { + // If there are no characters requiring quoting/escaping, we're done + if (argument.find_first_of(L" \t\n\v\"") == argument.npos) { + return argument; + } + + // The algorithm implemented here comes from a MSDN blog post: + // https://web.archive.org/web/20120201194949/http://blogs.msdn.com/b/twistylittlepassagesallalike/archive/2011/04/23/everyone-quotes-arguments-the-wrong-way.aspx + std::wstring escaped_arg; + escaped_arg.push_back(L'"'); + for (auto it = argument.begin();; it++) { + auto backslash_count = 0U; + while (it != argument.end() && *it == L'\\') { + it++; + backslash_count++; + } + + if (it == argument.end()) { + escaped_arg.append(backslash_count * 2, L'\\'); + break; + } + else if (*it == L'"') { + escaped_arg.append(backslash_count * 2 + 1, L'\\'); + } + else { + escaped_arg.append(backslash_count, L'\\'); + } + + escaped_arg.push_back(*it); + } + escaped_arg.push_back(L'"'); + return escaped_arg; + } + + /** + * @brief This function escapes an argument according to cmd's parsing convention. + * @param argument An argument already escaped by `escape_argument()`. + * @return An argument string suitable for use by cmd.exe. + */ + std::wstring + escape_argument_for_cmd(const std::wstring &argument) { + // Start with the original string and modify from there + std::wstring escaped_arg = argument; + + // Look for the next cmd metacharacter + size_t match_pos = 0; + while ((match_pos = escaped_arg.find_first_of(L"()%!^\"<>&|", match_pos)) != std::wstring::npos) { + // Insert an escape character and skip past the match + escaped_arg.insert(match_pos, 1, L'^'); + match_pos += 2; + } + + return escaped_arg; + } + /** * @brief This function resolves the given raw command into a proper command string for CreateProcess(). * @details This converts URLs and non-executable file paths into a runnable command like ShellExecute(). * @param raw_cmd The raw command provided by the user. * @param working_dir The working directory for the new process. * @param token The user token currently being impersonated or `NULL` if running as ourselves. + * @param creation_flags The creation flags for CreateProcess(), which may be modified by this function. * @return A command string suitable for use by CreateProcess(). */ std::wstring - resolve_command_string(const std::string &raw_cmd, const std::wstring &working_dir, HANDLE token) { - std::wstring raw_cmd_w = converter.from_bytes(raw_cmd); + resolve_command_string(const std::string &raw_cmd, const std::wstring &working_dir, HANDLE token, DWORD &creation_flags) { + std::wstring raw_cmd_w = from_utf8(raw_cmd); // First, convert the given command into parts so we can get the executable/file/URL without parameters auto raw_cmd_parts = boost::program_options::split_winmain(raw_cmd_w); if (raw_cmd_parts.empty()) { // This is highly unexpected, but we'll just return the raw string and hope for the best. BOOST_LOG(warning) << "Failed to split command string: "sv << raw_cmd; - return converter.from_bytes(raw_cmd); + return from_utf8(raw_cmd); } auto raw_target = raw_cmd_parts.at(0); @@ -645,7 +707,7 @@ namespace platf { res = UrlGetPartW(raw_target.c_str(), scheme.data(), &out_len, URL_PART_SCHEME, 0); if (res != S_OK) { BOOST_LOG(warning) << "Failed to extract URL scheme from URL: "sv << raw_target << " ["sv << util::hex(res).to_string_view() << ']'; - return converter.from_bytes(raw_cmd); + return from_utf8(raw_cmd); } // If the target is a URL, the class is found using the URL scheme (prior to and not including the ':') @@ -657,7 +719,14 @@ namespace platf { if (extension == nullptr || *extension == 0) { // If the file has no extension, assume it's a command and allow CreateProcess() // to try to find it via PATH - return converter.from_bytes(raw_cmd); + return from_utf8(raw_cmd); + } + else if (boost::iequals(extension, L".exe")) { + // If the file has an .exe extension, we will bypass the resolution here and + // directly pass the unmodified command string to CreateProcess(). The argument + // escaping rules are subtly different between CreateProcess() and ShellExecute(), + // and we want to preserve backwards compatibility with older configs. + return from_utf8(raw_cmd); } // For regular files, the class is found using the file extension (including the dot) @@ -665,6 +734,7 @@ namespace platf { } std::array shell_command_string; + bool needs_cmd_escaping = false; { // Overriding these predefined keys affects process-wide state, so serialize all calls // to ensure the handle state is consistent while we perform the command query. @@ -673,7 +743,7 @@ namespace platf { // Override HKEY_CLASSES_ROOT and HKEY_CURRENT_USER to ensure we query the correct class info if (!override_per_user_predefined_keys(token)) { - return converter.from_bytes(raw_cmd); + return from_utf8(raw_cmd); } // Find the command string for the specified class @@ -688,7 +758,13 @@ namespace platf { // FIXME: Maybe we can improve this in the future. if (res == HRESULT_FROM_WIN32(ERROR_NO_ASSOCIATION)) { BOOST_LOG(warning) << "Using trampoline to handle target: "sv << raw_cmd; - std::wcscpy(shell_command_string.data(), L"cmd.exe /c start \"\" \"%1\" %*"); + std::wcscpy(shell_command_string.data(), L"cmd.exe /c start \"\" /wait \"%1\" %*"); + needs_cmd_escaping = true; + + // We must suppress the console window that would otherwise appear when starting cmd.exe. + creation_flags &= ~CREATE_NEW_CONSOLE; + creation_flags |= CREATE_NO_WINDOW; + res = S_OK; } @@ -698,7 +774,7 @@ namespace platf { if (res != S_OK) { BOOST_LOG(warning) << "Failed to query command string for raw command: "sv << raw_cmd << " ["sv << util::hex(res).to_string_view() << ']'; - return converter.from_bytes(raw_cmd); + return from_utf8(raw_cmd); } // Finally, construct the real command string that will be passed into CreateProcess(). @@ -752,10 +828,18 @@ namespace platf { // All arguments following the target case L'*': for (int i = 1; i < raw_cmd_parts.size(); i++) { + // Insert a space before arguments after the first one if (i > 1) { match_replacement += L' '; } - match_replacement += raw_cmd_parts.at(i); + + // Argument escaping applies only to %*, not the single substitutions like %2 + auto escaped_argument = escape_argument(raw_cmd_parts.at(i)); + if (needs_cmd_escaping) { + // If we're using the cmd.exe trampoline, we'll need to add additional escaping + escaped_argument = escape_argument_for_cmd(escaped_argument); + } + match_replacement += escaped_argument; } break; @@ -824,7 +908,7 @@ namespace platf { */ bp::child run_command(bool elevated, bool interactive, const std::string &cmd, boost::filesystem::path &working_dir, const bp::environment &env, FILE *file, std::error_code &ec, bp::group *group) { - std::wstring start_dir = converter.from_bytes(working_dir.string()); + std::wstring start_dir = from_utf8(working_dir.string()); HANDLE job = group ? group->native_handle() : nullptr; STARTUPINFOEXW startup_info = create_startup_info(file, job ? &job : nullptr, ec); PROCESS_INFORMATION process_info; @@ -849,6 +933,31 @@ namespace platf { // Create a new console for interactive processes and use no console for non-interactive processes creation_flags |= interactive ? CREATE_NEW_CONSOLE : CREATE_NO_WINDOW; + // Find the PATH variable in our environment block using a case-insensitive search + auto sunshine_wenv = boost::this_process::wenvironment(); + std::wstring path_var_name { L"PATH" }; + std::wstring old_path_val; + auto itr = std::find_if(sunshine_wenv.cbegin(), sunshine_wenv.cend(), [&](const auto &e) { return boost::iequals(e.get_name(), path_var_name); }); + if (itr != sunshine_wenv.cend()) { + // Use the existing variable if it exists, since Boost treats these as case-sensitive. + path_var_name = itr->get_name(); + old_path_val = sunshine_wenv[path_var_name].to_string(); + } + + // Temporarily prepend the specified working directory to PATH to ensure CreateProcess() + // will (preferentially) find binaries that reside in the working directory. + sunshine_wenv[path_var_name].assign(start_dir + L";" + old_path_val); + + // Restore the old PATH value for our process when we're done here + auto restore_path = util::fail_guard([&]() { + if (old_path_val.empty()) { + sunshine_wenv[path_var_name].clear(); + } + else { + sunshine_wenv[path_var_name].assign(old_path_val); + } + }); + BOOL ret; if (is_running_as_system()) { // Duplicate the current user's token @@ -873,7 +982,7 @@ namespace platf { // Open the process as the current user account, elevation is handled in the token itself. ec = impersonate_current_user(user_token, [&]() { std::wstring env_block = create_environment_block(cloned_env); - std::wstring wcmd = resolve_command_string(cmd, start_dir, user_token); + std::wstring wcmd = resolve_command_string(cmd, start_dir, user_token, creation_flags); ret = CreateProcessAsUserW(user_token, NULL, (LPWSTR) wcmd.c_str(), @@ -907,7 +1016,7 @@ namespace platf { } std::wstring env_block = create_environment_block(cloned_env); - std::wstring wcmd = resolve_command_string(cmd, start_dir, NULL); + std::wstring wcmd = resolve_command_string(cmd, start_dir, NULL, creation_flags); ret = CreateProcessW(NULL, (LPWSTR) wcmd.c_str(), NULL, @@ -1601,4 +1710,70 @@ namespace platf { } return {}; } + + /** + * @brief Converts a UTF-8 string into a UTF-16 wide string. + * @param string The UTF-8 string. + * @return The converted UTF-16 wide string. + */ + std::wstring + from_utf8(const std::string &string) { + // No conversion needed if the string is empty + if (string.empty()) { + return {}; + } + + // Get the output size required to store the string + auto output_size = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, string.data(), string.size(), nullptr, 0); + if (output_size == 0) { + auto winerr = GetLastError(); + BOOST_LOG(error) << "Failed to get UTF-16 buffer size: "sv << winerr; + return {}; + } + + // Perform the conversion + std::wstring output(output_size, L'\0'); + output_size = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, string.data(), string.size(), output.data(), output.size()); + if (output_size == 0) { + auto winerr = GetLastError(); + BOOST_LOG(error) << "Failed to convert string to UTF-16: "sv << winerr; + return {}; + } + + return output; + } + + /** + * @brief Converts a UTF-16 wide string into a UTF-8 string. + * @param string The UTF-16 wide string. + * @return The converted UTF-8 string. + */ + std::string + to_utf8(const std::wstring &string) { + // No conversion needed if the string is empty + if (string.empty()) { + return {}; + } + + // Get the output size required to store the string + auto output_size = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, string.data(), string.size(), + nullptr, 0, nullptr, nullptr); + if (output_size == 0) { + auto winerr = GetLastError(); + BOOST_LOG(error) << "Failed to get UTF-8 buffer size: "sv << winerr; + return {}; + } + + // Perform the conversion + std::string output(output_size, '\0'); + output_size = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, string.data(), string.size(), + output.data(), output.size(), nullptr, nullptr); + if (output_size == 0) { + auto winerr = GetLastError(); + BOOST_LOG(error) << "Failed to convert string to UTF-8: "sv << winerr; + return {}; + } + + return output; + } } // namespace platf diff --git a/src/platform/windows/misc.h b/src/platform/windows/misc.h index 9228ce59fe7..5a3e29b0257 100644 --- a/src/platform/windows/misc.h +++ b/src/platform/windows/misc.h @@ -20,4 +20,20 @@ namespace platf { std::chrono::nanoseconds qpc_time_difference(int64_t performance_counter1, int64_t performance_counter2); + + /** + * @brief Converts a UTF-8 string into a UTF-16 wide string. + * @param string The UTF-8 string. + * @return The converted UTF-16 wide string. + */ + std::wstring + from_utf8(const std::string &string); + + /** + * @brief Converts a UTF-16 wide string into a UTF-8 string. + * @param string The UTF-16 wide string. + * @return The converted UTF-8 string. + */ + std::string + to_utf8(const std::wstring &string); } // namespace platf diff --git a/src/platform/windows/publish.cpp b/src/platform/windows/publish.cpp index 47c16721e20..131bb5ac11f 100644 --- a/src/platform/windows/publish.cpp +++ b/src/platform/windows/publish.cpp @@ -14,7 +14,6 @@ #include "misc.h" #include "src/config.h" #include "src/logging.h" -#include "src/main.h" #include "src/network.h" #include "src/nvhttp.h" #include "src/platform/common.h" @@ -108,12 +107,10 @@ namespace platf::publish { service(bool enable, PDNS_SERVICE_INSTANCE &existing_instance) { auto alarm = safe::make_alarm(); - std::wstring_convert, wchar_t> converter; - std::wstring name { SERVICE_INSTANCE_NAME.data(), SERVICE_INSTANCE_NAME.size() }; std::wstring domain { SERVICE_TYPE_DOMAIN.data(), SERVICE_TYPE_DOMAIN.size() }; - auto host = converter.from_bytes(boost::asio::ip::host_name() + ".local"); + auto host = from_utf8(boost::asio::ip::host_name() + ".local"); DNS_SERVICE_INSTANCE instance {}; instance.pszInstanceName = name.data(); diff --git a/src/process.cpp b/src/process.cpp index fef585cdae6..89dc4dc5ae5 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -8,6 +8,7 @@ #include #include +#include #include #include @@ -23,12 +24,14 @@ #include "config.h" #include "crypto.h" #include "logging.h" -#include "main.h" #include "platform/common.h" #include "system_tray.h" #include "utility.h" #ifdef _WIN32 + // from_utf8() string conversion function + #include "platform/windows/misc.h" + // _SH constants for _wfsopen() #include #endif @@ -187,8 +190,7 @@ namespace proc { #ifdef _WIN32 // fopen() interprets the filename as an ANSI string on Windows, so we must convert it // to UTF-16 and use the wchar_t variants for proper Unicode log file path support. - std::wstring_convert, wchar_t> converter; - auto woutput = converter.from_bytes(_app.output); + auto woutput = platf::from_utf8(_app.output); // Use _SH_DENYNO to allow us to open this log file again for writing even if it is // still open from a previous execution. This is required to handle the case of a @@ -662,6 +664,12 @@ namespace proc { if (working_dir) { ctx.working_dir = parse_env_val(this_env, *working_dir); +#ifdef _WIN32 + // The working directory, unlike the command itself, should not be quoted + // when it contains spaces. Unlike POSIX, Windows forbids quotes in paths, + // so we can safely strip them all out here to avoid confusing the user. + boost::erase_all(ctx.working_dir, "\""); +#endif } if (image_path) { diff --git a/src/rtsp.cpp b/src/rtsp.cpp index 73f224b8ef1..0180fbee37a 100644 --- a/src/rtsp.cpp +++ b/src/rtsp.cpp @@ -16,9 +16,9 @@ extern "C" { #include #include "config.h" +#include "globals.h" #include "input.h" #include "logging.h" -#include "main.h" #include "network.h" #include "rtsp.h" #include "stream.h" diff --git a/src/stream.cpp b/src/stream.cpp index 12fb8663fc8..df5b3d96194 100644 --- a/src/stream.cpp +++ b/src/stream.cpp @@ -18,9 +18,9 @@ extern "C" { } #include "config.h" +#include "globals.h" #include "input.h" #include "logging.h" -#include "main.h" #include "network.h" #include "stat_trackers.h" #include "stream.h" @@ -1814,8 +1814,8 @@ namespace stream { // The alternative is that Sunshine can never start another session until it's manually restarted. auto task = []() { BOOST_LOG(fatal) << "Hang detected! Session failed to terminate in 10 seconds."sv; - log_flush(); - std::abort(); + logging::log_flush(); + lifetime::debug_trap(); }; auto force_kill = task_pool.pushDelayed(task, 10s).task_id; auto fg = util::fail_guard([&force_kill]() { diff --git a/src/system_tray.cpp b/src/system_tray.cpp index 621ace0cfb9..c34c3d75a98 100644 --- a/src/system_tray.cpp +++ b/src/system_tray.cpp @@ -38,15 +38,17 @@ // local includes #include "confighttp.h" #include "logging.h" - #include "main.h" #include "platform/common.h" #include "process.h" + #include "src/entry_handler.h" #include "version.h" using namespace std::literals; // system_tray namespace namespace system_tray { + static std::atomic tray_initialized = false; + /** * @brief Callback for opening the UI from the system tray. * @param item The tray menu item. @@ -145,6 +147,8 @@ namespace system_tray { { .text = "Restart", .cb = tray_restart_cb }, { .text = "Quit", .cb = tray_quit_cb }, { .text = nullptr } }, + .iconPathCount = 4, + .allIconPaths = { TRAY_ICON, TRAY_ICON_LOCKED, TRAY_ICON_PLAYING, TRAY_ICON_PAUSING }, }; /** @@ -237,6 +241,7 @@ namespace system_tray { BOOST_LOG(info) << "System tray created"sv; } + tray_initialized = true; while (tray_loop(1) == 0) { BOOST_LOG(debug) << "System tray loop"sv; } @@ -273,6 +278,7 @@ namespace system_tray { */ int end_tray() { + tray_initialized = false; tray_exit(); return 0; } @@ -283,6 +289,10 @@ namespace system_tray { */ void update_tray_playing(std::string app_name) { + if (!tray_initialized) { + return; + } + tray.notification_title = NULL; tray.notification_text = NULL; tray.notification_cb = NULL; @@ -305,6 +315,10 @@ namespace system_tray { */ void update_tray_pausing(std::string app_name) { + if (!tray_initialized) { + return; + } + tray.notification_title = NULL; tray.notification_text = NULL; tray.notification_cb = NULL; @@ -327,6 +341,10 @@ namespace system_tray { */ void update_tray_stopped(std::string app_name) { + if (!tray_initialized) { + return; + } + tray.notification_title = NULL; tray.notification_text = NULL; tray.notification_cb = NULL; @@ -348,6 +366,10 @@ namespace system_tray { */ void update_tray_require_pin() { + if (!tray_initialized) { + return; + } + tray.notification_title = NULL; tray.notification_text = NULL; tray.notification_cb = NULL; diff --git a/src/upnp.cpp b/src/upnp.cpp index 55c49aaf67c..f65bcb87cc4 100644 --- a/src/upnp.cpp +++ b/src/upnp.cpp @@ -7,8 +7,8 @@ #include "config.h" #include "confighttp.h" +#include "globals.h" #include "logging.h" -#include "main.h" #include "network.h" #include "nvhttp.h" #include "rtsp.h" diff --git a/src/video.cpp b/src/video.cpp index e013249880b..6c1938c2849 100644 --- a/src/video.cpp +++ b/src/video.cpp @@ -14,14 +14,13 @@ extern "C" { #include #include #include -#include } #include "cbs.h" #include "config.h" +#include "globals.h" #include "input.h" #include "logging.h" -#include "main.h" #include "nvenc/nvenc_base.h" #include "platform/common.h" #include "sync.h" @@ -51,12 +50,6 @@ namespace video { av_buffer_unref(&ref); } - using avcodec_ctx_t = util::safe_ptr; - using avcodec_frame_t = util::safe_ptr; - using avcodec_buffer_t = util::safe_ptr; - using sws_t = util::safe_ptr; - using img_event_t = std::shared_ptr>>; - namespace nv { enum class profile_h264_e : int { @@ -87,11 +80,6 @@ namespace video { }; } // namespace qsv - platf::mem_type_e - map_base_dev_type(AVHWDeviceType type); - platf::pix_fmt_e - map_pix_fmt(AVPixelFormat fmt); - util::Either dxgi_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *); util::Either @@ -288,137 +276,6 @@ namespace video { ALWAYS_REPROBE = 1 << 9, // This is an encoder of last resort and we want to aggressively probe for a better one }; - struct encoder_platform_formats_t { - virtual ~encoder_platform_formats_t() = default; - platf::mem_type_e dev_type; - platf::pix_fmt_e pix_fmt_8bit, pix_fmt_10bit; - }; - - struct encoder_platform_formats_avcodec: encoder_platform_formats_t { - using init_buffer_function_t = std::function(platf::avcodec_encode_device_t *)>; - - encoder_platform_formats_avcodec( - const AVHWDeviceType &avcodec_base_dev_type, - const AVHWDeviceType &avcodec_derived_dev_type, - const AVPixelFormat &avcodec_dev_pix_fmt, - const AVPixelFormat &avcodec_pix_fmt_8bit, - const AVPixelFormat &avcodec_pix_fmt_10bit, - const init_buffer_function_t &init_avcodec_hardware_input_buffer_function): - avcodec_base_dev_type { avcodec_base_dev_type }, - avcodec_derived_dev_type { avcodec_derived_dev_type }, - avcodec_dev_pix_fmt { avcodec_dev_pix_fmt }, - avcodec_pix_fmt_8bit { avcodec_pix_fmt_8bit }, - avcodec_pix_fmt_10bit { avcodec_pix_fmt_10bit }, - init_avcodec_hardware_input_buffer { init_avcodec_hardware_input_buffer_function } { - dev_type = map_base_dev_type(avcodec_base_dev_type); - pix_fmt_8bit = map_pix_fmt(avcodec_pix_fmt_8bit); - pix_fmt_10bit = map_pix_fmt(avcodec_pix_fmt_10bit); - } - - AVHWDeviceType avcodec_base_dev_type, avcodec_derived_dev_type; - AVPixelFormat avcodec_dev_pix_fmt; - AVPixelFormat avcodec_pix_fmt_8bit, avcodec_pix_fmt_10bit; - - init_buffer_function_t init_avcodec_hardware_input_buffer; - }; - - struct encoder_platform_formats_nvenc: encoder_platform_formats_t { - encoder_platform_formats_nvenc( - const platf::mem_type_e &dev_type, - const platf::pix_fmt_e &pix_fmt_8bit, - const platf::pix_fmt_e &pix_fmt_10bit) { - encoder_platform_formats_t::dev_type = dev_type; - encoder_platform_formats_t::pix_fmt_8bit = pix_fmt_8bit; - encoder_platform_formats_t::pix_fmt_10bit = pix_fmt_10bit; - } - }; - - struct encoder_t { - std::string_view name; - enum flag_e { - PASSED, // Is supported - REF_FRAMES_RESTRICT, // Set maximum reference frames - CBR, // Some encoders don't support CBR, if not supported --> attempt constant quantatication parameter instead - DYNAMIC_RANGE, // hdr - VUI_PARAMETERS, // AMD encoder with VAAPI doesn't add VUI parameters to SPS - MAX_FLAGS - }; - - static std::string_view - from_flag(flag_e flag) { -#define _CONVERT(x) \ - case flag_e::x: \ - return #x##sv - switch (flag) { - _CONVERT(PASSED); - _CONVERT(REF_FRAMES_RESTRICT); - _CONVERT(CBR); - _CONVERT(DYNAMIC_RANGE); - _CONVERT(VUI_PARAMETERS); - _CONVERT(MAX_FLAGS); - } -#undef _CONVERT - - return "unknown"sv; - } - - struct option_t { - KITTY_DEFAULT_CONSTR_MOVE(option_t) - option_t(const option_t &) = default; - - std::string name; - std::variant *, std::function, std::string, std::string *> value; - - option_t(std::string &&name, decltype(value) &&value): - name { std::move(name) }, value { std::move(value) } {} - }; - - const std::unique_ptr platform_formats; - - struct { - std::vector common_options; - std::vector sdr_options; - std::vector hdr_options; - std::vector fallback_options; - - // QP option to set in the case that CBR/VBR is not supported - // by the encoder. If CBR/VBR is guaranteed to be supported, - // don't specify this option to avoid wasteful encoder probing. - std::optional qp; - - std::string name; - std::bitset capabilities; - - bool - operator[](flag_e flag) const { - return capabilities[(std::size_t) flag]; - } - - std::bitset::reference - operator[](flag_e flag) { - return capabilities[(std::size_t) flag]; - } - } av1, hevc, h264; - - uint32_t flags; - }; - - struct encode_session_t { - virtual ~encode_session_t() = default; - - virtual int - convert(platf::img_t &img) = 0; - - virtual void - request_idr_frame() = 0; - - virtual void - request_normal_frame() = 0; - - virtual void - invalidate_ref_frames(int64_t first_frame, int64_t last_frame) = 0; - }; - class avcodec_encode_session_t: public encode_session_t { public: avcodec_encode_session_t() = default; @@ -586,7 +443,7 @@ namespace video { auto capture_thread_sync = safe::make_shared(start_capture_sync, end_capture_sync); #ifdef _WIN32 - static encoder_t nvenc { + encoder_t nvenc { "nvenc"sv, std::make_unique( platf::mem_type_e::dxgi, @@ -630,7 +487,7 @@ namespace video { PARALLEL_ENCODING | REF_FRAMES_INVALIDATION // flags }; #elif !defined(__APPLE__) - static encoder_t nvenc { + encoder_t nvenc { "nvenc"sv, std::make_unique( #ifdef _WIN32 @@ -718,7 +575,7 @@ namespace video { #endif #ifdef _WIN32 - static encoder_t quicksync { + encoder_t quicksync { "quicksync"sv, std::make_unique( AV_HWDEVICE_TYPE_D3D11VA, AV_HWDEVICE_TYPE_QSV, @@ -763,7 +620,9 @@ namespace video { { "profile"s, (int) qsv::profile_hevc_e::main_10 }, }, // Fallback options - {}, + { + { "low_power"s, []() { return config::video.qsv.qsv_slow_hevc ? 0 : 1; } }, + }, std::nullopt, // QP rate control fallback "hevc_qsv"s, }, @@ -797,7 +656,7 @@ namespace video { PARALLEL_ENCODING | CBR_WITH_VBR | RELAXED_COMPLIANCE | NO_RC_BUF_LIMIT }; - static encoder_t amdvce { + encoder_t amdvce { "amdvce"sv, std::make_unique( AV_HWDEVICE_TYPE_D3D11VA, AV_HWDEVICE_TYPE_NONE, @@ -813,6 +672,7 @@ namespace video { { "quality"s, &config::video.amd.amd_quality_av1 }, { "rc"s, &config::video.amd.amd_rc_av1 }, { "usage"s, &config::video.amd.amd_usage_av1 }, + { "enforce_hrd"s, &config::video.amd.amd_enforce_hrd }, }, {}, // SDR-specific options {}, // HDR-specific options @@ -834,6 +694,7 @@ namespace video { { "rc"s, &config::video.amd.amd_rc_hevc }, { "usage"s, &config::video.amd.amd_usage_hevc }, { "vbaq"s, &config::video.amd.amd_vbaq }, + { "enforce_hrd"s, &config::video.amd.amd_enforce_hrd }, }, {}, // SDR-specific options {}, // HDR-specific options @@ -853,6 +714,7 @@ namespace video { { "rc"s, &config::video.amd.amd_rc_h264 }, { "usage"s, &config::video.amd.amd_usage_h264 }, { "vbaq"s, &config::video.amd.amd_vbaq }, + { "enforce_hrd"s, &config::video.amd.amd_enforce_hrd }, }, // SDR-specific options {}, @@ -869,7 +731,7 @@ namespace video { }; #endif - static encoder_t software { + encoder_t software { "software"sv, std::make_unique( AV_HWDEVICE_TYPE_NONE, AV_HWDEVICE_TYPE_NONE, @@ -934,7 +796,7 @@ namespace video { }; #ifdef __linux__ - static encoder_t vaapi { + encoder_t vaapi { "vaapi"sv, std::make_unique( AV_HWDEVICE_TYPE_VAAPI, AV_HWDEVICE_TYPE_NONE, @@ -1002,7 +864,7 @@ namespace video { #endif #ifdef __APPLE__ - static encoder_t videotoolbox { + encoder_t videotoolbox { "videotoolbox"sv, std::make_unique( AV_HWDEVICE_TYPE_VIDEOTOOLBOX, AV_HWDEVICE_TYPE_NONE, @@ -2719,7 +2581,13 @@ namespace video { } if (chosen_encoder == nullptr) { - BOOST_LOG(fatal) << "Couldn't find any working encoder"sv; + BOOST_LOG(fatal) << "Unable to find display or encoder during startup."sv; + if (!config::video.adapter_name.empty() || !config::video.output_name.empty()) { + BOOST_LOG(fatal) << "Please ensure your manually chosen GPU and monitor are connected and powered on."sv; + } + else { + BOOST_LOG(fatal) << "Please check that a display is connected and powered on."sv; + } return -1; } diff --git a/src/video.h b/src/video.h index fec5c38b343..ba80474669f 100644 --- a/src/video.h +++ b/src/video.h @@ -11,11 +11,181 @@ extern "C" { #include +#include } struct AVPacket; namespace video { + platf::mem_type_e + map_base_dev_type(AVHWDeviceType type); + platf::pix_fmt_e + map_pix_fmt(AVPixelFormat fmt); + + void + free_ctx(AVCodecContext *ctx); + void + free_frame(AVFrame *frame); + void + free_buffer(AVBufferRef *ref); + + using avcodec_ctx_t = util::safe_ptr; + using avcodec_frame_t = util::safe_ptr; + using avcodec_buffer_t = util::safe_ptr; + using sws_t = util::safe_ptr; + using img_event_t = std::shared_ptr>>; + + struct encoder_platform_formats_t { + virtual ~encoder_platform_formats_t() = default; + platf::mem_type_e dev_type; + platf::pix_fmt_e pix_fmt_8bit, pix_fmt_10bit; + }; + + struct encoder_platform_formats_avcodec: encoder_platform_formats_t { + using init_buffer_function_t = std::function(platf::avcodec_encode_device_t *)>; + + encoder_platform_formats_avcodec( + const AVHWDeviceType &avcodec_base_dev_type, + const AVHWDeviceType &avcodec_derived_dev_type, + const AVPixelFormat &avcodec_dev_pix_fmt, + const AVPixelFormat &avcodec_pix_fmt_8bit, + const AVPixelFormat &avcodec_pix_fmt_10bit, + const init_buffer_function_t &init_avcodec_hardware_input_buffer_function): + avcodec_base_dev_type { avcodec_base_dev_type }, + avcodec_derived_dev_type { avcodec_derived_dev_type }, + avcodec_dev_pix_fmt { avcodec_dev_pix_fmt }, + avcodec_pix_fmt_8bit { avcodec_pix_fmt_8bit }, + avcodec_pix_fmt_10bit { avcodec_pix_fmt_10bit }, + init_avcodec_hardware_input_buffer { init_avcodec_hardware_input_buffer_function } { + dev_type = map_base_dev_type(avcodec_base_dev_type); + pix_fmt_8bit = map_pix_fmt(avcodec_pix_fmt_8bit); + pix_fmt_10bit = map_pix_fmt(avcodec_pix_fmt_10bit); + } + + AVHWDeviceType avcodec_base_dev_type, avcodec_derived_dev_type; + AVPixelFormat avcodec_dev_pix_fmt; + AVPixelFormat avcodec_pix_fmt_8bit, avcodec_pix_fmt_10bit; + + init_buffer_function_t init_avcodec_hardware_input_buffer; + }; + + struct encoder_platform_formats_nvenc: encoder_platform_formats_t { + encoder_platform_formats_nvenc( + const platf::mem_type_e &dev_type, + const platf::pix_fmt_e &pix_fmt_8bit, + const platf::pix_fmt_e &pix_fmt_10bit) { + encoder_platform_formats_t::dev_type = dev_type; + encoder_platform_formats_t::pix_fmt_8bit = pix_fmt_8bit; + encoder_platform_formats_t::pix_fmt_10bit = pix_fmt_10bit; + } + }; + + struct encoder_t { + std::string_view name; + enum flag_e { + PASSED, // Is supported + REF_FRAMES_RESTRICT, // Set maximum reference frames + CBR, // Some encoders don't support CBR, if not supported --> attempt constant quantatication parameter instead + DYNAMIC_RANGE, // hdr + VUI_PARAMETERS, // AMD encoder with VAAPI doesn't add VUI parameters to SPS + MAX_FLAGS + }; + + static std::string_view + from_flag(flag_e flag) { +#define _CONVERT(x) \ + case flag_e::x: \ + return std::string_view(#x) + switch (flag) { + _CONVERT(PASSED); + _CONVERT(REF_FRAMES_RESTRICT); + _CONVERT(CBR); + _CONVERT(DYNAMIC_RANGE); + _CONVERT(VUI_PARAMETERS); + _CONVERT(MAX_FLAGS); + } +#undef _CONVERT + + return { "unknown" }; + } + + struct option_t { + KITTY_DEFAULT_CONSTR_MOVE(option_t) + option_t(const option_t &) = default; + + std::string name; + std::variant *, std::function, std::string, std::string *> value; + + option_t(std::string &&name, decltype(value) &&value): + name { std::move(name) }, value { std::move(value) } {} + }; + + const std::unique_ptr platform_formats; + + struct codec_t { + std::vector common_options; + std::vector sdr_options; + std::vector hdr_options; + std::vector fallback_options; + + // QP option to set in the case that CBR/VBR is not supported + // by the encoder. If CBR/VBR is guaranteed to be supported, + // don't specify this option to avoid wasteful encoder probing. + std::optional qp; + + std::string name; + std::bitset capabilities; + + bool + operator[](flag_e flag) const { + return capabilities[(std::size_t) flag]; + } + + std::bitset::reference + operator[](flag_e flag) { + return capabilities[(std::size_t) flag]; + } + } av1, hevc, h264; + + uint32_t flags; + }; + + struct encode_session_t { + virtual ~encode_session_t() = default; + + virtual int + convert(platf::img_t &img) = 0; + + virtual void + request_idr_frame() = 0; + + virtual void + request_normal_frame() = 0; + + virtual void + invalidate_ref_frames(int64_t first_frame, int64_t last_frame) = 0; + }; + + // encoders + extern encoder_t software; + +#if !defined(__APPLE__) + extern encoder_t nvenc; // available for windows and linux +#endif + +#ifdef _WIN32 + extern encoder_t amdvce; + extern encoder_t quicksync; +#endif + +#ifdef __linux__ + extern encoder_t vaapi; +#endif + +#ifdef __APPLE__ + extern encoder_t videotoolbox; +#endif + struct packet_raw_t { virtual ~packet_raw_t() = default; @@ -154,6 +324,8 @@ namespace video { config_t config, void *channel_data); + bool + validate_encoder(encoder_t &encoder, bool expect_failure); int probe_encoders(); } // namespace video diff --git a/src_assets/common/assets/web/Navbar.vue b/src_assets/common/assets/web/Navbar.vue index 948fb4d7244..9e4e1be64f5 100644 --- a/src_assets/common/assets/web/Navbar.vue +++ b/src_assets/common/assets/web/Navbar.vue @@ -11,22 +11,22 @@ diff --git a/src_assets/common/assets/web/ResourceCard.vue b/src_assets/common/assets/web/ResourceCard.vue index e2481ca475a..aee837689cf 100644 --- a/src_assets/common/assets/web/ResourceCard.vue +++ b/src_assets/common/assets/web/ResourceCard.vue @@ -1,35 +1,32 @@