diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000000..7b056d2a7b8 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,11 @@ +On Windows we use msys2 and ucrt64 to compile. +You need to prefix commands with `C:\msys64\msys2_shell.cmd -defterm -here -no-start -ucrt64 -c`. + +Prefix build directories with `cmake-build-`. + +The test executable is named `test_sunshine` and will be located inside the `tests` directory within +the build directory. + +The project uses gtest as a test framework. + +Always follow the style guidelines defined in .clang-format for c/c++ code. diff --git a/.github/dependabot.yml b/.github/dependabot.yml index a275ce968cd..9b3ceeb27f6 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -7,23 +7,29 @@ version: 2 updates: - package-ecosystem: "cargo" directory: "/" + rebase-strategy: disabled schedule: - interval: "daily" - time: "07:30" + interval: "cron" + cronjob: "0 1 * * *" + timezone: "America/New_York" open-pull-requests-limit: 10 - package-ecosystem: "docker" directory: "/" + rebase-strategy: disabled schedule: - interval: "daily" - time: "08:00" + interval: "cron" + cronjob: "30 1 * * *" + timezone: "America/New_York" open-pull-requests-limit: 10 - package-ecosystem: "github-actions" directory: "/" + rebase-strategy: disabled schedule: - interval: "daily" - time: "08:30" + interval: "cron" + cronjob: "0 2 * * *" + timezone: "America/New_York" open-pull-requests-limit: 10 groups: docker-actions: @@ -40,11 +46,22 @@ updates: patterns: - "LizardByte/*" + - package-ecosystem: "gitsubmodule" + directory: "/" + rebase-strategy: disabled + schedule: + interval: "cron" + cronjob: "30 2 * * *" + timezone: "America/New_York" + open-pull-requests-limit: 10 + - package-ecosystem: "npm" directory: "/" + rebase-strategy: disabled schedule: - interval: "daily" - time: "09:00" + interval: "cron" + cronjob: "0 3 * * *" + timezone: "America/New_York" open-pull-requests-limit: 10 groups: dev-dependencies: @@ -53,16 +70,20 @@ updates: - package-ecosystem: "nuget" directory: "/" + rebase-strategy: disabled schedule: - interval: "daily" - time: "09:30" + interval: "cron" + cronjob: "30 3 * * *" + timezone: "America/New_York" open-pull-requests-limit: 10 - package-ecosystem: "pip" directory: "/" + rebase-strategy: disabled schedule: - interval: "daily" - time: "10:00" + interval: "cron" + cronjob: "0 4 * * *" + timezone: "America/New_York" open-pull-requests-limit: 10 groups: pytest-dependencies: @@ -70,9 +91,11 @@ updates: patterns: - "pytest*" - - package-ecosystem: "gitsubmodule" + - package-ecosystem: "rust-toolchain" directory: "/" + rebase-strategy: disabled schedule: - interval: "daily" - time: "10:30" - open-pull-requests-limit: 10 + interval: "cron" + cronjob: "30 4 * * *" + timezone: "America/New_York" + open-pull-requests-limit: 1 diff --git a/.github/workflows/_release-notifier.yml b/.github/workflows/_release-notifier.yml index b32da784700..76ee855eecf 100644 --- a/.github/workflows/_release-notifier.yml +++ b/.github/workflows/_release-notifier.yml @@ -20,4 +20,6 @@ jobs: uses: LizardByte/.github/.github/workflows/__call-release-notifier.yml@master if: github.repository_owner == 'LizardByte' secrets: + GH_EMAIL: ${{ secrets.GH_BOT_EMAIL }} + GH_NAME: ${{ secrets.GH_BOT_NAME }} GH_TOKEN: ${{ secrets.GH_BOT_TOKEN }} diff --git a/.github/workflows/_update-flathub-repo.yml b/.github/workflows/_update-flathub-repo.yml index 1f4ba3c786e..35286db9141 100644 --- a/.github/workflows/_update-flathub-repo.yml +++ b/.github/workflows/_update-flathub-repo.yml @@ -26,4 +26,6 @@ jobs: uses: LizardByte/.github/.github/workflows/__call-update-flathub-repo.yml@master if: github.repository_owner == 'LizardByte' secrets: + GH_EMAIL: ${{ secrets.GH_BOT_EMAIL }} + GH_NAME: ${{ secrets.GH_BOT_NAME }} GH_TOKEN: ${{ secrets.GH_BOT_TOKEN }} diff --git a/.github/workflows/_update-pacman-repo.yml b/.github/workflows/_update-pacman-repo.yml index c62b34a4c50..dce30c57bde 100644 --- a/.github/workflows/_update-pacman-repo.yml +++ b/.github/workflows/_update-pacman-repo.yml @@ -26,4 +26,6 @@ jobs: uses: LizardByte/.github/.github/workflows/__call-update-pacman-repo.yml@master if: github.repository_owner == 'LizardByte' secrets: + GH_EMAIL: ${{ secrets.GH_BOT_EMAIL }} + GH_NAME: ${{ secrets.GH_BOT_NAME }} GH_TOKEN: ${{ secrets.GH_BOT_TOKEN }} diff --git a/.github/workflows/ci-bundle.yml b/.github/workflows/ci-bundle.yml new file mode 100644 index 00000000000..893be6eba9a --- /dev/null +++ b/.github/workflows/ci-bundle.yml @@ -0,0 +1,29 @@ +--- +name: CI-Bundle +permissions: + contents: read + +on: + workflow_call: + secrets: + CODECOV_TOKEN: + required: false + +jobs: + bundle_analysis: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup node + id: node + uses: actions/setup-node@v6 + + - name: Install npm dependencies + run: npm install + + - name: Build + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + run: npm run build diff --git a/.github/workflows/ci-copr.yml b/.github/workflows/ci-copr.yml index 4981d4c4a0b..c4b3a098e14 100644 --- a/.github/workflows/ci-copr.yml +++ b/.github/workflows/ci-copr.yml @@ -43,13 +43,16 @@ jobs: name: Release if: github.event_name == 'release' && - startsWith(github.repository, 'LizardByte/') + startsWith(github.repository, 'LizardByte/') && + github.event.release.prerelease == true needs: - call-copr-ci + permissions: + contents: write runs-on: ubuntu-latest steps: - name: Download build artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v6 with: path: artifacts pattern: build-* @@ -59,7 +62,7 @@ jobs: run: ls -l artifacts - name: Update GitHub Release - uses: LizardByte/actions/actions/release_create@v2025.715.25226 + uses: LizardByte/actions/actions/release_create@v2025.1028.23217 with: allowUpdates: true body: ${{ github.event.release.body }} @@ -68,5 +71,5 @@ jobs: name: ${{ github.event.release.name }} prerelease: true tag: ${{ github.event.release.tag_name }} - token: ${{ secrets.GH_BOT_TOKEN }} + token: ${{ secrets.GITHUB_TOKEN }} # use built-in token to avoid repeating workflow triggers virustotal_api_key: ${{ secrets.VIRUSTOTAL_API_KEY }} diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml index e6d1c14bd62..9ebf5eadd8a 100644 --- a/.github/workflows/ci-docker.yml +++ b/.github/workflows/ci-docker.yml @@ -263,4 +263,3 @@ jobs: cache-from: type=local,src=/tmp/.buildx-cache cache-to: type=local,dest=/tmp/.buildx-cache no-cache-filters: ${{ steps.prepare.outputs.no_cache_filters }} - diff --git a/.github/workflows/ci-flatpak.yml b/.github/workflows/ci-flatpak.yml index 22456851d8e..5bb3798fc92 100644 --- a/.github/workflows/ci-flatpak.yml +++ b/.github/workflows/ci-flatpak.yml @@ -18,8 +18,9 @@ jobs: name: ${{ matrix.arch }} env: APP_ID: dev.lizardbyte.app.Sunshine + MATRIX_ARCH: ${{ matrix.arch }} NODE_VERSION: "20" - PLATFORM_VERSION: "23.08" + PLATFORM_VERSION: "24.08" runs-on: ${{ matrix.runner }} strategy: fail-fast: false @@ -30,25 +31,21 @@ jobs: - arch: aarch64 runner: ubuntu-22.04-arm steps: - - name: Maximize build space + - name: More space if: matrix.arch == 'x86_64' - uses: easimon/maximize-build-space@v10 + uses: LizardByte/actions/actions/more_space@v2025.1028.23217 with: - root-reserve-mb: 10240 - remove-dotnet: 'true' - remove-android: 'true' - remove-haskell: 'true' - remove-codeql: 'true' - remove-docker-images: 'true' + analyze-space-savings: true + clean-all: true - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: submodules: recursive - name: Setup node id: node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: ${{ env.NODE_VERSION }} @@ -60,7 +57,7 @@ jobs: - name: Setup python id: python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.12' @@ -79,9 +76,9 @@ jobs: sudo su "$(whoami)" -c "flatpak --user install -y flathub \ org.flatpak.Builder \ - org.freedesktop.Platform/${{ matrix.arch }}/${PLATFORM_VERSION} \ - org.freedesktop.Sdk/${{ matrix.arch }}/${PLATFORM_VERSION} \ - org.freedesktop.Sdk.Extension.node${NODE_VERSION}/${{ matrix.arch }}/${PLATFORM_VERSION} \ + org.freedesktop.Platform/${MATRIX_ARCH}/${PLATFORM_VERSION} \ + org.freedesktop.Sdk/${MATRIX_ARCH}/${PLATFORM_VERSION} \ + org.freedesktop.Sdk.Extension.node${NODE_VERSION}/${MATRIX_ARCH}/${PLATFORM_VERSION} \ " flatpak run org.flatpak.Builder --version @@ -103,23 +100,21 @@ jobs: - name: Configure Flatpak Manifest env: - BRANCH: ${{ github.head_ref }} + INPUT_RELEASE_VERSION: ${{ inputs.release_version }} + INPUT_RELEASE_COMMIT: ${{ inputs.release_commit }} + REPOSITORY_CLONE_URL: ${{ github.event.repository.clone_url }} run: | # variables for manifest - branch="${{ env.BRANCH }}" - build_version=${{ inputs.release_version }} - commit=${{ inputs.release_commit }} + branch="${GITHUB_REF}" + build_version="${INPUT_RELEASE_VERSION}" + commit="${INPUT_RELEASE_COMMIT}" + clone_url="${REPOSITORY_CLONE_URL}" - # check the branch variable - if [ -z "$branch" ] - then + if [ "${GITHUB_EVENT_NAME}" == "push" ]; then echo "This is a PUSH event" - branch=${{ github.ref_name }} - clone_url=${{ github.event.repository.clone_url }} - else - echo "This is a PR event" - clone_url=${{ github.event.pull_request.head.repo.clone_url }} + branch="${GITHUB_REF_NAME}" fi + echo "Branch: ${branch}" echo "Commit: ${commit}" echo "Clone URL: ${clone_url}" @@ -132,7 +127,7 @@ jobs: mkdir -p build mkdir -p artifacts - cmake -DGITHUB_CLONE_URL=${clone_url} \ + cmake -DGITHUB_CLONE_URL="${clone_url}" \ -B build \ -S . \ -DSUNSHINE_CONFIGURE_FLATPAK_MAN=ON \ @@ -147,14 +142,14 @@ jobs: run: | echo "::add-matcher::.github/matchers/gcc-strip3.json" sudo su "$(whoami)" -c "flatpak run org.flatpak.Builder \ - --arch=${{ matrix.arch }} \ + --arch=${MATRIX_ARCH} \ --force-clean \ --repo=repo \ --sandbox \ --stop-at=cuda build-sunshine ${APP_ID}.yml" cp -r .flatpak-builder copy-of-flatpak-builder sudo su "$(whoami)" -c "flatpak run org.flatpak.Builder \ - --arch=${{ matrix.arch }} \ + --arch=${MATRIX_ARCH} \ --force-clean \ --repo=repo \ --sandbox \ @@ -162,20 +157,20 @@ jobs: rm -rf .flatpak-builder mv copy-of-flatpak-builder .flatpak-builder sudo su "$(whoami)" -c "flatpak build-bundle \ - --arch=${{ matrix.arch }} \ + --arch=${MATRIX_ARCH} \ ./repo \ - ../artifacts/sunshine_${{ matrix.arch }}.flatpak ${APP_ID}" + ../artifacts/sunshine_${MATRIX_ARCH}.flatpak ${APP_ID}" sudo su "$(whoami)" -c "flatpak build-bundle \ --runtime \ - --arch=${{ matrix.arch }} \ + --arch=${MATRIX_ARCH} \ ./repo \ - ../artifacts/sunshine_debug_${{ matrix.arch }}.flatpak ${APP_ID}.Debug" + ../artifacts/sunshine_debug_${MATRIX_ARCH}.flatpak ${APP_ID}.Debug" echo "::remove-matcher owner=gcc-strip3::" - name: Lint Flatpak working-directory: build run: | - exceptions_file="${{ github.workspace }}/packaging/linux/flatpak/exceptions.json" + exceptions_file="${GITHUB_WORKSPACE}/packaging/linux/flatpak/exceptions.json" echo "Linting flatpak manifest" flatpak run --command=flatpak-builder-lint org.flatpak.Builder \ @@ -211,7 +206,7 @@ jobs: tar -czf ./artifacts/flathub.tar.gz -C ./flathub . - name: Upload Artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: build-Linux-Flatpak-${{ matrix.arch }} path: artifacts/ diff --git a/.github/workflows/ci-freebsd.yml b/.github/workflows/ci-freebsd.yml new file mode 100644 index 00000000000..c7e9da0f7a6 --- /dev/null +++ b/.github/workflows/ci-freebsd.yml @@ -0,0 +1,272 @@ +--- +name: CI-FreeBSD +permissions: + contents: read + +on: + workflow_call: + inputs: + release_commit: + required: true + type: string + release_version: + required: true + type: string + +env: + BRANCH: ${{ github.head_ref || github.ref_name }} + BUILD_VERSION: ${{ inputs.release_version }} + COMMIT: ${{ inputs.release_commit }} + FREEBSD_CLANG_VERSION: 19 + +jobs: + setup-matrix: + name: Setup Build Matrix + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.generate-matrix.outputs.matrix }} + steps: + - name: Generate Matrix + id: generate-matrix + shell: bash + run: | + # Base matrix with amd64 build + matrix='{ + "include": [ + { + "bsd_release": "14.3", + "arch": "x86_64", + "cmake_processor": "amd64", + "runner": "ubuntu-latest" + } + ] + }' + + # Add aarch64 build only if not a pull request event + if [[ "${{ github.event_name }}" != "pull_request" ]]; then + matrix=$(echo "$matrix" | jq '.include += [{ + "bsd_release": "14.3", + "arch": "aarch64", + "cmake_processor": "aarch64", + "runner": "ubuntu-latest" + }]') + fi + + # Use heredoc for multiline JSON output + { + echo "matrix<> "${GITHUB_OUTPUT}" + + echo "Generated matrix:" + echo "$matrix" | jq . + + build_freebsd: + name: ${{ matrix.cmake_processor }}-${{ matrix.bsd_release }} + runs-on: ubuntu-latest + needs: setup-matrix + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.setup-matrix.outputs.matrix) }} + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + submodules: recursive + + - name: Get Processor Count + id: processor_count + shell: bash + run: | + PROCESSOR_COUNT=$(nproc) + echo "PROCESSOR_COUNT=${PROCESSOR_COUNT}" >> "${GITHUB_OUTPUT}" + echo "PROCESSOR_COUNT: $PROCESSOR_COUNT" + + - name: Setup FreeBSD + uses: vmactions/freebsd-vm@0cd283ca698d48b59cda444a9948f48a30709627 # v1.2.8 + with: + arch: ${{ matrix.arch }} + cpu: ${{ steps.processor_count.outputs.PROCESSOR_COUNT }} + envs: 'BRANCH BUILD_VERSION COMMIT' + # TODO: there is no libcap for freebsd... we need graphics/libdrm if we find a way to use libcap + # TODO: docs are off because doxygen is too old: https://www.freshports.org/devel/doxygen/ must be >= 1.10 + prepare: | + set -e + + pkg update + pkg upgrade -y + pkg install -y \ + audio/opus \ + audio/pulseaudio \ + devel/boost-all \ + devel/cmake-core \ + devel/evdev-proto \ + devel/git \ + devel/libayatana-appindicator \ + devel/libevdev \ + devel/libnotify \ + devel/llvm${{ env.FREEBSD_CLANG_VERSION }} \ + devel/ninja \ + devel/pkgconf \ + ftp/curl \ + graphics/libdrm \ + graphics/wayland \ + lang/python312 \ + multimedia/libva \ + net/miniupnpc \ + ports-mgmt/pkg \ + security/openssl \ + shells/bash \ + www/npm \ + x11/libX11 \ + x11/libxcb \ + x11/libXfixes \ + x11/libXrandr \ + x11/libXtst \ + x11-servers/xorg-server + + # create symlink for shebang bash compatibility + ln -s /usr/local/bin/bash /bin/bash + + # setup python + ln -s /usr/local/bin/python3.12 /usr/local/bin/python + python -m ensurepip + release: ${{ matrix.bsd_release }} + run: | + set -e + # install gcvor + python -m pip install gcovr + + # fix git safe.directory issues + git config --global --add safe.directory "*" + sync: nfs # sshfs is used for build-deps; however it's much slower than nfs + + - name: Configure + shell: freebsd {0} + run: | + set -e + cd "${GITHUB_WORKSPACE}" + + cc_path="$(which clang${{ env.FREEBSD_CLANG_VERSION }})" + cxx_path="$(which clang++${{ env.FREEBSD_CLANG_VERSION }})" + + export CC="${cc_path}" + export CXX="${cxx_path}" + + mkdir -p build + cmake \ + -B build \ + -G Ninja \ + -S . \ + -DBUILD_DOCS=OFF \ + -DBUILD_WERROR=OFF \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX=/usr/local \ + -DSUNSHINE_ASSETS_DIR=share/assets \ + -DSUNSHINE_EXECUTABLE_PATH=/usr/local/bin/sunshine \ + -DSUNSHINE_ENABLE_CUDA=OFF \ + -DSUNSHINE_ENABLE_DRM=OFF \ + -DSUNSHINE_ENABLE_WAYLAND=ON \ + -DSUNSHINE_ENABLE_X11=ON \ + -DSUNSHINE_PUBLISHER_NAME="${GITHUB_REPOSITORY_OWNER}" \ + -DSUNSHINE_PUBLISHER_WEBSITE="https://app.lizardbyte.dev" \ + -DSUNSHINE_PUBLISHER_ISSUE_URL="https://app.lizardbyte.dev/support" + + - name: Build + shell: freebsd {0} + run: | + set -e + cd "${GITHUB_WORKSPACE}" + ninja -C build + + - name: Package + shell: freebsd {0} + run: | + set -e + cd "${GITHUB_WORKSPACE}" + + mkdir -p artifacts + + cd build + cpack -G FREEBSD + + # move compiled files to artifacts + mv ./cpack_artifacts/Sunshine.pkg \ + ../artifacts/Sunshine-FreeBSD-${{ matrix.bsd_release }}-${{ matrix.cmake_processor }}.pkg + + - name: Debug + if: always() + shell: bash + working-directory: build/cpack_artifacts/_CPack_Packages/FreeBSD/FREEBSD/Sunshine + run: | + echo "FreeBSD CPack Debug" + echo "===== Staging Directory Contents =====" + ls -la + echo "" + echo "===== +MANIFEST Content =====" + cat +MANIFEST + echo "" + + # use tar to print the contents of the pkg + cd "${GITHUB_WORKSPACE}/artifacts" + echo "===== Package Contents =====" + tar -tvf Sunshine-FreeBSD-${{ matrix.bsd_release }}-${{ matrix.cmake_processor }}.pkg + echo "" + echo "===== Package Statistics =====" + echo -n "Total files in package: " + tar -tf Sunshine-FreeBSD-${{ matrix.bsd_release }}-${{ matrix.cmake_processor }}.pkg 2>&1 | wc -l + echo "" + echo "Package file size:" + ls -lh Sunshine-FreeBSD-${{ matrix.bsd_release }}-${{ matrix.cmake_processor }}.pkg + + - name: Test + id: test + shell: freebsd {0} + run: | + set -e + cd "${GITHUB_WORKSPACE}/build/tests" + + export DISPLAY=:1 + Xvfb ${DISPLAY} -screen 0 1024x768x24 & + XVFB_PID=$! + + ./test_sunshine --gtest_color=yes --gtest_output=xml:test_results.xml + + kill ${XVFB_PID} + + - name: Generate gcov report + id: test_report + # any except canceled or skipped + if: >- + always() && + (steps.test.outcome == 'success' || steps.test.outcome == 'failure') + shell: freebsd {0} + run: | + cd "${GITHUB_WORKSPACE}/build" + python -m gcovr . -r ../src \ + --exclude-noncode-lines \ + --exclude-throw-branches \ + --exclude-unreachable-branches \ + --verbose \ + --xml-pretty \ + -o coverage.xml + + - name: Upload coverage artifact + if: >- + always() && + (steps.test_report.outcome == 'success') + uses: actions/upload-artifact@v5 + with: + name: coverage-FreeBSD-${{ matrix.bsd_release }}-${{ matrix.cmake_processor }} + path: | + build/coverage.xml + build/tests/test_results.xml + if-no-files-found: error + + - name: Upload Artifacts + uses: actions/upload-artifact@v5 + with: + name: build-FreeBSD-${{ matrix.bsd_release }}-${{ matrix.cmake_processor }} + path: artifacts/ + if-no-files-found: error diff --git a/.github/workflows/ci-homebrew.yml b/.github/workflows/ci-homebrew.yml index 1b8eb72464e..06b6b4a608a 100644 --- a/.github/workflows/ci-homebrew.yml +++ b/.github/workflows/ci-homebrew.yml @@ -36,12 +36,12 @@ jobs: 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: "13" - os_name: "macos" - os_version: "14" os_name: "macos" - os_version: "15" os_name: "macos" + - os_version: "26" + os_name: "macos" - os_version: "latest" os_name: "ubuntu" - os_version: "latest" # this job will only configure the formula for release, no validation @@ -49,7 +49,7 @@ jobs: release: true steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Fix homebrew python if: matrix.os_name == 'macos' && matrix.os_version == '13' @@ -74,36 +74,38 @@ jobs: - name: Configure formula env: - HEAD_REF: ${{ github.head_ref }} + INPUT_RELEASE_VERSION: ${{ inputs.release_version }} + INPUT_RELEASE_COMMIT: ${{ inputs.release_commit }} + INPUT_RELEASE_TAG: ${{ inputs.release_tag }} + MATRIX_RELEASE: ${{ matrix.release }} + PR_CLONE_URL: ${{ github.event.pull_request.head.repo.clone_url }} PR_HEAD_REF: ${{ github.event.pull_request.head.ref }} PR_DEFAULT_BRANCH: ${{ github.event.pull_request.head.repo.default_branch }} + REPOSITORY_CLONE_URL: ${{ github.event.repository.clone_url }} + REPOSITORY_DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} run: | # variables for formula - branch="${{ env.HEAD_REF }}" - build_version=${{ inputs.release_version }} - commit=${{ inputs.release_commit }} - - # check the branch variable - if [ -z "$branch" ] - then + branch="${GITHUB_HEAD_REF}" + build_version="${INPUT_RELEASE_VERSION}" + clone_url="${REPOSITORY_CLONE_URL}" + commit="${INPUT_RELEASE_COMMIT}" + default_branch="${REPOSITORY_DEFAULT_BRANCH}" + tag="${GITHUB_REF_NAME}" + + if [ "${GITHUB_EVENT_NAME}" == "push" ]; 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 }}" - - if [ "${{ matrix.release }}" == "true" ]; then + if [ "${MATRIX_RELEASE}" == "true" ]; then # we will publish the formula with the release tag - tag="${{ inputs.release_tag }}" - else - tag="${{ github.ref_name }}" + tag="${INPUT_RELEASE_TAG}" fi - else + elif [ "${GITHUB_EVENT_NAME}" == "pull_request" ]; then echo "This is a PR event" - clone_url=${{ github.event.pull_request.head.repo.clone_url }} - branch="${{ env.PR_HEAD_REF }}" - default_branch="${{ env.PR_DEFAULT_BRANCH }}" - tag="${{ env.PR_HEAD_REF }}" + clone_url=${PR_CLONE_URL} + branch="${PR_HEAD_REF}" + default_branch="${PR_DEFAULT_BRANCH}" + tag="${PR_HEAD_REF}" fi + echo "Branch: ${branch}" echo "Clone URL: ${clone_url}" echo "Tag: ${tag}" @@ -131,7 +133,7 @@ jobs: - name: Upload Artifacts if: matrix.release - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: build-Homebrew path: homebrew/ @@ -153,7 +155,7 @@ jobs: - name: Validate Homebrew Formula id: test if: matrix.release != true - uses: LizardByte/actions/actions/release_homebrew@v2025.715.25226 + uses: LizardByte/actions/actions/release_homebrew@v2025.1028.23217 with: formula_file: ${{ github.workspace }}/homebrew/sunshine.rb git_email: ${{ secrets.GIT_EMAIL }} @@ -166,7 +168,7 @@ jobs: - name: Setup python id: python if: false - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.11' @@ -200,7 +202,7 @@ jobs: matrix.release != true && (steps.test.outcome == 'success' || steps.test.outcome == 'failure') && startsWith(github.repository, 'LizardByte/') - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: coverage-Homebrew-${{ matrix.os_name }}-${{ matrix.os_version }} path: | @@ -229,7 +231,7 @@ jobs: - name: Upload Artifacts (Beta) if: matrix.release - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: beta-Homebrew path: homebrew/ diff --git a/.github/workflows/ci-linux.yml b/.github/workflows/ci-linux.yml index c844300d520..d0f6788056c 100644 --- a/.github/workflows/ci-linux.yml +++ b/.github/workflows/ci-linux.yml @@ -18,6 +18,7 @@ jobs: name: ${{ matrix.name }} env: APP_ID: dev.lizardbyte.app.Sunshine + VERSION: ${{ inputs.release_version }} runs-on: ubuntu-${{ matrix.dist }} strategy: fail-fast: false @@ -27,18 +28,14 @@ jobs: EXTRA_ARGS: '--appimage-build' dist: 22.04 steps: - - name: Maximize build space - uses: easimon/maximize-build-space@v10 + - name: More space + uses: LizardByte/actions/actions/more_space@v2025.1028.23217 with: - root-reserve-mb: 30720 - remove-dotnet: 'true' - remove-android: 'true' - remove-haskell: 'true' - remove-codeql: 'true' - remove-docker-images: 'true' + analyze-space-savings: true + clean-all: true - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: submodules: recursive @@ -62,7 +59,7 @@ jobs: - name: Setup python id: python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.11' @@ -93,20 +90,14 @@ jobs: chmod +x ./scripts/linux_build.sh echo "::add-matcher::.github/matchers/gcc.json" ./scripts/linux_build.sh \ - --publisher-name='${{ github.repository_owner }}' \ - --publisher-website='https://app.lizardbyte.dev' \ - --publisher-issue-url='https://app.lizardbyte.dev/support' \ + --publisher-name="${GITHUB_REPOSITORY_OWNER}" \ + --publisher-website="https://app.lizardbyte.dev" \ + --publisher-issue-url="https://app.lizardbyte.dev/support" \ --skip-cleanup \ --skip-package \ --ubuntu-test-repo ${{ matrix.EXTRA_ARGS }} echo "::remove-matcher owner=gcc::" - - name: Set AppImage Version - if: matrix.name == 'AppImage' - run: | - version=${{ inputs.release_version }} - echo "VERSION=${version}" >> "${GITHUB_ENV}" - - name: Package Linux - AppImage if: matrix.name == 'AppImage' working-directory: build @@ -190,7 +181,7 @@ jobs: working-directory: build run: | ${{ steps.python.outputs.python-path }} -m pip install gcovr - ${{ steps.python.outputs.python-path }} -m gcovr . -r ../src \ + ${{ steps.python.outputs.python-path }} -m gcovr --gcov-executable "gcov-${GCC_VERSION}" . -r ../src \ --exclude-noncode-lines \ --exclude-throw-branches \ --exclude-unreachable-branches \ @@ -202,7 +193,7 @@ jobs: if: >- always() && (steps.test_report.outcome == 'success') - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: coverage-Linux-${{ matrix.name }} path: | @@ -211,7 +202,7 @@ jobs: if-no-files-found: error - name: Upload Artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: build-Linux-${{ matrix.name }} path: artifacts/ diff --git a/.github/workflows/ci-windows.yml b/.github/workflows/ci-windows.yml index 03cb909b60e..8243721edc1 100644 --- a/.github/workflows/ci-windows.yml +++ b/.github/workflows/ci-windows.yml @@ -12,9 +12,6 @@ on: release_version: required: true type: string - secrets: - CODECOV_TOKEN: - required: false jobs: build_windows: @@ -34,7 +31,7 @@ jobs: toolchain: ucrt-x86_64 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: submodules: recursive @@ -164,6 +161,7 @@ jobs: # dependencies dependencies=( "git" + "mingw-w64-${TOOLCHAIN}-boost" "mingw-w64-${TOOLCHAIN}-cmake" "mingw-w64-${TOOLCHAIN}-cppwinrt" "mingw-w64-${TOOLCHAIN}-curl-winssl" @@ -238,7 +236,7 @@ jobs: - name: Setup python id: setup-python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.11' @@ -258,7 +256,6 @@ jobs: env: BRANCH: ${{ github.head_ref || github.ref_name }} BUILD_VERSION: ${{ inputs.release_version }} - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} COMMIT: ${{ inputs.release_commit }} run: | mkdir -p build @@ -269,9 +266,9 @@ jobs: -DBUILD_WERROR=ON \ -DCMAKE_BUILD_TYPE=RelWithDebInfo \ -DSUNSHINE_ASSETS_DIR=assets \ - -DSUNSHINE_PUBLISHER_NAME='${{ github.repository_owner }}' \ - -DSUNSHINE_PUBLISHER_WEBSITE='https://app.lizardbyte.dev' \ - -DSUNSHINE_PUBLISHER_ISSUE_URL='https://app.lizardbyte.dev/support' + -DSUNSHINE_PUBLISHER_NAME="${GITHUB_REPOSITORY_OWNER}" \ + -DSUNSHINE_PUBLISHER_WEBSITE="https://app.lizardbyte.dev" \ + -DSUNSHINE_PUBLISHER_ISSUE_URL="https://app.lizardbyte.dev/support" echo "::add-matcher::.github/matchers/gcc.json" ninja -C build echo "::remove-matcher owner=gcc::" @@ -294,8 +291,7 @@ jobs: id: test shell: msys2 {0} working-directory: build/tests - run: | - ./test_sunshine.exe --gtest_color=yes --gtest_output=xml:test_results.xml + run: ./test_sunshine.exe --gtest_color=yes --gtest_output=xml:test_results.xml - name: Generate gcov report id: test_report @@ -317,7 +313,7 @@ jobs: if: >- always() && (steps.test_report.outcome == 'success') - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: coverage-${{ matrix.name }} path: | @@ -340,7 +336,7 @@ jobs: a "../artifacts/Sunshine-${{ matrix.name }}-debuginfo.7z" "*.dbg" - name: Upload Artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: build-${{ matrix.name }} path: artifacts/ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000000..45e5fb0fb2a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,283 @@ +--- +name: CI +permissions: + contents: read + +on: + pull_request: + branches: + - master + types: + - opened + - synchronize + - reopened + push: + branches: + - master + workflow_dispatch: + +concurrency: + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +jobs: + github-env: + name: GitHub Env Debug + uses: LizardByte/.github/.github/workflows/__call-github-env.yml@master + + release-setup: + name: Release Setup + outputs: + publish_release: ${{ steps.release-setup.outputs.publish_release }} + release_body: ${{ steps.release-setup.outputs.release_body }} + release_commit: ${{ steps.release-setup.outputs.release_commit }} + release_generate_release_notes: ${{ steps.release-setup.outputs.release_generate_release_notes }} + release_tag: ${{ steps.release-setup.outputs.release_tag }} + release_version: ${{ steps.release-setup.outputs.release_version }} + permissions: + contents: write + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Release Setup + id: release-setup + uses: LizardByte/actions/actions/release_setup@v2025.1028.23217 + + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + + build-docker: + name: Docker + needs: release-setup + permissions: + contents: read + packages: write + uses: LizardByte/.github/.github/workflows/__call-docker.yml@master + with: + maximize_build_space: true + publish_release: ${{ needs.release-setup.outputs.publish_release }} + release_commit: ${{ needs.release-setup.outputs.release_commit }} + release_tag: ${{ needs.release-setup.outputs.release_tag }} + release_version: ${{ needs.release-setup.outputs.release_version }} + secrets: + DOCKER_HUB_USERNAME: ${{ secrets.DOCKER_HUB_USERNAME }} + DOCKER_HUB_PASSWORD: ${{ secrets.DOCKER_HUB_PASSWORD }} + DOCKER_HUB_ACCESS_TOKEN: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} + GH_BOT_NAME: ${{ secrets.GH_BOT_NAME }} + GH_BOT_TOKEN: ${{ secrets.GH_BOT_TOKEN }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + build-freebsd: + name: FreeBSD + needs: release-setup + uses: ./.github/workflows/ci-freebsd.yml + with: + release_commit: ${{ needs.release-setup.outputs.release_commit }} + release_version: ${{ needs.release-setup.outputs.release_version }} + + build-homebrew: + name: Homebrew + needs: release-setup + uses: ./.github/workflows/ci-homebrew.yml + with: + publish_release: ${{ needs.release-setup.outputs.publish_release }} + release_commit: ${{ needs.release-setup.outputs.release_commit }} + release_tag: ${{ needs.release-setup.outputs.release_tag }} + release_version: ${{ needs.release-setup.outputs.release_version }} + secrets: + GH_TOKEN: ${{ secrets.GH_BOT_TOKEN }} + GIT_EMAIL: ${{ secrets.GH_BOT_EMAIL }} + GIT_USERNAME: ${{ secrets.GH_BOT_NAME }} + + build-linux: + name: Linux + needs: release-setup + uses: ./.github/workflows/ci-linux.yml + with: + release_commit: ${{ needs.release-setup.outputs.release_commit }} + release_version: ${{ needs.release-setup.outputs.release_version }} + + build-linux-copr: + name: Linux Copr + if: github.event_name != 'push' # releases are handled directly in ci-copr.yml + needs: release-setup + permissions: + contents: write # needed to update releases + uses: ./.github/workflows/ci-copr.yml + secrets: + COPR_BETA_WEBHOOK_TOKEN: ${{ secrets.COPR_BETA_WEBHOOK_TOKEN }} + COPR_STABLE_WEBHOOK_TOKEN: ${{ secrets.COPR_STABLE_WEBHOOK_TOKEN }} + COPR_CLI_CONFIG: ${{ secrets.COPR_CLI_CONFIG }} + + build-linux-flatpak: + name: Linux Flatpak + needs: release-setup + uses: ./.github/workflows/ci-flatpak.yml + with: + release_commit: ${{ needs.release-setup.outputs.release_commit }} + release_version: ${{ needs.release-setup.outputs.release_version }} + + build-windows: + name: Windows + needs: release-setup + uses: ./.github/workflows/ci-windows.yml + with: + release_commit: ${{ needs.release-setup.outputs.release_commit }} + release_version: ${{ needs.release-setup.outputs.release_version }} + + bundle-analysis: + name: Bundle Analysis + needs: release-setup + uses: ./.github/workflows/ci-bundle.yml + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + + coverage: + name: Coverage-${{ matrix.name }} + if: >- + always() && + !cancelled() && + startsWith(github.repository, 'LizardByte/') + needs: + - build-freebsd + - build-linux + - build-linux-flatpak + - build-homebrew + - build-windows + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - name: FreeBSD-14.3-amd64 + coverage: true + pr: true + - name: FreeBSD-14.3-aarch64 + coverage: true + pr: false + - name: Linux-AppImage + coverage: true + pr: true + - name: Homebrew-macos-14 + coverage: false + pr: true + - name: Homebrew-macos-15 + coverage: false + pr: true + - name: Homebrew-macos-26 + coverage: false + pr: true + - name: Homebrew-ubuntu-latest + coverage: false + pr: true + - name: Windows-AMD64 + coverage: true + pr: true + steps: + - name: Should run + id: should_run + run: | + should_run="false" + if [ "${GITHUB_EVENT_NAME}" != "pull_request" ] || [ ${{ matrix.pr }} == "true" ]; then + should_run="true" + fi + echo "SHOULD_RUN=${should_run}" >> "${GITHUB_OUTPUT}" + + - name: Checkout + if: steps.should_run.outputs.SHOULD_RUN == 'true' + uses: actions/checkout@v6 + + - name: Download coverage artifact + if: steps.should_run.outputs.SHOULD_RUN == 'true' + uses: actions/download-artifact@v6 + with: + name: coverage-${{ matrix.name }} + path: _coverage + + - name: Upload test results + if: steps.should_run.outputs.SHOULD_RUN == 'true' + uses: codecov/test-results-action@v1 + with: + disable_search: true + fail_ci_if_error: true + files: ./_coverage/tests/test_results.xml + flags: ${{ matrix.name }} + token: ${{ secrets.CODECOV_TOKEN }} + verbose: true + + - name: Upload coverage + if: steps.should_run.outputs.SHOULD_RUN == 'true' && matrix.coverage != false + uses: codecov/codecov-action@v5 + with: + disable_search: true + fail_ci_if_error: true + files: ./_coverage/coverage.xml + flags: ${{ matrix.name }} + token: ${{ secrets.CODECOV_TOKEN }} + verbose: true + + release: + name: Release + if: + needs.release-setup.outputs.publish_release == 'true' && + startsWith(github.repository, 'LizardByte/') + needs: + - release-setup + - build-docker + - build-freebsd + - build-linux + - build-linux-flatpak + - build-homebrew + - build-windows + runs-on: ubuntu-latest + steps: + - name: Download build artifacts + uses: actions/download-artifact@v6 + with: + path: artifacts + pattern: build-* + merge-multiple: true + + - name: Debug artifacts + run: ls -l artifacts + + - name: Create/Update GitHub Release + uses: LizardByte/actions/actions/release_create@v2025.1028.23217 + with: + allowUpdates: false + body: ${{ needs.release-setup.outputs.release_body }} + generateReleaseNotes: ${{ needs.release-setup.outputs.release_generate_release_notes }} + name: ${{ needs.release-setup.outputs.release_tag }} + prerelease: true + tag: ${{ needs.release-setup.outputs.release_tag }} + token: ${{ secrets.GH_BOT_TOKEN }} + virustotal_api_key: ${{ secrets.VIRUSTOTAL_API_KEY }} + + release-homebrew-beta: + name: Release Homebrew Beta + if: + needs.release-setup.outputs.publish_release == 'true' && + startsWith(github.repository, 'LizardByte/') + needs: + - release-setup + - build-homebrew + - release + runs-on: ubuntu-latest + steps: + - name: Download homebrew artifacts + uses: actions/download-artifact@v6 + with: + name: beta-Homebrew + path: homebrew + + - name: Upload Homebrew Beta Formula + uses: LizardByte/actions/actions/release_homebrew@v2025.1028.23217 + with: + formula_file: ${{ github.workspace }}/homebrew/sunshine-beta.rb + git_email: ${{ secrets.GH_BOT_EMAIL }} + git_username: ${{ secrets.GH_BOT_NAME }} + publish: true + token: ${{ secrets.GH_BOT_TOKEN }} + validate: false diff --git a/.github/workflows/localize.yml b/.github/workflows/localize.yml index cff329c404d..9ca49144ce8 100644 --- a/.github/workflows/localize.yml +++ b/.github/workflows/localize.yml @@ -14,7 +14,7 @@ on: workflow_dispatch: env: - file: ./locale/sunshine.po + FILE: ./locale/sunshine.po jobs: localize: @@ -23,10 +23,10 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Install Python 3.9 - uses: actions/setup-python@v5 # https://github.com/actions/setup-python + uses: actions/setup-python@v6 # https://github.com/actions/setup-python with: python-version: '3.9' @@ -44,20 +44,21 @@ jobs: - name: Update Strings run: | + new_file=true + # first, try to remove existing file as xgettext does not remove unused translations - if [ -f "${{ env.file }}" ]; + if [ -f "${FILE}" ]; then - rm ${{ env.file }} - echo "new_file=false" >> "${GITHUB_ENV}" - else - echo "new_file=true" >> "${GITHUB_ENV}" + rm "${FILE}" + new_file=false fi + echo "NEW_FILE=${new_file}" >> "${GITHUB_ENV}" # extract the new strings python ./scripts/_locale.py --extract - name: git diff - if: env.new_file == 'false' + if: env.NEW_FILE == 'false' run: | # disable the pager git config --global pager.diff false @@ -67,13 +68,13 @@ jobs: # set the variable with minimal output, replacing `\t` with ` ` OUTPUT=$(git diff --numstat locale/sunshine.po | sed -e "s#\t# #g") - echo "git_diff=${OUTPUT}" >> "${GITHUB_ENV}" + echo "GIT_DIFF=${OUTPUT}" >> "${GITHUB_ENV}" - name: git reset # only run if a single line changed (date/time) and file already existed if: >- - env.git_diff == '1 1 locale/sunshine.po' && - env.new_file == 'false' + env.GIT_DIFF == '1 1 locale/sunshine.po' && + env.NEW_FILE == 'false' run: | git reset --hard diff --git a/.github/workflows/release-notifier-moonlight.yml b/.github/workflows/release-notifier-moonlight.yml index 1e1f6cf42b0..0fc94fcfe8c 100644 --- a/.github/workflows/release-notifier-moonlight.yml +++ b/.github/workflows/release-notifier-moonlight.yml @@ -14,7 +14,7 @@ jobs: steps: - name: Check if latest GitHub release id: check-release - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: script: | const latestRelease = await github.rest.repos.getLatestRelease({ diff --git a/.github/workflows/update-pages.yml b/.github/workflows/update-pages.yml index 63d50804981..7d0376c2069 100644 --- a/.github/workflows/update-pages.yml +++ b/.github/workflows/update-pages.yml @@ -4,7 +4,6 @@ permissions: contents: read on: -<<<<<<< HEAD pull_request: branches: - master @@ -15,8 +14,6 @@ on: push: branches: - master -======= ->>>>>>> master workflow_dispatch: concurrency: @@ -28,10 +25,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Upload artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: prep path: gh-pages-template/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 5cedee55828..08c0b1ba39e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,6 +24,10 @@ if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Choose the type of build." FORCE) endif() +if(CMAKE_SYSTEM_NAME STREQUAL "FreeBSD") + set(FREEBSD ON) +endif() + # set the module path, used for includes set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake") diff --git a/README.md b/README.md index 80733b2006a..da3b61b629f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@
- + Sunshine icon

Sunshine

Self-hosted game stream host for Moonlight.

@@ -22,19 +22,180 @@ ## â„šī¸ About Sunshine is a self-hosted game stream host for Moonlight. -Offering low latency, cloud gaming server capabilities with support for AMD, Intel, and Nvidia GPUs for hardware +Offering low-latency, cloud gaming server capabilities with support for AMD, Intel, and Nvidia GPUs for hardware encoding. Software encoding is also available. You can connect to Sunshine from any Moonlight client on a variety of devices. A web UI is provided to allow configuration, and client pairing, from your favorite web browser. Pair from the local server or any mobile device. LizardByte has the full documentation hosted on [Read the Docs](https://docs.lizardbyte.dev/projects/sunshine) -* [Stable](https://docs.lizardbyte.dev/projects/sunshine/latest/) -* [Beta](https://docs.lizardbyte.dev/projects/sunshine/master/) +* [Stable Docs](https://docs.lizardbyte.dev/projects/sunshine/latest/) +* [Beta Docs](https://docs.lizardbyte.dev/projects/sunshine/master/) + +## 🎮 Feature Compatibility + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Platform Feature Support
FeatureFreeBSDLinuxmacOSWindows
Gamepad Emulation
+ What type of gamepads can be emulated on the host.
+ Clients may support other gamepads. +
DualShock / DS4 (PlayStation 4)➖➖❌✅
DualSense / DS5 (PlayStation 5)❌✅❌❌
Nintendo Switch Pro✅✅❌❌
Xbox 360➖➖❌✅
Xbox One/Series✅✅❌❌
GPU Encoding
AMD/AMF✅ (vaapi)✅ (vaapi)✅ (Video Toolbox)✅
Intel QuickSync✅ (vaapi)✅ (vaapi)✅ (Video Toolbox)✅
NVIDIA NVENC✅ (vaapi)✅ (vaapi)✅ (Video Toolbox)✅
Screen Capture
DXGI➖➖➖✅
KMS❌✅➖➖
NVIDIA NvFBC➖🟡➖➖
  â†ŗ X11 Support➖✅➖➖
  â†ŗ Wayland Support➖❌➖➖
Video Toolbox➖➖✅➖
Wayland✅✅➖➖
Windows.Graphics.Capture➖➖➖🟡
  â†ŗ Portable➖➖➖✅
  â†ŗ Service➖➖➖❌
X11✅✅➖➖
+ +**Legend:** ✅ Supported | 🟡 Partial Support | ❌ Not Yet Supported | ➖ Not Applicable ## đŸ–Ĩī¸ System Requirements -@warning{These tables are a work in progress. Do not purchase hardware based on this information.} +> [!WARNING] +> These tables are a work in progress. Do not purchase hardware based on this information. @@ -49,7 +210,7 @@ LizardByte has the full documentation hosted on [Read the Docs](https://docs.liz @@ -68,20 +229,23 @@ LizardByte has the full documentation hosted on [Read the Docs](https://docs.liz - - + + - + - + - + - + + + + @@ -105,12 +269,16 @@ LizardByte has the full documentation hosted on [Read the Docs](https://docs.liz - + @@ -167,9 +335,7 @@ Our support methods are listed in our [LizardByte Docs](https://docs.lizardbyte. ## 💲 Sponsors and Supporters

- - - + Sponsors

## đŸ‘Ĩ Contributors @@ -179,17 +345,13 @@ Thank you to all the contributors who have helped make Sunshine better! ### GitHub

- - - + GitHub contributors

### CrowdIn

- - - + CrowdIn contributors

diff --git a/_codeql_detected_source_root b/_codeql_detected_source_root new file mode 120000 index 00000000000..945c9b46d68 --- /dev/null +++ b/_codeql_detected_source_root @@ -0,0 +1 @@ +. \ No newline at end of file diff --git a/cmake/FindSystemd.cmake b/cmake/FindSystemd.cmake index c41ca64d248..ee5c70d3e62 100644 --- a/cmake/FindSystemd.cmake +++ b/cmake/FindSystemd.cmake @@ -4,6 +4,7 @@ # SYSTEMD_FOUND - system has systemd # SYSTEMD_USER_UNIT_INSTALL_DIR - the systemd system unit install directory # SYSTEMD_SYSTEM_UNIT_INSTALL_DIR - the systemd user unit install directory +# SYSTEMD_MODULES_LOAD_DIR - the systemd modules-load.d directory IF (NOT WIN32) @@ -14,20 +15,21 @@ IF (NOT WIN32) if (SYSTEMD_FOUND) execute_process(COMMAND ${PKG_CONFIG_EXECUTABLE} - --variable=systemduserunitdir systemd + --variable=systemd_user_unit_dir systemd + OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_VARIABLE SYSTEMD_USER_UNIT_INSTALL_DIR) - string(REGEX REPLACE "[ \t\n]+" "" SYSTEMD_USER_UNIT_INSTALL_DIR - "${SYSTEMD_USER_UNIT_INSTALL_DIR}") - execute_process(COMMAND ${PKG_CONFIG_EXECUTABLE} - --variable=systemdsystemunitdir systemd + --variable=systemd_system_unit_dir systemd + OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_VARIABLE SYSTEMD_SYSTEM_UNIT_INSTALL_DIR) - string(REGEX REPLACE "[ \t\n]+" "" SYSTEMD_SYSTEM_UNIT_INSTALL_DIR - "${SYSTEMD_SYSTEM_UNIT_INSTALL_DIR}") + execute_process(COMMAND ${PKG_CONFIG_EXECUTABLE} + --variable=modules_load_dir systemd + OUTPUT_STRIP_TRAILING_WHITESPACE + OUTPUT_VARIABLE SYSTEMD_MODULES_LOAD_DIR) - mark_as_advanced(SYSTEMD_USER_UNIT_INSTALL_DIR SYSTEMD_SYSTEM_UNIT_INSTALL_DIR) + mark_as_advanced(SYSTEMD_USER_UNIT_INSTALL_DIR SYSTEMD_SYSTEM_UNIT_INSTALL_DIR SYSTEMD_MODULES_LOAD_DIR) endif () diff --git a/cmake/FindUdev.cmake b/cmake/FindUdev.cmake index 8343f791d35..35b03ce70b3 100644 --- a/cmake/FindUdev.cmake +++ b/cmake/FindUdev.cmake @@ -3,26 +3,50 @@ # # UDEV_FOUND - system has udev # UDEV_RULES_INSTALL_DIR - the udev rules install directory +# UDEVADM_EXECUTABLE - path to udevadm executable +# UDEV_VERSION - version of udev/systemd -IF (NOT WIN32) - +if(NOT WIN32) find_package(PkgConfig QUIET) if(PKG_CONFIG_FOUND) pkg_check_modules(UDEV "udev") endif() - if (UDEV_FOUND) + if(UDEV_FOUND) + if(UDEV_VERSION) + message(STATUS "Found udev/systemd version: ${UDEV_VERSION}") + else() + message(WARNING "Could not determine udev/systemd version") + set(UDEV_VERSION "0") + endif() + execute_process(COMMAND ${PKG_CONFIG_EXECUTABLE} - --variable=udevdir udev + --variable=udev_dir udev + OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_VARIABLE UDEV_RULES_INSTALL_DIR) - string(REGEX REPLACE "[ \t\n]+" "" UDEV_RULES_INSTALL_DIR - "${UDEV_RULES_INSTALL_DIR}") - set(UDEV_RULES_INSTALL_DIR "${UDEV_RULES_INSTALL_DIR}/rules.d") mark_as_advanced(UDEV_RULES_INSTALL_DIR) - endif () - -ENDIF () + # Check if udevadm is available + find_program(UDEVADM_EXECUTABLE udevadm + PATHS /usr/bin /bin /usr/sbin /sbin + DOC "Path to udevadm executable") + mark_as_advanced(UDEVADM_EXECUTABLE) + + # Handle version requirements + if(Udev_FIND_VERSION) + if(UDEV_VERSION VERSION_LESS Udev_FIND_VERSION) + set(UDEV_FOUND FALSE) + if(Udev_FIND_REQUIRED) + message(FATAL_ERROR "Udev version ${UDEV_VERSION} less than required version ${Udev_FIND_VERSION}") + else() + message(STATUS "Udev version ${UDEV_VERSION} less than required version ${Udev_FIND_VERSION}") + endif() + else() + message(STATUS "Udev version ${UDEV_VERSION} meets requirement (>= ${Udev_FIND_VERSION})") + endif() + endif() + endif() +endif() diff --git a/cmake/compile_definitions/linux.cmake b/cmake/compile_definitions/linux.cmake index 27f52e63fe5..6f98c355536 100644 --- a/cmake/compile_definitions/linux.cmake +++ b/cmake/compile_definitions/linux.cmake @@ -1,6 +1,10 @@ # linux specific compile definitions -add_compile_definitions(SUNSHINE_PLATFORM="linux") +if(FREEBSD) + add_compile_definitions(SUNSHINE_PLATFORM="freebsd") +else() + add_compile_definitions(SUNSHINE_PLATFORM="linux") +endif() # AppImage if(${SUNSHINE_BUILD_APPIMAGE}) @@ -21,45 +25,32 @@ if(${SUNSHINE_ENABLE_CUDA}) message(STATUS "CUDA Compiler Version: ${CMAKE_CUDA_COMPILER_VERSION}") set(CMAKE_CUDA_ARCHITECTURES "") - # https://tech.amikelive.com/node-930/cuda-compatibility-of-nvidia-display-gpu-drivers/ - if(CMAKE_CUDA_COMPILER_VERSION VERSION_LESS 6.5) - list(APPEND CMAKE_CUDA_ARCHITECTURES 10) - elseif(CMAKE_CUDA_COMPILER_VERSION VERSION_GREATER_EQUAL 6.5) - list(APPEND CMAKE_CUDA_ARCHITECTURES 50 52) + # https://docs.nvidia.com/cuda/archive/12.0.0/cuda-compiler-driver-nvcc/index.html + if(CMAKE_CUDA_COMPILER_VERSION VERSION_GREATER_EQUAL 12.0) + list(APPEND CMAKE_CUDA_ARCHITECTURES 75 80 86 87 89 90) + else() + message(FATAL_ERROR + "Sunshine requires a minimum CUDA Compiler version of 12.0. + Found version: ${CMAKE_CUDA_COMPILER_VERSION}" + ) endif() - if(CMAKE_CUDA_COMPILER_VERSION VERSION_LESS 7.0) - list(APPEND CMAKE_CUDA_ARCHITECTURES 11) - elseif(CMAKE_CUDA_COMPILER_VERSION VERSION_GREATER 7.6) - list(APPEND CMAKE_CUDA_ARCHITECTURES 60 61 62) + # https://docs.nvidia.com/cuda/archive/12.8.0/cuda-compiler-driver-nvcc/index.html + if(CMAKE_CUDA_COMPILER_VERSION VERSION_GREATER_EQUAL 12.8) + list(APPEND CMAKE_CUDA_ARCHITECTURES 100 101 120) endif() - # https://docs.nvidia.com/cuda/archive/9.2/cuda-compiler-driver-nvcc/index.html - if(CMAKE_CUDA_COMPILER_VERSION VERSION_LESS 9.0) - list(APPEND CMAKE_CUDA_ARCHITECTURES 20) - elseif(CMAKE_CUDA_COMPILER_VERSION VERSION_GREATER_EQUAL 9.0) - list(APPEND CMAKE_CUDA_ARCHITECTURES 70) + # https://docs.nvidia.com/cuda/archive/12.9.0/cuda-compiler-driver-nvcc/index.html + if(CMAKE_CUDA_COMPILER_VERSION VERSION_GREATER_EQUAL 12.9) + list(APPEND CMAKE_CUDA_ARCHITECTURES 103 121) endif() - # https://docs.nvidia.com/cuda/archive/10.0/cuda-compiler-driver-nvcc/index.html - if(CMAKE_CUDA_COMPILER_VERSION VERSION_GREATER_EQUAL 10.0) - list(APPEND CMAKE_CUDA_ARCHITECTURES 72 75) - endif() - - # https://docs.nvidia.com/cuda/archive/11.0/cuda-compiler-driver-nvcc/index.html - if(CMAKE_CUDA_COMPILER_VERSION VERSION_LESS 11.0) - list(APPEND CMAKE_CUDA_ARCHITECTURES 30) - elseif(CMAKE_CUDA_COMPILER_VERSION VERSION_GREATER_EQUAL 11.0) - list(APPEND CMAKE_CUDA_ARCHITECTURES 80) - endif() - - # https://docs.nvidia.com/cuda/archive/11.8.0/cuda-compiler-driver-nvcc/index.html - if(CMAKE_CUDA_COMPILER_VERSION VERSION_GREATER_EQUAL 11.8) - list(APPEND CMAKE_CUDA_ARCHITECTURES 86 87 89 90) - endif() - - if(CMAKE_CUDA_COMPILER_VERSION VERSION_LESS 12.0) - list(APPEND CMAKE_CUDA_ARCHITECTURES 35) + # https://docs.nvidia.com/cuda/archive/13.0.0/cuda-compiler-driver-nvcc/index.html + if(CMAKE_CUDA_COMPILER_VERSION VERSION_GREATER_EQUAL 13.0) + list(REMOVE_ITEM CMAKE_CUDA_ARCHITECTURES 101) + list(APPEND CMAKE_CUDA_ARCHITECTURES 110) + else() + list(APPEND CMAKE_CUDA_ARCHITECTURES 50 52 53 60 61 62 70 72) endif() # sort the architectures @@ -224,6 +215,9 @@ endif() # These need to be set before adding the inputtino subdirectory in order for them to be picked up set(LIBEVDEV_CUSTOM_INCLUDE_DIR "${EVDEV_INCLUDE_DIR}") set(LIBEVDEV_CUSTOM_LIBRARY "${EVDEV_LIBRARY}") +if(FREEBSD) + set(USE_UHID OFF) +endif() add_subdirectory("${CMAKE_SOURCE_DIR}/third-party/inputtino") list(APPEND SUNSHINE_EXTERNAL_LIBRARIES inputtino::libinputtino) diff --git a/cmake/compile_definitions/macos.cmake b/cmake/compile_definitions/macos.cmake index fb33d3bf235..448ad65ee0a 100644 --- a/cmake/compile_definitions/macos.cmake +++ b/cmake/compile_definitions/macos.cmake @@ -28,9 +28,6 @@ list(APPEND SUNSHINE_EXTERNAL_LIBRARIES set(APPLE_PLIST_FILE "${SUNSHINE_SOURCE_ASSETS_DIR}/macos/assets/Info.plist") -# todo - tray is not working on macos -set(SUNSHINE_TRAY 0) - set(PLATFORM_TARGET_FILES "${CMAKE_SOURCE_DIR}/src/platform/macos/av_audio.h" "${CMAKE_SOURCE_DIR}/src/platform/macos/av_audio.m" diff --git a/cmake/dependencies/Boost_Sunshine.cmake b/cmake/dependencies/Boost_Sunshine.cmake index eb2ac4095fd..0bffabc45c3 100644 --- a/cmake/dependencies/Boost_Sunshine.cmake +++ b/cmake/dependencies/Boost_Sunshine.cmake @@ -3,7 +3,7 @@ # include_guard(GLOBAL) -set(BOOST_VERSION "1.87.0") +set(BOOST_VERSION "1.89.0") set(BOOST_COMPONENTS filesystem locale @@ -55,7 +55,7 @@ if(NOT Boost_FOUND) # Limit boost to the required libraries only set(BOOST_INCLUDE_LIBRARIES ${BOOST_COMPONENTS}) set(BOOST_URL "https://github.com/boostorg/boost/releases/download/boost-${BOOST_VERSION}/boost-${BOOST_VERSION}-cmake.tar.xz") # cmake-lint: disable=C0301 - set(BOOST_HASH "SHA256=7da75f171837577a52bbf217e17f8ea576c7c246e4594d617bfde7fafd408be5") + set(BOOST_HASH "SHA256=67acec02d0d118b5de9eb441f5fb707b3a1cdd884be00ca24b9a73c995511f74") if(CMAKE_VERSION VERSION_LESS "3.24.0") FetchContent_Declare( diff --git a/cmake/dependencies/common.cmake b/cmake/dependencies/common.cmake index c92b4777b86..24a80883762 100644 --- a/cmake/dependencies/common.cmake +++ b/cmake/dependencies/common.cmake @@ -30,6 +30,9 @@ include_directories(SYSTEM ${MINIUPNP_INCLUDE_DIRS}) if(NOT DEFINED FFMPEG_PREPARED_BINARIES) if(WIN32) set(FFMPEG_PLATFORM_LIBRARIES mfplat ole32 strmiids mfuuid vpl) + elseif(FREEBSD) + # numa is not available on FreeBSD + set(FFMPEG_PLATFORM_LIBRARIES va va-drm va-x11 X11) elseif(UNIX AND NOT APPLE) set(FFMPEG_PLATFORM_LIBRARIES numa va va-drm va-x11 X11) endif() @@ -51,10 +54,10 @@ if(NOT DEFINED FFMPEG_PREPARED_BINARIES) endif() set(FFMPEG_LIBRARIES "${FFMPEG_PREPARED_BINARIES}/lib/libavcodec.a" + "${FFMPEG_PREPARED_BINARIES}/lib/libswscale.a" "${FFMPEG_PREPARED_BINARIES}/lib/libavutil.a" "${FFMPEG_PREPARED_BINARIES}/lib/libcbs.a" "${FFMPEG_PREPARED_BINARIES}/lib/libSvtAv1Enc.a" - "${FFMPEG_PREPARED_BINARIES}/lib/libswscale.a" "${FFMPEG_PREPARED_BINARIES}/lib/libx264.a" "${FFMPEG_PREPARED_BINARIES}/lib/libx265.a" ${HDR10_PLUS_LIBRARY} @@ -62,9 +65,9 @@ if(NOT DEFINED FFMPEG_PREPARED_BINARIES) else() set(FFMPEG_LIBRARIES "${FFMPEG_PREPARED_BINARIES}/lib/libavcodec.a" + "${FFMPEG_PREPARED_BINARIES}/lib/libswscale.a" "${FFMPEG_PREPARED_BINARIES}/lib/libavutil.a" "${FFMPEG_PREPARED_BINARIES}/lib/libcbs.a" - "${FFMPEG_PREPARED_BINARIES}/lib/libswscale.a" ${FFMPEG_PLATFORM_LIBRARIES}) endif() diff --git a/cmake/packaging/freebsd_custom_cpack.cmake b/cmake/packaging/freebsd_custom_cpack.cmake new file mode 100644 index 00000000000..8446789c3eb --- /dev/null +++ b/cmake/packaging/freebsd_custom_cpack.cmake @@ -0,0 +1,138 @@ +# FreeBSD post-build script to fix +POST_INSTALL and +PRE_DEINSTALL scripts +# in the generated .pkg file. +# +# This script runs AFTER CPack creates the .pkg file. We need to: +# 1. Extract the .pkg file (which is a tar.xz archive) +# 2. Add our install/deinstall scripts to the root +# 3. Remove script entries from the +MANIFEST files section +# 4. Repack the .pkg file using pkg-static + +if(NOT CPACK_GENERATOR STREQUAL "FREEBSD") + return() +endif() + +message(STATUS "FreeBSD post-build: Processing install/deinstall scripts") + +# Get script paths from the list we set +if(NOT DEFINED CPACK_FREEBSD_PACKAGE_SCRIPTS) + message(FATAL_ERROR "FreeBSD post-build: CPACK_FREEBSD_PACKAGE_SCRIPTS not defined") +endif() + +list(LENGTH CPACK_FREEBSD_PACKAGE_SCRIPTS _script_count) +if(_script_count EQUAL 0) + message(FATAL_ERROR "FreeBSD post-build: CPACK_FREEBSD_PACKAGE_SCRIPTS is empty") +endif() + +# Find the package file in CPACK_TOPLEVEL_DIRECTORY +file(GLOB _pkg_files "${CPACK_TOPLEVEL_DIRECTORY}/*.pkg") + +if(NOT _pkg_files) + message(FATAL_ERROR "FreeBSD post-build: No .pkg file found in ${CPACK_TOPLEVEL_DIRECTORY}") +endif() + +list(GET _pkg_files 0 _pkg_file) +message(STATUS "FreeBSD post-build: Found package: ${_pkg_file}") + +# Create a temporary directory for extraction +get_filename_component(_pkg_dir "${_pkg_file}" DIRECTORY) +set(_tmp_dir "${_pkg_dir}/pkg_repack_tmp") +file(REMOVE_RECURSE "${_tmp_dir}") +file(MAKE_DIRECTORY "${_tmp_dir}") + +# Extract the package using tar (pkg files are tar.xz archives) +message(STATUS "FreeBSD post-build: Extracting package...") +find_program(TAR_EXECUTABLE tar REQUIRED) +find_program(PKG_STATIC_EXECUTABLE pkg-static REQUIRED) + +execute_process( + COMMAND ${TAR_EXECUTABLE} -xf ${_pkg_file} --no-same-owner --numeric-owner + WORKING_DIRECTORY "${_tmp_dir}" + RESULT_VARIABLE _extract_result + ERROR_VARIABLE _extract_error +) + +if(NOT _extract_result EQUAL 0) + message(FATAL_ERROR "FreeBSD post-build: Failed to extract package: ${_extract_error}") +endif() + +# Debug: Check what was extracted +file(GLOB_RECURSE _extracted_files RELATIVE "${_tmp_dir}" "${_tmp_dir}/*") +list(LENGTH _extracted_files _file_count) +message(STATUS "FreeBSD post-build: Extracted ${_file_count} files") + +# Copy the install/deinstall scripts to the extracted package root +message(STATUS "FreeBSD post-build: Adding install/deinstall scripts...") + +foreach(script_path ${CPACK_FREEBSD_PACKAGE_SCRIPTS}) + if(EXISTS "${script_path}") + get_filename_component(_script_name "${script_path}" NAME) + file(COPY "${script_path}" + DESTINATION "${_tmp_dir}/" + FILE_PERMISSIONS + OWNER_READ OWNER_WRITE OWNER_EXECUTE + GROUP_READ GROUP_EXECUTE + WORLD_READ WORLD_EXECUTE) + message(STATUS " Added: ${_script_name}") + else() + message(FATAL_ERROR "FreeBSD post-build: Script not found: ${script_path}") + endif() +endforeach() + +# Repack the package using pkg-static create +message(STATUS "FreeBSD post-build: Repacking package...") + +# Debug: Verify files before repacking +file(GLOB_RECURSE _files_before_repack RELATIVE "${_tmp_dir}" "${_tmp_dir}/*") +list(LENGTH _files_before_repack _count_before_repack) +message(STATUS "FreeBSD post-build: About to repack ${_count_before_repack} files") + +# Debug: Check directory structure +if(EXISTS "${_tmp_dir}/usr") + message(STATUS "FreeBSD post-build: Found usr directory in extracted package") + file(GLOB_RECURSE _usr_files RELATIVE "${_tmp_dir}/usr" "${_tmp_dir}/usr/*") + list(LENGTH _usr_files _usr_file_count) + message(STATUS "FreeBSD post-build: usr directory contains ${_usr_file_count} files") +endif() + +# Create metadata directory separate from rootdir +set(_metadata_dir "${_tmp_dir}/metadata") +file(MAKE_DIRECTORY "${_metadata_dir}") + +# Move manifest and scripts to metadata directory +file(GLOB _metadata_files "${_tmp_dir}/+*") +foreach(meta_file ${_metadata_files}) + get_filename_component(_meta_name "${meta_file}" NAME) + file(RENAME "${meta_file}" "${_metadata_dir}/${_meta_name}") + message(STATUS "FreeBSD post-build: Moved ${_meta_name} to metadata directory") +endforeach() + +# Use pkg-static create to rebuild the package +# pkg create -r rootdir -m manifestdir -o outdir +# The rootdir should contain the actual files (usr/local/...) +# The manifestdir should contain +MANIFEST and install scripts +execute_process( + COMMAND ${PKG_STATIC_EXECUTABLE} create -r ${_tmp_dir} -m ${_metadata_dir} -o ${_pkg_dir} + RESULT_VARIABLE _pack_result + OUTPUT_VARIABLE _pack_output + ERROR_VARIABLE _pack_error +) + +if(NOT _pack_result EQUAL 0) + message(FATAL_ERROR "FreeBSD post-build: Failed to repack package: ${_pack_error}") +endif() + +# Find the generated package file (pkg create generates its own name based on manifest) +file(GLOB _new_pkg_files "${_pkg_dir}/Sunshine-*.pkg") +if(NOT _new_pkg_files) + message(FATAL_ERROR "FreeBSD post-build: pkg-static create succeeded but no package file was generated") +endif() + +list(GET _new_pkg_files 0 _generated_pkg) + +# Replace the original package with the newly created one +file(REMOVE "${_pkg_file}") +file(RENAME "${_generated_pkg}" "${_pkg_file}") +message(STATUS "FreeBSD post-build: Successfully processed package") + +# Clean up +file(REMOVE_RECURSE "${_tmp_dir}") diff --git a/cmake/packaging/linux.cmake b/cmake/packaging/linux.cmake index 659bbc37ab3..3be29976597 100644 --- a/cmake/packaging/linux.cmake +++ b/cmake/packaging/linux.cmake @@ -14,6 +14,8 @@ file(CREATE_LINK "${SUNSHINE_SOURCE_ASSETS_DIR}/linux/assets/shaders" if(${SUNSHINE_BUILD_APPIMAGE} OR ${SUNSHINE_BUILD_FLATPAK}) install(FILES "${SUNSHINE_SOURCE_ASSETS_DIR}/linux/misc/60-sunshine.rules" DESTINATION "${SUNSHINE_ASSETS_DIR}/udev/rules.d") + install(FILES "${SUNSHINE_SOURCE_ASSETS_DIR}/linux/misc/60-sunshine.conf" + DESTINATION "${SUNSHINE_ASSETS_DIR}/modules-load.d") install(FILES "${CMAKE_CURRENT_BINARY_DIR}/sunshine.service" DESTINATION "${SUNSHINE_ASSETS_DIR}/systemd/user") else() @@ -27,13 +29,39 @@ else() if(SYSTEMD_FOUND) install(FILES "${CMAKE_CURRENT_BINARY_DIR}/sunshine.service" DESTINATION "${SYSTEMD_USER_UNIT_INSTALL_DIR}") + install(FILES "${SUNSHINE_SOURCE_ASSETS_DIR}/linux/misc/60-sunshine.conf" + DESTINATION "${SYSTEMD_MODULES_LOAD_DIR}") endif() endif() +# RPM specific +set(CPACK_RPM_PACKAGE_LICENSE "GPLv3") + +# FreeBSD specific +set(CPACK_FREEBSD_PACKAGE_MAINTAINER "${CPACK_PACKAGE_VENDOR}") +set(CPACK_FREEBSD_PACKAGE_ORIGIN "misc/${CPACK_PACKAGE_NAME}") +set(CPACK_FREEBSD_PACKAGE_LICENSE "GPLv3") + # Post install set(CPACK_DEBIAN_PACKAGE_CONTROL_EXTRA "${SUNSHINE_SOURCE_ASSETS_DIR}/linux/misc/postinst") set(CPACK_RPM_POST_INSTALL_SCRIPT_FILE "${SUNSHINE_SOURCE_ASSETS_DIR}/linux/misc/postinst") +# FreeBSD post install/deinstall scripts +if(FREEBSD) + # Note: CPack's FreeBSD generator does NOT natively support install/deinstall scripts + # like CPACK_DEBIAN_PACKAGE_CONTROL_EXTRA or CPACK_RPM_POST_INSTALL_SCRIPT_FILE. + # This is a known limitation of the CPack FREEBSD generator. + # + # Workaround: Use CPACK_POST_BUILD_SCRIPTS to extract the generated .pkg file, + # add the install/deinstall scripts, and repack the package. This ensures they are + # recognized as package control scripts rather than installed files. + set(CPACK_FREEBSD_PACKAGE_SCRIPTS + "${SUNSHINE_SOURCE_ASSETS_DIR}/bsd/misc/+POST_INSTALL" + "${SUNSHINE_SOURCE_ASSETS_DIR}/bsd/misc/+PRE_DEINSTALL" + ) + list(APPEND CPACK_POST_BUILD_SCRIPTS "${CMAKE_MODULE_PATH}/packaging/freebsd_custom_cpack.cmake") +endif() + # Apply setcap for RPM # https://github.com/coreos/rpm-ostree/discussions/5036#discussioncomment-10291071 set(CPACK_RPM_USER_FILELIST "%caps(cap_sys_admin+p) ${SUNSHINE_EXECUTABLE_PATH}") @@ -42,9 +70,11 @@ set(CPACK_RPM_USER_FILELIST "%caps(cap_sys_admin+p) ${SUNSHINE_EXECUTABLE_PATH}" set(CPACK_DEB_COMPONENT_INSTALL ON) set(CPACK_DEBIAN_PACKAGE_DEPENDS "\ ${CPACK_DEB_PLATFORM_PACKAGE_DEPENDS} \ + debianutils, \ libcap2, \ libcurl4, \ libdrm2, \ + libgbm1, \ libevdev2, \ libnuma1, \ libopus0, \ @@ -65,10 +95,21 @@ set(CPACK_RPM_PACKAGE_REQUIRES "\ libva >= 2.14.0, \ libwayland-client >= 1.20.0, \ libX11 >= 1.7.3.1, \ + mesa-libgbm >= 25.0.7, \ miniupnpc >= 2.2.4, \ numactl-libs >= 2.0.14, \ openssl >= 3.0.2, \ - pulseaudio-libs >= 10.0") + pulseaudio-libs >= 10.0, \ + which >= 2.21") +list(APPEND CPACK_FREEBSD_PACKAGE_DEPS + audio/opus + ftp/curl + devel/libevdev + net/avahi + x11/libX11 + net/miniupnpc + security/openssl +) if(NOT BOOST_USE_STATIC) set(CPACK_DEBIAN_PACKAGE_DEPENDS "\ @@ -83,10 +124,14 @@ if(NOT BOOST_USE_STATIC) boost-locale >= ${Boost_VERSION}, \ boost-log >= ${Boost_VERSION}, \ boost-program-options >= ${Boost_VERSION}") + list(APPEND CPACK_FREEBSD_PACKAGE_DEPS + devel/boost-libs + ) endif() -# This should automatically figure out dependencies, doesn't work with the current config -set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS OFF) +# This should automatically figure out dependencies on packages +set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON) +set(CPACK_RPM_PACKAGE_AUTOREQ ON) # application icon if(NOT ${SUNSHINE_BUILD_FLATPAK}) @@ -133,6 +178,10 @@ if(${SUNSHINE_TRAY} STREQUAL 1) set(CPACK_RPM_PACKAGE_REQUIRES "\ ${CPACK_RPM_PACKAGE_REQUIRES}, \ libappindicator-gtk3 >= 12.10.0") + list(APPEND CPACK_FREEBSD_PACKAGE_DEPS + devel/libayatana-appindicator + devel/libnotify + ) endif() # desktop file diff --git a/cmake/packaging/windows_nsis.cmake b/cmake/packaging/windows_nsis.cmake index 2f4f2d6cf28..8ac1ce955a3 100644 --- a/cmake/packaging/windows_nsis.cmake +++ b/cmake/packaging/windows_nsis.cmake @@ -16,7 +16,7 @@ SET(CPACK_NSIS_EXTRA_INSTALL_COMMANDS nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\migrate-config.bat\\\"' nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\add-firewall-rule.bat\\\"' nsExec::ExecToLog \ - 'powershell.exe -ExecutionPolicy Bypass -File \\\"$INSTDIR\\\\scripts\\\\install-gamepad.ps1\\\"' + 'powershell.exe -NoProfile -ExecutionPolicy Bypass -File \\\"$INSTDIR\\\\scripts\\\\install-gamepad.ps1\\\"' nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\install-service.bat\\\"' nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\autostart-service.bat\\\"' NoController: @@ -33,7 +33,8 @@ set(CPACK_NSIS_EXTRA_UNINSTALL_COMMANDS 'Do you want to remove Virtual Gamepad?' \ /SD IDNO IDNO NoGamepad nsExec::ExecToLog \ - 'powershell.exe -ExecutionPolicy Bypass -File \\\"$INSTDIR\\\\scripts\\\\uninstall-gamepad.ps1\\\"'; \ + 'powershell.exe -NoProfile -ExecutionPolicy Bypass -File \ + \\\"$INSTDIR\\\\scripts\\\\uninstall-gamepad.ps1\\\"'; \ skipped if no NoGamepad: MessageBox MB_YESNO|MB_ICONQUESTION \ diff --git a/cmake/prep/options.cmake b/cmake/prep/options.cmake index cc2e1e1f59b..b1b916ac8e2 100644 --- a/cmake/prep/options.cmake +++ b/cmake/prep/options.cmake @@ -16,7 +16,7 @@ option(BUILD_WERROR "Enable -Werror flag." OFF) # if this option is set, the build will exit after configuring special package configuration files option(SUNSHINE_CONFIGURE_ONLY "Configure special files only, then exit." OFF) -option(SUNSHINE_ENABLE_TRAY "Enable system tray icon. This option will be ignored on macOS." ON) +option(SUNSHINE_ENABLE_TRAY "Enable system tray icon." ON) option(SUNSHINE_SYSTEM_WAYLAND_PROTOCOLS "Use system installation of wayland-protocols rather than the submodule." OFF) diff --git a/cmake/targets/linux.cmake b/cmake/targets/linux.cmake index fa1f33c0752..8ce7e024a04 100644 --- a/cmake/targets/linux.cmake +++ b/cmake/targets/linux.cmake @@ -1 +1,4 @@ # linux specific target definitions + +# Using newer c++ compilers / features on older distros causes runtime dyn link errors +target_link_libraries(sunshine -static-libgcc -static-libstdc++) diff --git a/docker/archlinux.dockerfile b/docker/archlinux.bak similarity index 92% rename from docker/archlinux.dockerfile rename to docker/archlinux.bak index 63a9be63c54..6d23160c2f2 100644 --- a/docker/archlinux.dockerfile +++ b/docker/archlinux.bak @@ -17,21 +17,11 @@ pacman -Syu --disable-download-timeout --noconfirm pacman -Scc --noconfirm _DEPS -FROM sunshine-base AS sunshine-build - -ARG BRANCH -ARG BUILD_VERSION -ARG COMMIT -ARG CLONE_URL -# note: BUILD_VERSION may be blank - -ENV BRANCH=${BRANCH} -ENV BUILD_VERSION=${BUILD_VERSION} -ENV COMMIT=${COMMIT} -ENV CLONE_URL=${CLONE_URL} +FROM sunshine-base AS sunshine-deps SHELL ["/bin/bash", "-o", "pipefail", "-c"] +# Install dependencies first - this layer will be cached RUN <<_SETUP #!/bin/bash set -e @@ -55,6 +45,26 @@ pacman -Syu --disable-download-timeout --needed --noconfirm \ pacman -Scc --noconfirm _SETUP +FROM sunshine-deps AS sunshine-build + +ARG BRANCH +ARG BUILD_VERSION +ARG COMMIT +ARG CLONE_URL +# note: BUILD_VERSION may be blank + +ENV BRANCH=${BRANCH} +ENV BUILD_VERSION=${BUILD_VERSION} +ENV COMMIT=${COMMIT} +ENV CLONE_URL=${CLONE_URL} + +# PKGBUILD options +ENV _use_cuda=true +ENV _run_unit_tests=true +ENV _support_headless_testing=true + +SHELL ["/bin/bash", "-o", "pipefail", "-c"] + # Setup builder user USER builder diff --git a/docker/clion-toolchain.dockerfile b/docker/clion-toolchain.dockerfile index f5080d1899d..2346419073e 100644 --- a/docker/clion-toolchain.dockerfile +++ b/docker/clion-toolchain.dockerfile @@ -3,8 +3,8 @@ # platforms: linux/amd64 # platforms_pr: linux/amd64 # no-cache-filters: toolchain-base,toolchain -ARG BASE=ubuntu -ARG TAG=22.04 +ARG BASE=debian +ARG TAG=trixie-slim FROM ${BASE}:${TAG} AS toolchain-base ENV DEBIAN_FRONTEND=noninteractive @@ -25,11 +25,11 @@ set -e apt-get update -y apt-get install -y --no-install-recommends \ build-essential \ - cmake=3.22.* \ + cmake=3.31.* \ ca-certificates \ doxygen \ - gcc=4:11.2.* \ - g++=4:11.2.* \ + gcc=4:14.2.* \ + g++=4:14.2.* \ gdb \ git \ graphviz \ @@ -38,6 +38,7 @@ apt-get install -y --no-install-recommends \ libcurl4-openssl-dev \ libdrm-dev \ libevdev-dev \ + libgbm-dev \ libminiupnpc-dev \ libnotify-dev \ libnuma-dev \ @@ -53,26 +54,20 @@ apt-get install -y --no-install-recommends \ libxfixes-dev \ libxrandr-dev \ libxtst-dev \ + npm \ udev \ wget \ x11-xserver-utils \ xvfb apt-get clean rm -rf /var/lib/apt/lists/* - -# Install Node -wget --max-redirect=0 -qO- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash -source "$HOME/.nvm/nvm.sh" -nvm install node -nvm use node -nvm alias default node _DEPS # install cuda WORKDIR /build/cuda # versions: https://developer.nvidia.com/cuda-toolkit-archive -ENV CUDA_VERSION="11.8.0" -ENV CUDA_BUILD="520.61.05" +ENV CUDA_VERSION="12.9.1" +ENV CUDA_BUILD="575.57.08" RUN <<_INSTALL_CUDA #!/bin/bash set -e @@ -83,18 +78,19 @@ if [[ "${TARGETPLATFORM}" == 'linux/arm64' ]]; then fi url="${cuda_prefix}${CUDA_VERSION}/local_installers/cuda_${CUDA_VERSION}_${CUDA_BUILD}_linux${cuda_suffix}.run" echo "cuda url: ${url}" -wget "$url" --progress=bar:force:noscroll -q --show-progress -O ./cuda.run -chmod a+x ./cuda.run -./cuda.run --silent --toolkit --toolkitpath=/usr/local --no-opengl-libs --no-man-page --no-drm -rm ./cuda.run +tmpfile="/tmp/cuda.run" +wget "$url" --progress=bar:force:noscroll --show-progress -O "$tmpfile" +chmod a+x "${tmpfile}" +"${tmpfile}" --silent --toolkit --toolkitpath=/usr/local --no-opengl-libs --no-man-page --no-drm +rm -f "${tmpfile}" _INSTALL_CUDA -WORKDIR / -# Write a shell script that starts Xvfb and then runs a shell +WORKDIR /toolchain +# Create a shell script that starts Xvfb and then runs a shell RUN <<_ENTRYPOINT #!/bin/bash set -e -cat < /entrypoint.sh +cat < entrypoint.sh #!/bin/bash Xvfb ${DISPLAY} -screen 0 1024x768x24 & if [ "\$#" -eq 0 ]; then @@ -105,11 +101,11 @@ fi EOF # Make the script executable -chmod +x /entrypoint.sh +chmod +x entrypoint.sh # Note about CLion echo "ATTENTION: CLion will override the entrypoint, you can disable this in the toolchain settings" _ENTRYPOINT # Use the shell script as the entrypoint -ENTRYPOINT ["/entrypoint.sh"] +ENTRYPOINT ["/toolchain/entrypoint.sh"] diff --git a/docker/debian-bookworm.dockerfile b/docker/debian-trixie.dockerfile similarity index 74% rename from docker/debian-bookworm.dockerfile rename to docker/debian-trixie.dockerfile index ffa68b7c507..78d8637e9bb 100644 --- a/docker/debian-bookworm.dockerfile +++ b/docker/debian-trixie.dockerfile @@ -4,12 +4,34 @@ # platforms_pr: linux/amd64 # no-cache-filters: sunshine-base,artifacts,sunshine ARG BASE=debian -ARG TAG=bookworm +ARG TAG=trixie FROM ${BASE}:${TAG} AS sunshine-base ENV DEBIAN_FRONTEND=noninteractive -FROM sunshine-base AS sunshine-build +FROM sunshine-base AS sunshine-deps + +SHELL ["/bin/bash", "-o", "pipefail", "-c"] + +# Copy only the build script and necessary files first for better layer caching +WORKDIR /build/sunshine/ +COPY --link scripts/linux_build.sh ./scripts/linux_build.sh +COPY --link packaging/linux/patches/ ./packaging/linux/patches/ + +# Install dependencies first - this layer will be cached +RUN <<_DEPS +#!/bin/bash +set -e +chmod +x ./scripts/linux_build.sh +./scripts/linux_build.sh \ + --step=deps \ + --cuda-patches \ + --sudo-off +apt-get clean +rm -rf /var/lib/apt/lists/* +_DEPS + +FROM sunshine-deps AS sunshine-build ARG BRANCH ARG BUILD_VERSION @@ -20,24 +42,31 @@ ENV BRANCH=${BRANCH} ENV BUILD_VERSION=${BUILD_VERSION} ENV COMMIT=${COMMIT} -SHELL ["/bin/bash", "-o", "pipefail", "-c"] - -# copy repository -WORKDIR /build/sunshine/ +# Now copy the full repository COPY --link .. . -# cmake and cpack +# Configure, validate, build and package RUN <<_BUILD #!/bin/bash set -e -chmod +x ./scripts/linux_build.sh ./scripts/linux_build.sh \ + --step=cmake \ --publisher-name='LizardByte' \ --publisher-website='https://app.lizardbyte.dev' \ --publisher-issue-url='https://app.lizardbyte.dev/support' \ --sudo-off -apt-get clean -rm -rf /var/lib/apt/lists/* + +./scripts/linux_build.sh \ + --step=validation \ + --sudo-off + +./scripts/linux_build.sh \ + --step=build \ + --sudo-off + +./scripts/linux_build.sh \ + --step=package \ + --sudo-off _BUILD # run tests diff --git a/docker/ubuntu-22.04.dockerfile b/docker/ubuntu-22.04.dockerfile index 06032175f85..1693367c329 100644 --- a/docker/ubuntu-22.04.dockerfile +++ b/docker/ubuntu-22.04.dockerfile @@ -9,7 +9,28 @@ FROM ${BASE}:${TAG} AS sunshine-base ENV DEBIAN_FRONTEND=noninteractive -FROM sunshine-base AS sunshine-build +FROM sunshine-base AS sunshine-deps + +SHELL ["/bin/bash", "-o", "pipefail", "-c"] + +# Copy only the build script first for better layer caching +WORKDIR /build/sunshine/ +COPY --link scripts/linux_build.sh ./scripts/linux_build.sh + +# Install dependencies first - this layer will be cached +RUN <<_DEPS +#!/bin/bash +set -e +chmod +x ./scripts/linux_build.sh +./scripts/linux_build.sh \ + --step=deps \ + --ubuntu-test-repo \ + --sudo-off +apt-get clean +rm -rf /var/lib/apt/lists/* +_DEPS + +FROM sunshine-deps AS sunshine-build ARG BRANCH ARG BUILD_VERSION @@ -20,24 +41,31 @@ ENV BRANCH=${BRANCH} ENV BUILD_VERSION=${BUILD_VERSION} ENV COMMIT=${COMMIT} -SHELL ["/bin/bash", "-o", "pipefail", "-c"] - -# copy repository -WORKDIR /build/sunshine/ +# Now copy the full repository COPY --link .. . -# cmake and cpack +# Configure, validate, build and package RUN <<_BUILD #!/bin/bash set -e -chmod +x ./scripts/linux_build.sh ./scripts/linux_build.sh \ + --step=cmake \ --publisher-name='LizardByte' \ --publisher-website='https://app.lizardbyte.dev' \ --publisher-issue-url='https://app.lizardbyte.dev/support' \ --sudo-off -apt-get clean -rm -rf /var/lib/apt/lists/* + +./scripts/linux_build.sh \ + --step=validation \ + --sudo-off + +./scripts/linux_build.sh \ + --step=build \ + --sudo-off + +./scripts/linux_build.sh \ + --step=package \ + --sudo-off _BUILD # run tests diff --git a/docker/ubuntu-24.04.dockerfile b/docker/ubuntu-24.04.dockerfile index e11f18b2f95..6c0f82fcbb1 100644 --- a/docker/ubuntu-24.04.dockerfile +++ b/docker/ubuntu-24.04.dockerfile @@ -9,7 +9,27 @@ FROM ${BASE}:${TAG} AS sunshine-base ENV DEBIAN_FRONTEND=noninteractive -FROM sunshine-base AS sunshine-build +FROM sunshine-base AS sunshine-deps + +SHELL ["/bin/bash", "-o", "pipefail", "-c"] + +# Copy only the build script first for better layer caching +WORKDIR /build/sunshine/ +COPY --link scripts/linux_build.sh ./scripts/linux_build.sh + +# Install dependencies first - this layer will be cached +RUN <<_DEPS +#!/bin/bash +set -e +chmod +x ./scripts/linux_build.sh +./scripts/linux_build.sh \ + --step=deps \ + --sudo-off +apt-get clean +rm -rf /var/lib/apt/lists/* +_DEPS + +FROM sunshine-deps AS sunshine-build ARG BRANCH ARG BUILD_VERSION @@ -20,24 +40,31 @@ ENV BRANCH=${BRANCH} ENV BUILD_VERSION=${BUILD_VERSION} ENV COMMIT=${COMMIT} -SHELL ["/bin/bash", "-o", "pipefail", "-c"] - -# copy repository -WORKDIR /build/sunshine/ +# Now copy the full repository COPY --link .. . -# cmake and cpack +# Configure, validate, build and package RUN <<_BUILD #!/bin/bash set -e -chmod +x ./scripts/linux_build.sh ./scripts/linux_build.sh \ + --step=cmake \ --publisher-name='LizardByte' \ --publisher-website='https://app.lizardbyte.dev' \ --publisher-issue-url='https://app.lizardbyte.dev/support' \ --sudo-off -apt-get clean -rm -rf /var/lib/apt/lists/* + +./scripts/linux_build.sh \ + --step=validation \ + --sudo-off + +./scripts/linux_build.sh \ + --step=build \ + --sudo-off + +./scripts/linux_build.sh \ + --step=package \ + --sudo-off _BUILD # run tests diff --git a/docs/app_examples.md b/docs/app_examples.md index 782681fdc56..e6e251b68d6 100644 --- a/docs/app_examples.md +++ b/docs/app_examples.md @@ -2,10 +2,12 @@ Since not all applications behave the same, we decided to create some examples to help you get started adding games and applications to Sunshine. -@attention{Throughout these examples, any fields not shown are left blank. You can enhance your experience by -adding an image or a log file (via the `Output` field).} +> [!TIP] +> 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.} +> [!WARNING] +> When a working directory is not specified, it defaults to the folder where the target application resides. ## Common Examples @@ -18,10 +20,20 @@ adding an image or a log file (via the `Output` field).} | Image | @code{}desktop.png@endcode | ### 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.} + +> [!NOTE] +> Steam is launched as a detached command because Steam starts with a process that self updates itself and the original +> process is killed. @tabs{ + @tab{FreeBSD | + \| Field \| Value \| + \|------------------------------\|------------------------------------------------------\| + \| Application Name \| @code{}Steam Big Picture@endcode \| + \| Command Preporations -> Undo \| @code{}setsid steam steam://close/bigpicture@endcode \| + \| Detached Commands \| @code{}setsid steam steam://open/bigpicture@endcode \| + \| Image \| @code{}steam.png@endcode \| + } @tab{Linux | \| Field \| Value \| \|------------------------------\|------------------------------------------------------\| @@ -49,7 +61,9 @@ process is killed.} } ### Epic Game Store game -@note{Using URI method will be the most consistent between various games.} + +> [!NOTE] +> Using the URI method will be the most consistent between various games. #### URI @@ -84,11 +98,19 @@ process is killed.} } ### Steam game -@note{Using URI method will be the most consistent between various games.} + +> [!NOTE] +> Using the URI method will be the most consistent between various games. #### URI @tabs{ + @tab{FreeBSD | + \| Field \| Value \| + \|-------------------\|------------------------------------------------------\| + \| Application Name \| @code{}Surviving Mars@endcode \| + \| Detached Commands \| @code{}setsid steam steam://rungameid/464920@endcode \| + } @tab{Linux | \| Field \| Value \| \|-------------------\|------------------------------------------------------\| @@ -111,6 +133,13 @@ process is killed.} #### Binary (w/ working directory @tabs{ + @tab{FreeBSD | + \| Field \| Value \| + \|-------------------\|--------------------------------------------------------------\| + \| Application Name \| @code{}Surviving Mars@endcode \| + \| Command \| @code{}MarsSteam@endcode \| + \| Working Directory \| @code{}~/.steam/steam/SteamApps/common/Survivng Mars@endcode \| + } @tab{Linux | \| Field \| Value \| \|-------------------\|--------------------------------------------------------------\| @@ -136,6 +165,12 @@ process is killed.} #### Binary (w/o working directory) @tabs{ + @tab{FreeBSD | + \| Field \| Value \| + \|-------------------\|------------------------------------------------------------------------\| + \| Application Name \| @code{}Surviving Mars@endcode \| + \| Command \| @code{}~/.steam/steam/SteamApps/common/Survivng Mars/MarsSteam@endcode \| + } @tab{Linux | \| Field \| Value \| \|-------------------\|------------------------------------------------------------------------\| @@ -169,49 +204,49 @@ process is killed.} | Do | @code{}sh -c "xrandr --output HDMI-1 --mode ${SUNSHINE_CLIENT_WIDTH}x${SUNSHINE_CLIENT_HEIGHT} --rate ${SUNSHINE_CLIENT_FPS}"@endcode | | Undo | @code{}xrandr --output HDMI-1 --mode 3840x2160 --rate 120@endcode | -@hint{The above only works if the xrandr mode already exists. You will need to create new modes to stream to macOS -and iOS devices, since they use non-standard resolutions. - -You can update the ``Do`` command to this: -```bash -bash -c "${HOME}/scripts/set-custom-res.sh \"${SUNSHINE_CLIENT_WIDTH}\" \"${SUNSHINE_CLIENT_HEIGHT}\" \"${SUNSHINE_CLIENT_FPS}\"" -``` - -The `set-custom-res.sh` will have this content: -```bash -#!/bin/bash -set -e - -# Get params and set any defaults -width=${1:-1920} -height=${2:-1080} -refresh_rate=${3:-60} - -# You may need to adjust the scaling differently so the UI/text isn't too small / big -scale=${4:-0.55} - -# Get the name of the active display -display_output=$(xrandr | grep " connected" | awk '{ print $1 }') - -# Get the modeline info from the 2nd row in the cvt output -modeline=$(cvt ${width} ${height} ${refresh_rate} | awk 'FNR == 2') -xrandr_mode_str=${modeline//Modeline \"*\" /} -mode_alias="${width}x${height}" - -echo "xrandr setting new mode ${mode_alias} ${xrandr_mode_str}" -xrandr --newmode ${mode_alias} ${xrandr_mode_str} -xrandr --addmode ${display_output} ${mode_alias} - -# Reset scaling -xrandr --output ${display_output} --scale 1 - -# Apply new xrandr mode -xrandr --output ${display_output} --primary --mode ${mode_alias} --pos 0x0 --rotate normal --scale ${scale} - -# Optional reset your wallpaper to fit to new resolution -# xwallpaper --zoom /path/to/wallpaper.png -``` -} +> [!TIP] +> The above only works if the xrandr mode already exists. You will need to create new modes to stream to macOS +> and iOS devices, since they use non-standard resolutions. +> +> You can update the ``Do`` command to this: +> ```bash +> bash -c "${HOME}/scripts/set-custom-res.sh \"${SUNSHINE_CLIENT_WIDTH}\" \"${SUNSHINE_CLIENT_HEIGHT}\" \"${SUNSHINE_CLIENT_FPS}\"" +> ``` +> +> The `set-custom-res.sh` will have this content: +> ```bash +> #!/bin/bash +> set -e +> +> # Get params and set any defaults +> width=${1:-1920} +> height=${2:-1080} +> refresh_rate=${3:-60} +> +> # You may need to adjust the scaling differently so the UI/text isn't too small / big +> scale=${4:-0.55} +> +> # Get the name of the active display +> display_output=$(xrandr | grep " connected" | awk '{ print $1 }') +> +> # Get the modeline info from the 2nd row in the cvt output +> modeline=$(cvt ${width} ${height} ${refresh_rate} | awk 'FNR == 2') +> xrandr_mode_str=${modeline//Modeline \"*\" /} +> mode_alias="${width}x${height}" +> +> echo "xrandr setting new mode ${mode_alias} ${xrandr_mode_str}" +> xrandr --newmode ${mode_alias} ${xrandr_mode_str} +> xrandr --addmode ${display_output} ${mode_alias} +> +> # Reset scaling +> xrandr --output ${display_output} --scale 1 +> +> # Apply new xrandr mode +> xrandr --output ${display_output} --primary --mode ${mode_alias} --pos 0x0 --rotate normal --scale ${scale} +> +> # Optional reset your wallpaper to fit to new resolution +> # xwallpaper --zoom /path/to/wallpaper.png +> ``` ###### Wayland (wlroots, e.g. hyprland) @@ -220,7 +255,8 @@ xrandr --output ${display_output} --primary --mode ${mode_alias} --pos 0x0 --rot | Do | @code{}sh -c "wlr-xrandr --output HDMI-1 --mode \"${SUNSHINE_CLIENT_WIDTH}x${SUNSHINE_CLIENT_HEIGHT}@${SUNSHINE_CLIENT_FPS}Hz\""@endcode | | Undo | @code{}wlr-xrandr --output HDMI-1 --mode 3840x2160@120Hz@endcode | -@hint{`wlr-xrandr` only works with wlroots-based compositors.} +> [!TIP] +> `wlr-xrandr` only works with wlroots-based compositors. ###### Gnome (X11) @@ -240,12 +276,12 @@ Installation instructions for displayconfig-mutter can be [found here](https://g [gnome-randr-rust](https://github.com/maxwellainatchi/gnome-randr-rust) and [gnome-randr.py](https://gitlab.com/Oschowa/gnome-randr), but both of those are unmaintained and do not support newer Mutter features such as HDR and VRR. -@hint{HDR support has been added to Gnome 48, to check if your display supports it you can run this: -``` -displayconfig-mutter list -``` -If it doesn't, then remove ``--hdr`` flag from both ``Do`` and ``Undo`` steps. -} +> [!TIP] +> HDR support has been added to Gnome 48, to check if your display supports it, you can run this: +> ``` +> displayconfig-mutter list +> ``` +> If it doesn't, then remove ``--hdr`` flag from both ``Do`` and ``Undo`` steps. ###### KDE Plasma (Wayland, X11) @@ -254,22 +290,22 @@ If it doesn't, then remove ``--hdr`` flag from both ``Do`` and ``Undo`` steps. | Do | @code{}sh -c "kscreen-doctor output.HDMI-A-1.mode.${SUNSHINE_CLIENT_WIDTH}x${SUNSHINE_CLIENT_HEIGHT}@${SUNSHINE_CLIENT_FPS}"@endcode | | Undo | @code{}kscreen-doctor output.HDMI-A-1.mode.3840x2160@120@endcode | -@attention{The names of your displays will differ between X11 and Wayland. -Be sure to use the correct name, depending on your session manager. -e.g. On X11, the monitor may be called ``HDMI-A-0``, but on Wayland, it may be called ``HDMI-A-1``. -} - -@hint{Replace ``HDMI-A-1`` with the display name of the monitor you would like to use for Moonlight. -You can list the monitors available to you with: -``` -kscreen-doctor -o -``` - -These will also give you the supported display properties for each monitor. You can select them either by -hard-coding their corresponding number (e.g. ``kscreen-doctor output.HDMI-A1.mode.0``) or using the above -``do`` command to fetch the resolution requested by your Moonlight client -(which has a chance of not being supported by your monitor). -} +> [!CAUTION] +> The names of your displays will differ between X11 and Wayland. +> Be sure to use the correct name, depending on your session manager. +> e.g., On X11, the monitor may be called ``HDMI-A-0``, but on Wayland, it may be called ``HDMI-A-1``. + +> [!TIP] +> Replace ``HDMI-A-1`` with the display name of the monitor you would like to use for Moonlight. +> You can list the monitors available to you with: +> ``` +> kscreen-doctor -o +> ``` +> +> These will also give you the supported display properties for each monitor. You can select them either by +> hard-coding their corresponding number (e.g. ``kscreen-doctor output.HDMI-A1.mode.0``) or using the above +> ``do`` command to fetch the resolution requested by your Moonlight client +> (which has a chance of not being supported by your monitor). ###### NVIDIA @@ -281,9 +317,11 @@ hard-coding their corresponding number (e.g. ``kscreen-doctor output.HDMI-A1.mod ##### macOS ###### displayplacer -@note{This example uses the `displayplacer` tool to change the resolution. -This tool can be installed following instructions in their -[GitHub repository](https://github.com/jakehilborn/displayplacer)}. + +> [!NOTE] +> This example uses the `displayplacer` tool to change the resolution. +> This tool can be installed following instructions in their +> [GitHub repository](https://github.com/jakehilborn/displayplacer). | Prep Step | Command | |-----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------| @@ -295,8 +333,10 @@ Sunshine has built-in support for changing the resolution and refresh rate on Wi third-party tool, you can use *QRes* as an example. ###### QRes -@note{This example uses the *QRes* tool to change the resolution and refresh rate. -This tool can be downloaded from their [SourceForge repository](https://sourceforge.net/projects/qres).} + +> [!NOTE] +> This example uses the *QRes* tool to change the resolution and refresh rate. +> This tool can be downloaded from their [SourceForge repository](https://sourceforge.net/projects/qres). | Prep Step | Command | |-----------|---------------------------------------------------------------------------------------------------------------------------| @@ -306,8 +346,10 @@ This tool can be downloaded from their [SourceForge repository](https://sourcefo ### Additional Considerations #### Linux (Flatpak) -@attention{Because Flatpak packages run in a sandboxed environment and do not normally have access to the -host, the Flatpak of Sunshine requires commands to be prefixed with `flatpak-spawn --host`.} + +> [!CAUTION] +> Because Flatpak packages run in a sandboxed environment and do not normally have access to the +> host, the Flatpak of Sunshine requires commands to be prefixed with `flatpak-spawn --host`. #### Windows **Elevating Commands (Windows)** diff --git a/docs/building.md b/docs/building.md index b7f5d402601..56febbce8ed 100644 --- a/docs/building.md +++ b/docs/building.md @@ -3,8 +3,50 @@ Sunshine binaries are built using [CMake](https://cmake.org) and requires `cmake ## Building Locally +### Compiler +It is recommended to use one of the following compilers: + +| Compiler | Version | +|:------------|:--------| +| GCC | 13+ | +| Clang | 17+ | +| Apple Clang | 15+ | + ### Dependencies +#### FreeBSD +> [!CAUTION] +> Sunshine support for FreeBSD is experimental and may be incomplete or not work as expected + +##### Install dependencies +```sh +pkg install -y \ + audio/opus \ + audio/pulseaudio \ + devel/cmake \ + devel/evdev-proto \ + devel/git \ + devel/libayatana-appindicator \ + devel/libevdev \ + devel/libnotify \ + devel/ninja \ + devel/pkgconf \ + ftp/curl \ + graphics/libdrm \ + graphics/wayland \ + multimedia/libva \ + net/miniupnpc \ + ports-mgmt/pkg \ + security/openssl \ + shells/bash \ + www/npm \ + x11/libX11 \ + x11/libxcb \ + x11/libXfixes \ + x11/libXrandr \ + x11/libXtst +``` + #### Linux Dependencies vary depending on the distribution. You can reference our [linux_build.sh](https://github.com/LizardByte/Sunshine/blob/master/scripts/linux_build.sh) script for a list of @@ -16,11 +58,12 @@ Sunshine requires CUDA Toolkit for NVFBC capture. There are two caveats to CUDA: 1. The version installed depends on the version of GCC. 2. 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. + At the time of writing, the recommended version to use is CUDA ~12.9. See [CUDA compatibility](https://docs.nvidia.com/deploy/cuda-compatibility/index.html) for more info. -@tip{To install older versions, select the appropriate run file based on your desired CUDA version and architecture -according to [CUDA Toolkit Archive](https://developer.nvidia.com/cuda-toolkit-archive)} +> [!NOTE] +> To install older versions, select the appropriate run file based on your desired CUDA version and architecture +> according to [CUDA Toolkit Archive](https://developer.nvidia.com/cuda-toolkit-archive) #### macOS You can either use [Homebrew](https://brew.sh) or [MacPorts](https://www.macports.org) to install dependencies. @@ -118,12 +161,18 @@ cmake -B build -G Ninja -S . ninja -C build ``` -@tip{Available build options can be found in -[options.cmake](https://github.com/LizardByte/Sunshine/blob/master/cmake/prep/options.cmake).} +> [!TIP] +> Available build options can be found in +> [options.cmake](https://github.com/LizardByte/Sunshine/blob/master/cmake/prep/options.cmake). ### Package @tabs{ + @tab{FreeBSD | @tabs{ + @tab{pkg | ```bash + cpack -G FREEBSD --config ./build/CPackConfig.cmake + ```} + }} @tab{Linux | @tabs{ @tab{deb | ```bash cpack -G DEB --config ./build/CPackConfig.cmake diff --git a/docs/configuration.md b/docs/configuration.md index a1ae900c368..d060c4e04ad 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -26,6 +26,7 @@ location by modifying the configuration file. | OS | Location | |---------|-------------------------------------------------| | Docker | @code{}/config@endcode | +| FreeBSD | @code{}~/.config/sunshine@endcode | | Linux | @code{}~/.config/sunshine@endcode | | macOS | @code{}~/.config/sunshine@endcode | | Windows | @code{}%ProgramFiles%\\Sunshine\\config@endcode | @@ -261,6 +262,29 @@ editing the `conf` file in a text editor. Use the examples as reference.
Minimum Requirements
Intel:
-   Linux: VAAPI-compatible, see: VAAPI hardware support
+   FreeBSD/Linux: VAAPI-compatible, see: VAAPI hardware support
  Windows: Skylake or newer with QuickSync encoding support
4GB or more
OSWindows: 10+ (Windows Server does not support virtual gamepads)OSFreeBSD: 14.3+
macOS: 13+Linux/Debian: 13+ (trixie)
Linux/Debian: 12+ (bookworm)Linux/Fedora: 41+
Linux/Fedora: 40+Linux/Ubuntu: 22.04+ (jammy)
Linux/Ubuntu: 22.04+ (jammy)macOS: 14+
Windows: 11+ (Windows Server does not support virtual gamepads)
Network
Intel:
-   Linux: HD Graphics 510 or higher
+   FreeBSD/Linux: HD Graphics 510 or higher
  Windows: Skylake or newer with QuickSync encoding support
Nvidia: GeForce GTX 1080 or higher + Nvidia:
+   FreeBSD/Linux: GeForce RTX 2000 series or higher
+   Windows: Geforce GTX 1080 or higher +
CPU
+### system_tray + + + + + + + + + + + + + + +
Description + Show icon in system tray and display desktop notifications +
Default@code{} + enabled + @endcode
Example@code{} + system_tray = enabled + @endcode
+ ## Input ### controller @@ -316,12 +340,12 @@ editing the `conf` file in a text editor. Use the examples as reference. ds5 DualShock 5 controller (PS5) - @note{This option applies to Linux only.} + @note{This option applies to FreeBSD and Linux only.} switch Switch Pro controller - @note{This option applies to Linux only.} + @note{This option applies to FreeBSD and Linux only.} x360 @@ -331,7 +355,7 @@ editing the `conf` file in a text editor. Use the examples as reference. xone Xbox One controller - @note{This option applies to Linux only.} + @note{This option applies to FreeBSD and Linux only.} @@ -416,6 +440,30 @@ editing the `conf` file in a text editor. Use the examples as reference. +### ds5_inputtino_randomize_mac + + + + + + + + + + + + + + +
Description + Randomize the MAC-Address for the generated virtual controller. + @hint{Only applies on linux for gamepads created as PS5-style controllers} +
Default@code{} + enabled + @endcode
Example@code{} + ds5_inputtino_randomize_mac = enabled + @endcode
+ ### back_button_timeout @@ -688,14 +736,14 @@ editing the `conf` file in a text editor. Use the examples as reference. @tip{To find the name of the audio sink follow these instructions.

- **Linux + pulseaudio:** + **FreeBSD/Linux + pulseaudio:**
@code{} pacmd list-sinks | grep "name:" @endcode

- **Linux + pipewire:** + **FreeBSD/Linux + pipewire:**
@code{} pactl info | grep Source @@ -729,7 +777,7 @@ editing the `conf` file in a text editor. Use the examples as reference. - + @@ -836,7 +884,7 @@ editing the `conf` file in a text editor. Use the examples as reference. @tip{To find the appropriate values follow these instructions.

- **Linux + VA-API:** + **FreeBSD/Linux + VA-API:**
Unlike with *amdvce* and *nvenc*, it doesn't matter if video encoding is done on a different GPU. @code{} @@ -866,7 +914,7 @@ editing the `conf` file in a text editor. Use the examples as reference. - + @@ -889,7 +937,7 @@ editing the `conf` file in a text editor. Use the examples as reference. @tip{To find the appropriate values follow these instructions.

- **Linux:** + **FreeBSD/Linux:**
During Sunshine startup, you should see the list of detected displays: @code{} @@ -974,7 +1022,7 @@ editing the `conf` file in a text editor. Use the examples as reference. - + @@ -1987,7 +2035,7 @@ editing the `conf` file in a text editor. Use the examples as reference. + @note{Applies to FreeBSD and Linux only.} @@ -2036,7 +2084,7 @@ editing the `conf` file in a text editor. Use the examples as reference. - + diff --git a/docs/contributing.md b/docs/contributing.md index a508f0d49a9..e7c19d5d1f7 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -73,13 +73,15 @@ The following is a simple example of how to use it. } ``` - @note{The json keys should be sorted alphabetically. You can use [jsonabc](https://novicelab.org/jsonabc) - to sort the keys.} + > [!NOTE] + > The JSON keys should be sorted alphabetically. You can use [jsonabc](https://novicelab.org/jsonabc) + > to sort the keys. - @attention{Due to the integration with Crowdin, it is important to only add strings to the *en.json* file, - and to not modify any other language files. After the PR is merged, the translations can take place - on [CrowdIn][crowdin-url]. Once the translations are complete, a PR will be made - to merge the translations into Sunshine.} + > [!IMPORTANT] + > 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][crowdin-url]. Once the translations are complete, a PR will be made + > to merge the translations into Sunshine. * Use the string in the Vue component. ```html @@ -90,8 +92,9 @@ The following is a simple example of how to use it. ``` - @tip{More formatting examples can be found in the - [Vue I18n guide](https://kazupon.github.io/vue-i18n/guide/formatting.html).} + > [!TIP] + > More formatting examples can be found in the + > [Vue I18n guide](https://kazupon.github.io/vue-i18n/guide/formatting.html). ##### C++ @@ -106,11 +109,13 @@ some situations. For example the system tray icon could be localized as it is us std::string msg = boost::locale::translate("Hello world!"); ``` -@tip{More examples can be found in the documentation for -[boost locale](https://www.boost.org/doc/libs/1_70_0/libs/locale/doc/html/messages_formatting.html).} +> [!TIP] +> More examples can be found in the documentation for +> [boost locale](https://www.boost.org/doc/libs/1_70_0/libs/locale/doc/html/messages_formatting.html). -@warning{The below is for information only. Contributors should never include manually updated template files, or -manually compiled language files in Pull Requests.} +> [!WARNING] +> The below 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 @@ -135,10 +140,11 @@ required for this, along with the python dependencies in the `./scripts/requirem 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][crowdin-url]. Once the translations are -complete, a PR will be made to merge the translations into Sunshine.} +> [!IMPORTANT] +> 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][crowdin-url]. Once the translations are +> complete, a PR will be made to merge the translations into Sunshine. ### Testing @@ -175,8 +181,8 @@ To see all available options, run the tests with the `--help` flag. ./build/tests/test_sunshine --help ``` -@tip{See the googletest [FAQ](https://google.github.io/googletest/faq.html) for more information on how to use -Google Test.} +> [!TIP] +> See the googletest [FAQ](https://google.github.io/googletest/faq.html) for more information on how to use Google Test. We use [gcovr](https://www.gcovr.com) to generate code coverage reports, and [Codecov](https://about.codecov.io) to analyze the reports for all PRs and commits. diff --git a/docs/gamestream_migration.md b/docs/gamestream_migration.md index 8a8699b7525..5018ba585a9 100644 --- a/docs/gamestream_migration.md +++ b/docs/gamestream_migration.md @@ -13,14 +13,15 @@ to a specified directory. If you are using the Moonlight Internet Hosting Tool, you can remove it from your system when you migrate to Sunshine. To stream over the Internet with Sunshine and a UPnP-capable router, enable the UPnP option in the Sunshine Web UI. -@note{Running Sunshine together with versions of the Moonlight Internet Hosting Tool prior to v5.6 will cause UPnP -port forwarding to become unreliable. Either uninstall the tool entirely or update it to v5.6 or later.} +> [!NOTE] +> Running Sunshine together with versions of the Moonlight Internet Hosting Tool prior to v5.6 will cause UPnP +> port forwarding to become unreliable. Either uninstall the tool entirely or update it to v5.6 or later. ## Limitations Sunshine does have some limitations, as compared to Nvidia GameStream. * Automatic game/application list. -* Changing game settings automatically, to optimize streaming. +* Changing game settings automatically to optimize streaming.
diff --git a/docs/getting_started.md b/docs/getting_started.md index f30362390c5..c0d40752c65 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -8,31 +8,56 @@ and release artifacts may be missing when merging changes on a faster cadence. ## Binaries -Binaries of Sunshine are created for each release. They are available for Linux, macOS, and Windows. +Binaries of Sunshine are created for each release. They are available for FreeBSD, Linux, macOS, and Windows. Binaries can be found in the [latest release][latest-release]. -@tip{Some third party packages also exist. -See [Third Party Packages](third_party_packages.md) for more information. -No support will be provided for third party packages!} +> [!NOTE] +> Some third party packages also exist. +> See [Third Party Packages](third_party_packages.md) for more information. +> No support will be provided for third party packages! ## Install ### Docker -@warning{The Docker images are not recommended for most users.} + +> [!WARNING] +> The Docker images are not recommended for most users. Docker images are available on [Dockerhub.io](https://hub.docker.com/repository/docker/lizardbyte/sunshine) and [ghcr.io](https://github.com/orgs/LizardByte/packages?repo_name=sunshine). See [Docker](../DOCKER_README.md) for more information. +### FreeBSD + +#### Install +1. Download the appropriate package for your architecture + + | Architecture | Package | + |---------------|----------------------------------------------------------------------------------------------------------------------------------------| + | amd64/x86_64 | [Sunshine-FreeBSD-14.3-amd64.pkg](https://github.com/LizardByte/Sunshine/releases/latest/download/Sunshine-FreeBSD-14.3-amd64.pkg) | + | arm64/aarch64 | [Sunshine-FreeBSD-14.3-aarch64.pkg](https://github.com/LizardByte/Sunshine/releases/latest/download/Sunshine-FreeBSD-14.3-aarch64.pkg) | + +2. Open terminal and run the following command. + ```sh + sudo pkg install ./Sunshine-FreeBSD-14.3-{arch}.pkg + ``` + +#### Uninstall +```sh +sudo pkg delete Sunshine +``` + ### Linux + **CUDA Compatibility** CUDA is used for NVFBC capture. -@tip{See [CUDA GPUS](https://developer.nvidia.com/cuda-gpus) to cross-reference Compute Capability to your GPU. -The table below applies to packages provided by LizardByte. If you use an official LizardByte package then you do not -need to install CUDA.} +> [!NOTE] +> See [CUDA GPUS](https://developer.nvidia.com/cuda-gpus) to cross-reference Compute Capability to your GPU. +> The table below applies to packages provided by LizardByte. If you use an official LizardByte package, then you do not +> need to install CUDA.
Sunshine will select the default audio device.
Example (Linux)Example (FreeBSD/Linux) @code{} audio_sink = alsa_output.pci-0000_09_00.3.analog-stereo @endcode Sunshine will select the default video card.
Example (Linux)Example (FreeBSD/Linux) @code{} adapter_name = /dev/dri/renderD128 @endcode Sunshine will select the default display.
Example (Linux)Example (FreeBSD/Linux) @code{} output_name = 0 @endcode
x11 Uses XCB. This is the slowest and most CPU intensive so should be avoided if possible. - @note{Applies to Linux only.}
ddx
vaapiUse Linux VA-API (AMD, Intel)Use VA-API (AMD, Intel)
software
@@ -43,9 +68,9 @@ need to install CUDA.} - - - + + + @@ -55,33 +80,26 @@ need to install CUDA.} - - - - + - - - + - - - + - -
CUDA Compatibility
Package
11.8.0450.80.0235;50;52;60;61;62;70;72;75;80;86;87;89;9012.9.1575.57.0850;52;60;61;62;70;72;75;80;86;87;89;90;100;101;103;120;121 sunshine.AppImage
sunshine-ubuntu-24.04-{arch}.deb
12.0.0525.60.1350;52;60;61;62;70;72;75;80;86;87;89;90sunshine-debian-bookworm-{arch}.debsunshine-debian-trixie-{arch}.deb
12.6.2560.35.03 sunshine_{arch}.flatpak
Sunshine (copr - Fedora 41)Sunshine (copr - Fedora)
12.8.1570.124.06Sunshine (copr - Fedora 42)Sunshine (copr - OpenSUSE)
12.9.1575.57.08 sunshine.pkg.tar.zst
#### AppImage -@caution{Use distro-specific packages instead of the AppImage if they are available.} + +> [!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. @@ -89,13 +107,15 @@ According to AppImageLint the supported distro matrix of the AppImage is below. - ✔ Debian bookworm - ✔ Debian trixie - ✔ Debian sid +- ✔ Ubuntu plucky - ✔ Ubuntu noble - ✔ Ubuntu jammy - ✖ Ubuntu focal - ✖ Ubuntu bionic - ✖ Ubuntu xenial - ✖ Ubuntu trusty -- ✖ CentOS 7 +- ✖ Rocky Linux 8 +- ✖ Rocky Linux 9 ##### Install 1. Download [sunshine.AppImage](https://github.com/LizardByte/Sunshine/releases/latest/download/sunshine.AppImage) @@ -120,7 +140,9 @@ According to AppImageLint the supported distro matrix of the AppImage is below. ``` #### ArchLinux -@warning{We do not provide support for any AUR packages.} + +> [!CAUTION] +> Use AUR packages at your own risk. ##### Install Prebuilt Packages Follow the instructions at LizardByte's [pacman-repo](https://github.com/LizardByte/pacman-repo) to add @@ -149,26 +171,49 @@ pacman -R sunshine ``` #### Debian/Ubuntu + ##### Install Download `sunshine-{distro}-{distro-version}-{arch}.deb` and run the following command. ```bash sudo dpkg -i ./sunshine-{distro}-{distro-version}-{arch}.deb ``` -@note{The `{distro-version}` is the version of the distro we built the package on. The `{arch}` is the -architecture of your operating system.} +> [!NOTE] +> The `{distro-version}` is the version of the distro we built the package on. The `{arch}` is the +> architecture of your operating system. -@tip{You can double-click the deb file to see details about the package and begin installation.} +> [!TIP] +> You can double-click the deb file to see details about the package and begin installation. ##### Uninstall ```bash sudo apt remove sunshine ``` -#### Fedora -@tip{The package name is case-sensitive.} +#### Fedora/OpenSUSE -##### Install +> [!TIP] +> The package name is case-sensitive. + +##### Install (GitHub releases) +Download `Sunshine-{version}.{distro+version}.{arch}.rpm` and run the following command. +```bash +sudo dnf install ./Sunshine-{version}.{distro}.{arch}.rpm +``` + +> [!NOTE] +> The `{distro+version}` is the distro and distro version of the distro we built the package on. The `{arch}` is the +> architecture of your operating system. + +> [!TIP] +> You can double-click the rpm file to see details about the package and begin installation. + +##### Uninstall +```bash +sudo dnf remove sunshine +``` + +##### Install (Copr) 1. Enable copr repository. ```bash sudo dnf copr enable lizardbyte/stable @@ -190,13 +235,17 @@ sudo dnf remove Sunshine ``` #### Flatpak -@caution{Use distro-specific packages instead of the Flatpak if they are available.} + +> [!CAUTION] +> Use distro-specific packages instead of the Flatpak if they are available. Using this package requires that you have [Flatpak](https://flatpak.org/setup) installed. ##### Download (local option) 1. Download `sunshine_{arch}.flatpak` and run the following command. - @note{Replace `{arch}` with your system architecture.} + + > [!NOTE] + > Replace `{arch}` with your system architecture. ##### Install (system level) **Flathub** @@ -242,7 +291,9 @@ flatpak uninstall --delete-data dev.lizardbyte.app.Sunshine ``` #### Homebrew -@important{The Homebrew package is experimental on Linux.} + +> [!IMPORTANT] +> The Homebrew package is experimental on Linux. This package requires that you have [Homebrew](https://docs.brew.sh/Installation) installed. @@ -261,7 +312,8 @@ brew uninstall sunshine ### macOS -@important{Sunshine on macOS is experimental. Gamepads do not work.} +> [!IMPORTANT] +> Sunshine on macOS is experimental. Gamepads do not work. #### Homebrew This package requires that you have [Homebrew](https://docs.brew.sh/Installation) installed. @@ -277,7 +329,8 @@ brew install sunshine brew uninstall sunshine ``` -@tip{For beta you can replace `sunshine` with `sunshine-beta` in the above commands.} +> [!TIP] +> For beta you can replace `sunshine` with `sunshine-beta` in the above commands. ### Windows @@ -286,16 +339,18 @@ brew uninstall sunshine 1. Download and install [Sunshine-Windows-AMD64-installer.exe](https://github.com/LizardByte/Sunshine/releases/latest/download/Sunshine-Windows-AMD64-installer.exe) -@attention{You should carefully select or unselect the options you want to install. Do not blindly install or -enable features.} +> [!CAUTION] +> You should carefully select or unselect the options you want to install. Do not blindly install or +> enable features. To uninstall, find Sunshine in the list here and select "Uninstall" from the overflow menu. Different versions of Windows may provide slightly different steps for uninstall. #### Standalone (lite version) -@warning{By using this package instead of the installer, performance will be reduced. This package is not -recommended for most users. No support will be provided!} +> [!WARNING] +> By using this package instead of the installer, performance will be reduced. This package is not +> recommended for most users. No support will be provided! 1. Download and extract [Sunshine-Windows-AMD64-portable.zip](https://github.com/LizardByte/Sunshine/releases/latest/download/Sunshine-Windows-AMD64-portable.zip) @@ -346,12 +401,32 @@ recommended for most users. No support will be provided!} ## Initial Setup After installation, some initial setup is required. +### FreeBSD + +#### Virtual Input Devices + +> [!IMPORTANT] +> To use virtual input devices (keyboard, mouse, gamepads), you must add your user to the `input` group. + +The installation process creates the `input` group and configures permissions for `/dev/uinput`. +To allow your user to create virtual input devices, run: + +```bash +pw groupmod input -m $USER +``` + +After adding yourself to the group, log out and log back in for the changes to take effect. + ### Linux #### 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 the program. This is necessary to -allow Sunshine to use 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 the program. This is necessary to +> allow Sunshine to use KMS capture. ##### Enable ```bash @@ -384,8 +459,11 @@ Sunshine can only access microphones on macOS due to system limitations. To stre [Soundflower](https://github.com/mattingalls/Soundflower) or [BlackHole](https://github.com/ExistentialAudio/BlackHole). -@note{Command Keys are not forwarded by Moonlight. Right Option-Key is mapped to CMD-Key.} -@caution{Gamepads are not currently supported.} +> [!NOTE] +> Command Keys are not forwarded by Moonlight. Right Option-Key is mapped to CMD-Key. + +> [!CAUTION] +> Gamepads are not currently supported. ## Usage @@ -393,8 +471,9 @@ Sunshine can only access microphones on macOS due to system limitations. To stre If Sunshine is not installed/running as a service, then start Sunshine with the following command, unless a start command is listed in the specified package [install](#install) instructions above. -@note{A service is a process that runs in the background. This is the default when installing Sunshine from the -Windows installer. Running multiple instances of Sunshine is not advised.} +> [!NOTE] +> A service is a process that runs in the background. This is the default when installing Sunshine from the +> Windows installer. Running multiple instances of Sunshine is not advised. ```bash sunshine @@ -405,9 +484,11 @@ sunshine sunshine /sunshine.conf ``` -@note{You do not need to specify a config file. If no config file is entered the default location will be used.} +> [!NOTE] +> You do not need to specify a config file. If no config file is entered, the default location will be used. -@attention{The configuration file specified will be created if it doesn't exist.} +> [!TIP] +> The configuration file specified will be created if it doesn't exist. ### Start Sunshine over SSH (Linux/X11) Assuming you are already logged into the host, you can use this command @@ -424,9 +505,10 @@ be ready. ssh @ 'startx &; export DISPLAY=:0; sunshine' ``` -@tip{You could also utilize the `~/.bash_profile` or `~/.bashrc` files to set up the `DISPLAY` variable.} +> [!TIP] +> You could also use the `~/.bash_profile` or `~/.bashrc` files to set up the `DISPLAY` variable. -@seealso{ See [Remote SSH Headless Setup](https://app.lizardbyte.dev/2023-09-14-remote-ssh-headless-sunshine-setup) +@seealso{See [Remote SSH Headless Setup](https://app.lizardbyte.dev/2023-09-14-remote-ssh-headless-sunshine-setup) on how to set up a headless streaming server without autologin and dummy plugs (X11 + NVidia GPUs)} ### Configuration @@ -434,10 +516,12 @@ on how to set up a headless streaming server without autologin and dummy plugs ( Sunshine is configured via the web ui, which is available on [https://localhost:47990](https://localhost:47990) by default. You may replace *localhost* with your internal ip address. -@attention{Ignore any warning given by your browser about "insecure website". This is due to the SSL certificate -being self-signed.} +> [!NOTE] +> Ignore any warning given by your browser about "insecure website". This is due to the SSL certificate +> being self-signed. -@caution{If running for the first time, make sure to note the username and password that you created.} +> [!CAUTION] +> If running for the first time, make sure to note the username and password that you created. 1. Add games and applications. 2. Adjust any configuration settings as needed. @@ -453,15 +537,15 @@ being self-signed.} To get a list of available arguments, run the following command. @tabs{ - @tab{ General | @code{.bash} + @tab{ General | ```bash sunshine --help - @endcode } - @tab{ AppImage | @code{.bash} + ```} + @tab{ AppImage | ```bash ./sunshine.AppImage --help - @endcode } - @tab{ Flatpak | @code{.bash} + ```} + @tab{ Flatpak | ```bash flatpak run --command=sunshine dev.lizardbyte.app.Sunshine --help - @endcode } + ```} } ### Shortcuts @@ -495,7 +579,16 @@ All shortcuts start with `Ctrl+Alt+Shift`, just like Moonlight. instead it simply starts a stream. If you removed it and would like to get it back, just add a new application with the name "Desktop" and "desktop.png" as the image path. * For the Linux flatpak you must prepend commands with `flatpak-spawn --host`. -* If inputs (mouse, keyboard, gamepads...) aren't working after connecting, add the user running sunshine to the `input` group. +* If inputs (mouse, keyboard, gamepads...) aren't working after connecting: + + * On FreeBSD/Linux, add the user running sunshine to the `input` group. + +* The FreeBSD version of Sunshine is missing some features that are present on Linux. + The following are known limitations. + + * Only X11 and Wayland capture are supported + * DualSense/DS5 emulation is not available due to missing uhid features + ### HDR Support Streaming HDR content is officially supported on Windows hosts and experimentally supported for Linux hosts. @@ -513,24 +606,24 @@ Streaming HDR content is officially supported on Windows hosts and experimentall * Some GPUs video encoders can produce lower image quality or encoding performance when streaming in HDR compared to SDR. -* Additional information: +Additional information: - @tabs{ - @tab{ Windows | - - HDR streaming is supported for Intel, AMD, and NVIDIA GPUs that support encoding HEVC Main 10 or AV1 10-bit profiles. - - We recommend calibrating the display by streaming the Windows HDR Calibration app to your client device and saving an HDR calibration profile to use while streaming. - - Older games that use NVIDIA-specific NVAPI HDR rather than native Windows HDR support may not display properly in HDR. - } +@tabs{ + @tab{ Windows | + - HDR streaming is supported for Intel, AMD, and NVIDIA GPUs that support encoding HEVC Main 10 or AV1 10-bit profiles. + - We recommend calibrating the display by streaming the Windows HDR Calibration app to your client device and saving an HDR calibration profile to use while streaming. + - Older games that use NVIDIA-specific NVAPI HDR rather than native Windows HDR support may not display properly in HDR. + } - @tab{ Linux | - - HDR streaming is supported for Intel and AMD GPUs that support encoding HEVC Main 10 or AV1 10-bit profiles using VAAPI. - - The KMS capture backend is required for HDR capture. Other capture methods, like NvFBC or X11, do not support HDR. - - You will need a desktop environment with a compositor that supports HDR rendering, such as Gamescope or KDE Plasma 6. +@tab{ Linux | + - HDR streaming is supported for Intel and AMD GPUs that support encoding HEVC Main 10 or AV1 10-bit profiles using VAAPI. + - The KMS capture backend is required for HDR capture. Other capture methods, like NvFBC or X11, do not support HDR. + - You will need a desktop environment with a compositor that supports HDR rendering, such as Gamescope or KDE Plasma 6. - @seealso{[Arch wiki on HDR Support for Linux](https://wiki.archlinux.org/title/HDR_monitor_support) and - [Reddit Guide for HDR Support for AMD GPUs](https://www.reddit.com/r/linux_gaming/comments/10m2gyx/guide_alpha_test_hdr_on_linux)} - } + @seealso{[Arch wiki on HDR Support for Linux](https://wiki.archlinux.org/title/HDR_monitor_support) and + [Reddit Guide for HDR Support for AMD GPUs](https://www.reddit.com/r/linux_gaming/comments/10m2gyx/guide_alpha_test_hdr_on_linux)} } +} ### Tutorials and Guides Tutorial videos are available [here](https://www.youtube.com/playlist?list=PLMYr5_xSeuXAbhxYHz86hA1eCDugoxXY0). diff --git a/docs/legal.md b/docs/legal.md index 161791a6f97..d171389fcd4 100644 --- a/docs/legal.md +++ b/docs/legal.md @@ -1,6 +1,8 @@ # Legal -@attention{This documentation is for informational purposes only and is not intended as legal advice. If you have -any legal questions or concerns about using Sunshine, we recommend consulting with a lawyer.} + +> [!CAUTION] +> This documentation is for informational purposes only and is not intended as legal advice. If you have +> any legal questions or concerns about using Sunshine, we recommend consulting with a lawyer. Sunshine is licensed under the GPL-3.0 license, which allows for free use and modification of the software. The full text of the license can be reviewed [here](https://github.com/LizardByte/Sunshine/blob/master/LICENSE). diff --git a/docs/third_party_packages.md b/docs/third_party_packages.md index 148672997e1..33b18e14dbf 100644 --- a/docs/third_party_packages.md +++ b/docs/third_party_packages.md @@ -1,6 +1,7 @@ # Third-Party Packages -@danger{These packages are not maintained by LizardByte. Use at your own risk.} +> [!WARNING] +> These packages are not maintained by LizardByte. Use at your own risk. ## Chocolatey [![Chocolatey](https://img.shields.io/badge/dynamic/xml.svg?color=orange&label=chocolatey&style=for-the-badge&prefix=v&query=%2F%2Ftr%5B%40id%3D%27chocolatey%27%5D%2Ftd%5B3%5D%2Fspan%2Fa&url=https%3A%2F%2Frepology.org%2Fproject%2Fsunshine%2Fversions&logo=chocolatey)](https://community.chocolatey.org/packages/sunshine) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 479956bb164..b5b211bf945 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -20,8 +20,9 @@ If you forgot your credentials to the web UI, try this. } } -@tip{Don't forget to replace `{new-username}` and `{new-password}` with your new credentials. -Do not include the curly braces.} +> [!TIP] +> Remember to replace `{new-username}` and `{new-password}` with your new credentials. +> Do not include the curly braces. ### Unusual Mouse Behavior If you experience unusual mouse behavior, try attaching a physical mouse to the Sunshine host. @@ -32,11 +33,11 @@ Can't access the web UI? 1. Check firewall rules. ### Controller works on Steam but not in games -One trick might be to change Steam settings and check or uncheck the configuration to support Xbox/Playstation +One trick might be to change Steam settings and check or uncheck the configuration to support Xbox/PlayStation controllers and leave only support for Generic controllers. Also, if you have many controllers already directly connected to the host, it might help to disable them so that the -Sunshine provided controller (connected to the guest) is the "first" one. In Linux this can be accomplished on USB +Sunshine-provided controller (connected to the guest) is the "first" one. In Linux this can be achieved on USB devices by finding the device in `/sys/bus/usb/devices/` and writing `0` to the `authorized` file. ### Network performance test @@ -53,7 +54,7 @@ On the Sunshine host `iperf3` is started in server mode: iperf3 -s ``` -On the client device iperf3 is asked to perform a 60-second UDP test in reverse +On the client device iperf3 is asked to perform a 60-second UDP test in a reverse direction (from server to client) at a given bitrate (e.g. 50 Mbps): ```bash @@ -61,31 +62,31 @@ iperf3 -c {HostIpAddress} -t 60 -u -R -b 50M ``` Watch the output on the client for packet loss and jitter values. Both should be -(very) low. Ideally packet loss remains less than 5% and jitter below 1ms. +(very) low. Ideally, packet loss remains less than 5% and jitter below 1 ms. For Android clients use [PingMaster](https://play.google.com/store/apps/details?id=com.appplanex.pingmasternetworktools). For iOS clients use [HE.NET Network Tools](https://apps.apple.com/us/app/he-net-network-tools/id858241710). -If you are testing a remote connection (over the internet) you will need to +If you are testing a remote connection (over the internet), you will need to forward the port 5201 (TCP and UDP) from your host. ### Packet loss (Buffer overrun) If the host PC (running Sunshine) has a much faster connection to the network than the slowest segment of the network path to the client device (running Moonlight), massive packet loss can occur: Sunshine emits its stream in bursts -every 16ms (for 60fps) but those bursts can't be passed on fast enough to the +every 16 ms (for 60 fps), but those bursts can't be passed on fast enough to the client and must be buffered by one of the network devices inbetween. If the bitrate is high enough, these buffers will overflow and data will be discarded. -This can easily happen if e.g. the host has a 2.5 Gbit/s connection and the +This can easily happen if e.g., the host has a 2.5 Gbit/s connection and the client only 1 Gbit/s or Wi-Fi. Similarly, a 1 Gbps host may be too fast for a client having only a 100 Mbps interface. As a workaround the transmission speed of the host NIC can be reduced: 1 Gbps instead of 2.5 or 100 Mbps instead of 1 Gbps. A technically more advanced -solution would be to configure traffic shaping rules at the OS-level, so that +solution would be to configure traffic shaping rules at the OS level, so that only Sunshine's traffic is slowed down. Such a solution on Linux could look like that: @@ -112,7 +113,7 @@ sudo tc filter add dev protocol ip parent 1: prio 1 \ ``` In that way only the Sunshine traffic is limited by 1 Gbit. This is not persistent on reboots. -If you use a different port for the game stream you need to adjust the last command. +If you use a different port for the game stream, you need to adjust the last command. Sunshine versions > 0.23.1 include improved networking code that should alleviate or even solve this issue (without reducing the NIC speed). @@ -120,7 +121,7 @@ alleviate or even solve this issue (without reducing the NIC speed). ### Packet loss (MTU) Although unlikely, some guests might work better with a lower [MTU](https://en.wikipedia.org/wiki/Maximum_transmission_unit) from the host. -For example, a LG TV was found to have 30-60% packet loss when the host had MTU +For example, an LG TV was found to have 30–60% packet loss when the host had MTU set to 1500 and 1472, but 0% packet loss with a MTU of 1428 set in the network card serving the stream (a Linux PC). It's unclear how that helped precisely, so it's a last resort suggestion. @@ -134,19 +135,23 @@ Due to legal concerns, Mesa has disabled hardware decoding and encoding by defau Error: Could not open codec [h264_vaapi]: Function not implemented ``` -If you see the above error in the Sunshine logs, compiling *Mesa* manually, may be required. See the official Mesa3D +If you see the above error in the Sunshine logs, compiling *Mesa* manually may be required. See the official Mesa3D [Compiling and Installing](https://docs.mesa3d.org/install.html) documentation for instructions. -@important{You must re-enable the disabled encoders. You can do so, by passing the following argument to the build -system. You may also want to enable decoders, however that is not required for Sunshine and is not covered here. -```bash --Dvideo-codecs=h264enc,h265enc -``` -} +> [!IMPORTANT] +> You must re-enable the disabled encoders. You can do so by passing the following argument to the build +> system. You may also want to enable decoders, however, that is not required for Sunshine and is not covered here. +> ```bash +> -Dvideo-codecs=h264enc,h265enc +> ``` + +> [!NOTE] +> Other build options are listed in the +> [meson options](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/meson_options.txt) file. ### Input not working After installation, the `udev` rules need to be reloaded. Our post-install script tries to do this for you -automatically, but if it fails you may need to restart your system. +automatically, but if it fails, you may need to restart your system. If the input is still not working, you may need to add your user to the `input` group. @@ -154,9 +159,6 @@ If the input is still not working, you may need to add your user to the `input` sudo usermod -aG input $USER ``` -@note{Other build options are listed in the -[meson options](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/meson_options.txt) file.} - ### KMS Streaming fails If screencasting fails with KMS, you may need to run the following to force unprivileged screencasting. @@ -164,9 +166,10 @@ If screencasting fails with KMS, you may need to run the following to force unpr sudo setcap -r $(readlink -f $(which sunshine)) ``` -@note{The above command will not work with the AppImage or Flatpak packages. Please refer to the -[AppImage setup](md_docs_2getting__started.html#appimage) or -[Flatpak setup](md_docs_2getting__started.html#flatpak) for more specific instructions.} +> [!NOTE] +> The above command will not work with the AppImage or Flatpak packages. Please refer to the +> [AppImage setup](md_docs_2getting__started.html#appimage) or +> [Flatpak setup](md_docs_2getting__started.html#flatpak) for more specific instructions. ### KMS streaming fails on Nvidia GPUs If KMS screen capture results in a black screen being streamed, you may need to @@ -181,12 +184,12 @@ Consult your distribution's documentation for details on how to do this. (Most often grub is used to load the kernel and set its command line.) ### AMD encoding latency issues -If you notice unexpectedly high encoding latencies (e.g. in Moonlight's +If you notice unexpectedly high encoding latencies (e.g., in Moonlight's performance overlay) or strong fluctuations thereof, your system's Mesa libraries are outdated (<24.2). This is particularly problematic at higher resolutions (4K). -Starting with Mesa-24.2 applications can request a +Starting with Mesa-24.2, applications can request a [low-latency mode](https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/30039) by running them with a special [environment variable](https://docs.mesa3d.org/envvars.html#envvar-AMD_DEBUG): @@ -224,7 +227,7 @@ Verify that you've installed [Nefarius Virtual Gamepad](https://github.com/nefar ### Permission denied Since Sunshine runs as a service on Windows, it may not have the same level of access that your regular user account -has. You may get permission denied errors when attempting to launch a game or application from a non system drive. +has. You may get permission denied errors when attempting to launch a game or application from a non-system drive. You will need to modify the security permissions on your disk. Ensure that user/principal SYSTEM has full permissions on the disk. diff --git a/package.json b/package.json index 390945455c2..bff403e5adb 100644 --- a/package.json +++ b/package.json @@ -7,16 +7,17 @@ "dev": "vite build --watch", "serve": "serve ./tests/fixtures/http --no-port-switching" }, + "type": "module", "dependencies": { - "@lizardbyte/shared-web": "2025.626.181239", - "vue": "3.5.18", - "vue-i18n": "11.1.11" + "@lizardbyte/shared-web": "2025.922.181114", + "vue": "3.5.25", + "vue-i18n": "11.2.2" }, "devDependencies": { "@codecov/vite-plugin": "1.9.1", - "@vitejs/plugin-vue": "4.6.2", - "serve": "14.2.4", - "vite": "4.5.14", - "vite-plugin-ejs": "1.6.4" + "@vitejs/plugin-vue": "6.0.1", + "serve": "14.2.5", + "vite": "6.3.6", + "vite-plugin-ejs": "1.7.0" } } diff --git a/packaging/linux/AppImage/AppRun b/packaging/linux/AppImage/AppRun index e90ee3a4fef..c021e425936 100644 --- a/packaging/linux/AppImage/AppRun +++ b/packaging/linux/AppImage/AppRun @@ -47,6 +47,8 @@ function install() { # user input rules # shellcheck disable=SC2002 cat "$SUNSHINE_SHARE_HERE/udev/rules.d/60-sunshine.rules" | sudo tee /etc/udev/rules.d/60-sunshine.rules + cat "$SUNSHINE_SHARE_HERE/modules-load.d/60-sunshine.conf" | sudo tee /etc/modules-load.d/60-sunshine.conf + sudo modprobe uhid sudo udevadm control --reload-rules sudo udevadm trigger --property-match=DEVNAME=/dev/uinput sudo udevadm trigger --property-match=DEVNAME=/dev/uhid @@ -65,6 +67,9 @@ function remove() { # remove input rules sudo rm -f /etc/udev/rules.d/60-sunshine.rules + # remove uhid module loading config + sudo rm -f /etc/modules-load.d/60-sunshine.conf + # remove service sudo rm -f ~/.config/systemd/user/sunshine.service } diff --git a/packaging/linux/Arch/PKGBUILD b/packaging/linux/Arch/PKGBUILD index eab48e31dd1..f1ac59837f1 100644 --- a/packaging/linux/Arch/PKGBUILD +++ b/packaging/linux/Arch/PKGBUILD @@ -1,6 +1,13 @@ # Edit on github: https://github.com/LizardByte/Sunshine/blob/master/packaging/linux/Arch/PKGBUILD # Reference: https://wiki.archlinux.org/title/PKGBUILD +## options +: "${_run_unit_tests:=false}" # if set to true; unit tests will be executed post build; useful in CI +: "${_support_headless_testing:=false}" +: "${_use_cuda:=detect}" # nvenc + +: "${_commit:=@GITHUB_COMMIT@}" + pkgname='sunshine' pkgver=@PROJECT_VERSION@@SUNSHINE_SUB_VERSION@ pkgrel=1 @@ -33,6 +40,7 @@ depends=( 'openssl' 'opus' 'udev' + 'which' ) makedepends=( @@ -40,7 +48,6 @@ makedepends=( 'appstream-glib' 'cmake' 'desktop-file-utils' - 'cuda' "gcc${_gcc_version}" 'git' 'make' @@ -49,17 +56,45 @@ makedepends=( ) optdepends=( - 'cuda: Nvidia GPU encoding support' 'libva-mesa-driver: AMD GPU encoding support' - 'xorg-server-xvfb: Virtual X server for headless testing' ) provides=() conflicts=() -source=("$pkgname::git+@GITHUB_CLONE_URL@#commit=@GITHUB_COMMIT@") +source=("$pkgname::git+@GITHUB_CLONE_URL@#commit=${_commit}") sha256sums=('SKIP') +# Options Handling +if [[ "${_use_cuda::1}" == "d" ]] && pacman -Qi cuda &> /dev/null; then + _use_cuda=true +fi + +if [[ "${_use_cuda::1}" == "t" ]]; then + optdepends+=( + 'cuda: Nvidia GPU encoding support' + ) +fi + +if [[ "${_support_headless_testing::1}" == "t" ]]; then + optdepends+=( + 'xorg-server-xvfb: Virtual X server for headless testing' + ) +fi + +# Ensure makedepends, checkdepends, optdepends are sorted +if [ -n "${makedepends+x}" ]; then + mapfile -t tmp_array < <(printf '%s\n' "${makedepends[@]}" | sort) + makedepends=("${tmp_array[@]}") + unset tmp_array +fi + +if [ -n "${optdepends+x}" ]; then + mapfile -t tmp_array < <(printf '%s\n' "${optdepends[@]}" | sort) + optdepends=("${tmp_array[@]}") + unset tmp_array +fi + prepare() { cd "$pkgname" git submodule update --recursive --init @@ -68,7 +103,7 @@ prepare() { build() { export BRANCH="@GITHUB_BRANCH@" export BUILD_VERSION="@BUILD_VERSION@" - export COMMIT="@GITHUB_COMMIT@" + export COMMIT="${_commit}" export CC="gcc-${_gcc_version}" export CXX="g++-${_gcc_version}" @@ -76,18 +111,45 @@ build() { export CFLAGS="${CFLAGS/-Werror=format-security/}" export CXXFLAGS="${CXXFLAGS/-Werror=format-security/}" - cmake \ - -S "$pkgname" \ - -B build \ - -Wno-dev \ - -D BUILD_DOCS=OFF \ - -D BUILD_WERROR=ON \ - -D CMAKE_INSTALL_PREFIX=/usr \ - -D SUNSHINE_EXECUTABLE_PATH=/usr/bin/sunshine \ - -D SUNSHINE_ASSETS_DIR="share/sunshine" \ - -D SUNSHINE_PUBLISHER_NAME='LizardByte' \ - -D SUNSHINE_PUBLISHER_WEBSITE='https://app.lizardbyte.dev' \ - -D SUNSHINE_PUBLISHER_ISSUE_URL='https://app.lizardbyte.dev/support' + export MAKEFLAGS="${MAKEFLAGS:--j$(nproc)}" + + local _cmake_options=( + -S "$pkgname" + -B build + -Wno-dev + -D BUILD_DOCS=OFF + -D BUILD_WERROR=ON + -D CMAKE_INSTALL_PREFIX=/usr + -D SUNSHINE_EXECUTABLE_PATH=/usr/bin/sunshine + -D SUNSHINE_ASSETS_DIR="share/sunshine" + -D SUNSHINE_PUBLISHER_NAME='LizardByte' + -D SUNSHINE_PUBLISHER_WEBSITE='https://app.lizardbyte.dev' + -D SUNSHINE_PUBLISHER_ISSUE_URL='https://app.lizardbyte.dev/support' + ) + + if [[ "${_use_cuda::1}" != "t" ]]; then + _cmake_options+=(-DSUNSHINE_ENABLE_CUDA=OFF -DCUDA_FAIL_ON_MISSING=OFF) + else + # If cuda has just been installed, its variables will not be available in the environment + # therefore, set them manually to the expected values on Arch Linux + if [ -z "${CUDA_PATH:-}" ] && pacman -Qi cuda &> /dev/null; then + local _cuda_gcc_version + _cuda_gcc_version="$(LC_ALL=C pacman -Si cuda | grep -Pom1 '^Depends On\s*:.*\bgcc\K[0-9]+\b' || true)" + + export CUDA_PATH=/opt/cuda + if [ -n "$_cuda_gcc_version" ]; then + export NVCC_CCBIN="/usr/bin/g++-${_cuda_gcc_version}" + else + export NVCC_CCBIN="/usr/bin/g++" + fi + fi + fi + + if [[ "${_run_unit_tests::1}" != "t" ]]; then + _cmake_options+=(-DBUILD_TESTS=OFF) + fi + + cmake "${_cmake_options[@]}" appstreamcli validate "build/dev.lizardbyte.app.Sunshine.metainfo.xml" appstream-util validate "build/dev.lizardbyte.app.Sunshine.metainfo.xml" @@ -98,13 +160,19 @@ build() { } check() { - export CC="gcc-${_gcc_version}" - export CXX="g++-${_gcc_version}" + cd "${srcdir}/build" + ./sunshine --version + + if [[ "${_run_unit_tests::1}" == "t" ]]; then + export CC="gcc-${_gcc_version}" + export CXX="g++-${_gcc_version}" - cd "${srcdir}/build/tests" - ./test_sunshine --gtest_color=yes + cd "${srcdir}/build/tests" + ./test_sunshine --gtest_color=yes + fi } package() { + export MAKEFLAGS="${MAKEFLAGS:--j$(nproc)}" make -C build install DESTDIR="$pkgdir" } diff --git a/packaging/linux/Arch/sunshine.install b/packaging/linux/Arch/sunshine.install index 6b274cdf8c1..d7b87737fc1 100644 --- a/packaging/linux/Arch/sunshine.install +++ b/packaging/linux/Arch/sunshine.install @@ -1,5 +1,5 @@ do_setcap() { - setcap cap_sys_admin+p $(readlink -f $(which sunshine)) + setcap cap_sys_admin+p $(readlink -f usr/bin/sunshine) } do_udev_reload() { @@ -13,10 +13,11 @@ do_udev_reload() { post_install() { do_setcap do_udev_reload + modprobe uhid } post_upgrade() { do_setcap do_udev_reload + modprobe uhid } - diff --git a/packaging/linux/fedora/Sunshine.spec b/packaging/linux/copr/Sunshine.spec similarity index 76% rename from packaging/linux/fedora/Sunshine.spec rename to packaging/linux/copr/Sunshine.spec index 798d0009491..3d515fecc8d 100644 --- a/packaging/linux/fedora/Sunshine.spec +++ b/packaging/linux/copr/Sunshine.spec @@ -7,6 +7,13 @@ %undefine _hardened_build +# Define _metainfodir for OpenSUSE if not already defined +%if 0%{?suse_version} +%if !0%{?_metainfodir:1} +%global _metainfodir %{_datadir}/metainfo +%endif +%endif + Name: Sunshine Version: %{build_version} Release: 1%{?dist} @@ -15,17 +22,14 @@ License: GPLv3-only URL: https://github.com/LizardByte/Sunshine Source0: tarball.tar.gz -BuildRequires: appstream -# BuildRequires: boost-devel >= 1.86.0 +# Common BuildRequires BuildRequires: cmake >= 3.25.0 BuildRequires: desktop-file-utils -BuildRequires: libappstream-glib -BuildRequires: libayatana-appindicator3-devel +BuildRequires: git BuildRequires: libcap-devel BuildRequires: libcurl-devel BuildRequires: libdrm-devel BuildRequires: libevdev-devel -BuildRequires: libgudev BuildRequires: libnotify-devel BuildRequires: libva-devel BuildRequires: libX11-devel @@ -36,42 +40,93 @@ BuildRequires: libXi-devel BuildRequires: libXinerama-devel BuildRequires: libXrandr-devel BuildRequires: libXtst-devel -BuildRequires: git +BuildRequires: npm +BuildRequires: openssl-devel +BuildRequires: rpm-build +BuildRequires: systemd-rpm-macros +BuildRequires: wget +BuildRequires: which + +%if 0%{?fedora} +# Fedora-specific BuildRequires +BuildRequires: appstream +# BuildRequires: boost-devel >= 1.86.0 +BuildRequires: libappstream-glib +BuildRequires: libayatana-appindicator3-devel +BuildRequires: libgudev BuildRequires: mesa-libGL-devel BuildRequires: mesa-libgbm-devel BuildRequires: miniupnpc-devel -BuildRequires: npm BuildRequires: numactl-devel -BuildRequires: openssl-devel BuildRequires: opus-devel BuildRequires: pulseaudio-libs-devel -BuildRequires: rpm-build BuildRequires: systemd-udev -BuildRequires: systemd-rpm-macros %{?sysusers_requires_compat} -BuildRequires: wget -BuildRequires: which - # for unit tests BuildRequires: xorg-x11-server-Xvfb +%endif -# Conditional BuildRequires for cuda-gcc based on Fedora version -%if 0%{?fedora} >= 40 && 0%{?fedora} <= 41 +%if 0%{?suse_version} +# OpenSUSE-specific BuildRequires +BuildRequires: AppStream +BuildRequires: appstream-glib +BuildRequires: libappindicator3-devel +BuildRequires: libgudev-1_0-devel +BuildRequires: Mesa-libGL-devel +BuildRequires: libgbm-devel +BuildRequires: libminiupnpc-devel +BuildRequires: libnuma-devel +BuildRequires: libopus-devel +BuildRequires: libpulse-devel +BuildRequires: udev +# for unit tests +BuildRequires: xvfb-run +%endif + +# Conditional BuildRequires for cuda-gcc based on distribution version +%if 0%{?fedora} +%if 0%{?fedora} <= 41 BuildRequires: gcc13 BuildRequires: gcc13-c++ %global gcc_version 13 -%global cuda_version 12.6.3 -%global cuda_build 560.35.05 +%global cuda_version 12.9.1 +%global cuda_build 575.57.08 %elif %{?fedora} >= 42 BuildRequires: gcc14 BuildRequires: gcc14-c++ %global gcc_version 14 -%global cuda_version 12.8.1 -%global cuda_build 570.124.06 +%global cuda_version 12.9.1 +%global cuda_build 575.57.08 +%endif +%endif + +%if 0%{?suse_version} +%if 0%{?suse_version} <= 1699 +# OpenSUSE Leap 15.x +BuildRequires: gcc13 +BuildRequires: gcc13-c++ +%global gcc_version 13 +%global cuda_version 12.9.1 +%global cuda_build 575.57.08 +%else +# OpenSUSE Tumbleweed +BuildRequires: gcc14 +BuildRequires: gcc14-c++ +%global gcc_version 14 +%global cuda_version 12.9.1 +%global cuda_build 575.57.08 +%endif %endif %global cuda_dir %{_builddir}/cuda +# Common runtime requirements +Requires: miniupnpc >= 2.2.4 +Requires: which >= 2.21 + +%if 0%{?fedora} +# Fedora runtime requirements +Requires: libayatana-appindicator3 >= 0.5.3 Requires: libcap >= 2.22 Requires: libcurl >= 7.0 Requires: libdrm > 2.4.97 @@ -80,11 +135,26 @@ Requires: libopusenc >= 0.2.1 Requires: libva >= 2.14.0 Requires: libwayland-client >= 1.20.0 Requires: libX11 >= 1.7.3.1 -Requires: miniupnpc >= 2.2.4 Requires: numactl-libs >= 2.0.14 Requires: openssl >= 3.0.2 Requires: pulseaudio-libs >= 10.0 -Requires: libayatana-appindicator3 >= 0.5.3 +%endif + +%if 0%{?suse_version} +# OpenSUSE runtime requirements +Requires: libappindicator3-1 +Requires: libcap2 +Requires: libcurl4 +Requires: libdrm2 +Requires: libevdev2 +Requires: libopusenc0 +Requires: libva2 +Requires: libwayland-client0 +Requires: libX11-6 +Requires: libnuma1 +Requires: libopenssl3 +Requires: libpulse0 +%endif %description Self-hosted game stream host for Moonlight. @@ -160,9 +230,9 @@ function install_cuda() { --toolkitpath="%{cuda_dir}" rm "%{_builddir}/cuda.run" - # we need to patch math_functions.h on fedora 42 + # we need to patch math_functions.h on fedora 42+ # see https://forums.developer.nvidia.com/t/error-exception-specification-is-incompatible-for-cospi-sinpi-cospif-sinpif-with-glibc-2-41/323591/3 - if [ "%{?fedora}" -eq 42 ]; then + if [ "%{?fedora}" -ge 42 ]; then echo "Original math_functions.h:" find "%{cuda_dir}" -name math_functions.h -exec cat {} \; @@ -171,7 +241,7 @@ function install_cuda() { --backup \ --directory="%{cuda_dir}" \ --verbose \ - < "%{_builddir}/Sunshine/packaging/linux/fedora/patches/f42/${architecture}/01-math_functions.patch" + < "%{_builddir}/Sunshine/packaging/linux/patches/${architecture}/01-math_functions.patch" fi } @@ -210,15 +280,13 @@ xvfb-run ./tests/test_sunshine cd %{_builddir}/Sunshine/build %make_install -# Add modules-load configuration -# load the uhid module in initramfs even if it doesn't detect the module as being used during dracut -# which must be run every time a new kernel is installed -install -D -m 0644 /dev/stdin %{buildroot}/usr/lib/modules-load.d/uhid.conf <= 12.2 +runtime-version: "24.08" sdk: org.freedesktop.Sdk sdk-extensions: - org.freedesktop.Sdk.Extension.node20 diff --git a/packaging/linux/flatpak/modules/boost.json b/packaging/linux/flatpak/modules/boost.json index 79eadba48f8..c78661daee8 100644 --- a/packaging/linux/flatpak/modules/boost.json +++ b/packaging/linux/flatpak/modules/boost.json @@ -3,14 +3,14 @@ "buildsystem": "simple", "build-commands": [ "cd tools/build && bison -y -d -o src/engine/jamgram.cpp src/engine/jamgram.y", - "./bootstrap.sh --prefix=$FLATPAK_DEST --with-libraries=filesystem,locale,log,program_options,system", + "./bootstrap.sh --prefix=$FLATPAK_DEST --with-libraries=filesystem,locale,log,program_options", "./b2 install variant=release link=shared runtime-link=shared cxxflags=\"$CXXFLAGS\"" ], "sources": [ { "type": "archive", - "url": "https://github.com/boostorg/boost/releases/download/boost-1.87.0/boost-1.87.0-cmake.tar.xz", - "sha256": "7da75f171837577a52bbf217e17f8ea576c7c246e4594d617bfde7fafd408be5" + "url": "https://github.com/boostorg/boost/releases/download/boost-1.89.0/boost-1.89.0-cmake.tar.xz", + "sha256": "67acec02d0d118b5de9eb441f5fb707b3a1cdd884be00ca24b9a73c995511f74" } ] } diff --git a/packaging/linux/flatpak/modules/cuda.json b/packaging/linux/flatpak/modules/cuda.json index 3e10f2fb7df..27fdb58114b 100644 --- a/packaging/linux/flatpak/modules/cuda.json +++ b/packaging/linux/flatpak/modules/cuda.json @@ -19,8 +19,8 @@ "only-arches": [ "x86_64" ], - "url": "https://developer.download.nvidia.com/compute/cuda/12.6.2/local_installers/cuda_12.6.2_560.35.03_linux.run", - "sha256": "3729a89cb58f7ca6a46719cff110d6292aec7577585a8d71340f0dbac54fb237", + "url": "https://developer.download.nvidia.com/compute/cuda/12.9.1/local_installers/cuda_12.9.1_575.57.08_linux.run", + "sha256": "0f6d806ddd87230d2adbe8a6006a9d20144fdbda9de2d6acc677daa5d036417a", "dest-filename": "cuda.run" }, { @@ -28,8 +28,8 @@ "only-arches": [ "aarch64" ], - "url": "https://developer.download.nvidia.com/compute/cuda/12.6.2/local_installers/cuda_12.6.2_560.35.03_linux_sbsa.run", - "sha256": "2249408848b705c18b9eadfb5161b52e4e36fcc5753647329cce93db141e5466", + "url": "https://developer.download.nvidia.com/compute/cuda/12.9.1/local_installers/cuda_12.9.1_575.57.08_linux_sbsa.run", + "sha256": "64f47ab791a76b6889702425e0755385f5fa216c5a9f061875c7deed5f08cdb6", "dest-filename": "cuda.run" } ] diff --git a/packaging/linux/flatpak/modules/miniupnpc.json b/packaging/linux/flatpak/modules/miniupnpc.json index 2ab0c2dc3fa..9920633c62f 100644 --- a/packaging/linux/flatpak/modules/miniupnpc.json +++ b/packaging/linux/flatpak/modules/miniupnpc.json @@ -2,6 +2,7 @@ "name": "miniupnpc", "buildsystem": "cmake-ninja", "builddir": true, + "subdir": "miniupnpc", "config-opts": [ "-DCMAKE_BUILD_TYPE=RelWithDebInfo", "-DUPNPC_BUILD_STATIC=OFF", @@ -11,15 +12,16 @@ ], "sources": [ { - "url": "https://miniupnp.tuxfamily.org/files/miniupnpc-2.3.3.tar.gz", - "sha256": "d52a0afa614ad6c088cc9ddff1ae7d29c8c595ac5fdd321170a05f41e634bd1a", + "type": "git", + "url": "https://github.com/miniupnp/miniupnp.git", + "tag": "miniupnpc_2_3_3", + "commit": "bf4215a7574f88aa55859db9db00e3ae58cf42d6", "x-checker-data": { "type": "anitya", "project-id": 1986, "stable-only": true, - "url-template": "https://miniupnp.tuxfamily.org/files/miniupnpc-$version.tar.gz" - }, - "type": "archive" + "tag-template": "miniupnpc_${version0}_${version1}_${version2}" + } } ], "cleanup": [ diff --git a/packaging/linux/flatpak/scripts/additional-install.sh b/packaging/linux/flatpak/scripts/additional-install.sh index 06c5835ec6d..5cd6cfbc809 100644 --- a/packaging/linux/flatpak/scripts/additional-install.sh +++ b/packaging/linux/flatpak/scripts/additional-install.sh @@ -6,6 +6,12 @@ cp "/app/share/sunshine/systemd/user/sunshine.service" "$HOME/.config/systemd/us echo "Sunshine User Service has been installed." echo "Use [systemctl --user enable sunshine] once to autostart Sunshine on login." +# Load uhid (DS5 emulation) +UHID=$(cat /app/share/sunshine/modules-load.d/60-sunshine.conf) +echo "Enabling DS5 emulation." +flatpak-spawn --host pkexec sh -c "echo '$UHID' > /etc/modules-load.d/60-sunshine.conf" +flatpak-spawn --host pkexec modprobe uhid + # Udev rule UDEV=$(cat /app/share/sunshine/udev/rules.d/60-sunshine.rules) echo "Configuring mouse permission." diff --git a/packaging/linux/flatpak/scripts/remove-additional-install.sh b/packaging/linux/flatpak/scripts/remove-additional-install.sh index ea2680efdea..b3b30149bc6 100644 --- a/packaging/linux/flatpak/scripts/remove-additional-install.sh +++ b/packaging/linux/flatpak/scripts/remove-additional-install.sh @@ -6,6 +6,7 @@ rm "$HOME/.config/systemd/user/sunshine.service" systemctl --user daemon-reload echo "Sunshine User Service has been removed." -# Udev rule +# Remove rules +flatpak-spawn --host pkexec sh -c "rm /etc/modules-load.d/60-sunshine.conf" flatpak-spawn --host pkexec sh -c "rm /etc/udev/rules.d/60-sunshine.rules" echo "Input rules removed. Restart computer to take effect." diff --git a/packaging/linux/patches/aarch64/01-math_functions.patch b/packaging/linux/patches/aarch64/01-math_functions.patch new file mode 100644 index 00000000000..f123aefd89d --- /dev/null +++ b/packaging/linux/patches/aarch64/01-math_functions.patch @@ -0,0 +1,89 @@ +diff '--color=auto' -ur a/cuda/targets/sbsa-linux/include/crt/math_functions.h b/cuda/targets/sbsa-linux/include/crt/math_functions.h +--- a/cuda/targets/sbsa-linux/include/crt/math_functions.h 2024-08-23 00:25:39.000000000 +0200 ++++ b/cuda/targets/sbsa-linux/include/crt/math_functions.h 2025-02-17 01:19:44.270292640 +0100 +@@ -594,7 +594,7 @@ + * + * \note_accuracy_double + */ +-extern __DEVICE_FUNCTIONS_DECL__ __device_builtin__ double rsqrt(double x); ++extern __DEVICE_FUNCTIONS_DECL__ __device_builtin__ double rsqrt(double x) noexcept (true); + + /** + * \ingroup CUDA_MATH_SINGLE +@@ -618,7 +618,7 @@ + * + * \note_accuracy_single + */ +-extern __DEVICE_FUNCTIONS_DECL__ __device_builtin__ float rsqrtf(float x); ++extern __DEVICE_FUNCTIONS_DECL__ __device_builtin__ float rsqrtf(float x) noexcept (true); + + #if defined(__QNX__) && !defined(_LIBCPP_VERSION) + namespace std { +@@ -2553,7 +2553,7 @@ + * + * \note_accuracy_double + */ +-extern __DEVICE_FUNCTIONS_DECL__ __device_builtin__ double sinpi(double x); ++extern __DEVICE_FUNCTIONS_DECL__ __device_builtin__ double sinpi(double x) noexcept (true); + /** + * \ingroup CUDA_MATH_SINGLE + * \brief Calculate the sine of the input argument +@@ -2576,7 +2576,7 @@ + * + * \note_accuracy_single + */ +-extern __DEVICE_FUNCTIONS_DECL__ __device_builtin__ float sinpif(float x); ++extern __DEVICE_FUNCTIONS_DECL__ __device_builtin__ float sinpif(float x) noexcept (true); + /** + * \ingroup CUDA_MATH_DOUBLE + * \brief Calculate the cosine of the input argument +@@ -2598,7 +2598,7 @@ + * + * \note_accuracy_double + */ +-extern __DEVICE_FUNCTIONS_DECL__ __device_builtin__ double cospi(double x); ++extern __DEVICE_FUNCTIONS_DECL__ __device_builtin__ double cospi(double x) noexcept (true); + /** + * \ingroup CUDA_MATH_SINGLE + * \brief Calculate the cosine of the input argument +@@ -2620,7 +2620,7 @@ + * + * \note_accuracy_single + */ +-extern __DEVICE_FUNCTIONS_DECL__ __device_builtin__ float cospif(float x); ++extern __DEVICE_FUNCTIONS_DECL__ __device_builtin__ float cospif(float x) noexcept (true); + /** + * \ingroup CUDA_MATH_DOUBLE + * \brief Calculate the sine and cosine of the first input argument +@@ -5982,13 +5982,13 @@ + #pragma warning (disable : 4211) + + #endif /* _WIN32 */ + +-__func__(double rsqrt(double a)); ++__func__(double rsqrt(double a) noexcept (true)); + + __func__(double rcbrt(double a)); + +-__func__(double sinpi(double a)); ++__func__(double sinpi(double a) noexcept (true)); + +-__func__(double cospi(double a)); ++__func__(double cospi(double a) noexcept (true)); + + __func__(void sincospi(double a, double *sptr, double *cptr)); +@@ -6004,10 +6004,10 @@ + __func__(double erfcx(double a)); + +-__func__(float rsqrtf(float a)); ++__func__(float rsqrtf(float a) noexcept (true)); + + __func__(float rcbrtf(float a)); + +-__func__(float sinpif(float a)); ++__func__(float sinpif(float a) noexcept (true)); + +-__func__(float cospif(float a)); ++__func__(float cospif(float a) noexcept (true)); + + __func__(void sincospif(float a, float *sptr, float *cptr)); diff --git a/packaging/linux/patches/x86_64/01-math_functions.patch b/packaging/linux/patches/x86_64/01-math_functions.patch new file mode 100644 index 00000000000..fb8160696d8 --- /dev/null +++ b/packaging/linux/patches/x86_64/01-math_functions.patch @@ -0,0 +1,89 @@ +diff '--color=auto' -ur a/cuda/targets/x86_64-linux/include/crt/math_functions.h b/cuda/targets/x86_64-linux/include/crt/math_functions.h +--- a/cuda/targets/x86_64-linux/include/crt/math_functions.h 2024-08-23 00:25:39.000000000 +0200 ++++ b/cuda/targets/x86_64-linux/include/crt/math_functions.h 2025-02-17 01:19:44.270292640 +0100 +@@ -594,7 +594,7 @@ + * + * \note_accuracy_double + */ +-extern __DEVICE_FUNCTIONS_DECL__ __device_builtin__ double rsqrt(double x); ++extern __DEVICE_FUNCTIONS_DECL__ __device_builtin__ double rsqrt(double x) noexcept (true); + + /** + * \ingroup CUDA_MATH_SINGLE +@@ -618,7 +618,7 @@ + * + * \note_accuracy_single + */ +-extern __DEVICE_FUNCTIONS_DECL__ __device_builtin__ float rsqrtf(float x); ++extern __DEVICE_FUNCTIONS_DECL__ __device_builtin__ float rsqrtf(float x) noexcept (true); + + #if defined(__QNX__) && !defined(_LIBCPP_VERSION) + namespace std { +@@ -2553,7 +2553,7 @@ + * + * \note_accuracy_double + */ +-extern __DEVICE_FUNCTIONS_DECL__ __device_builtin__ double sinpi(double x); ++extern __DEVICE_FUNCTIONS_DECL__ __device_builtin__ double sinpi(double x) noexcept (true); + /** + * \ingroup CUDA_MATH_SINGLE + * \brief Calculate the sine of the input argument +@@ -2576,7 +2576,7 @@ + * + * \note_accuracy_single + */ +-extern __DEVICE_FUNCTIONS_DECL__ __device_builtin__ float sinpif(float x); ++extern __DEVICE_FUNCTIONS_DECL__ __device_builtin__ float sinpif(float x) noexcept (true); + /** + * \ingroup CUDA_MATH_DOUBLE + * \brief Calculate the cosine of the input argument +@@ -2598,7 +2598,7 @@ + * + * \note_accuracy_double + */ +-extern __DEVICE_FUNCTIONS_DECL__ __device_builtin__ double cospi(double x); ++extern __DEVICE_FUNCTIONS_DECL__ __device_builtin__ double cospi(double x) noexcept (true); + /** + * \ingroup CUDA_MATH_SINGLE + * \brief Calculate the cosine of the input argument +@@ -2620,7 +2620,7 @@ + * + * \note_accuracy_single + */ +-extern __DEVICE_FUNCTIONS_DECL__ __device_builtin__ float cospif(float x); ++extern __DEVICE_FUNCTIONS_DECL__ __device_builtin__ float cospif(float x) noexcept (true); + /** + * \ingroup CUDA_MATH_DOUBLE + * \brief Calculate the sine and cosine of the first input argument +@@ -5982,13 +5982,13 @@ + #pragma warning (disable : 4211) + + #endif /* _WIN32 */ + +-__func__(double rsqrt(double a)); ++__func__(double rsqrt(double a) noexcept (true)); + + __func__(double rcbrt(double a)); + +-__func__(double sinpi(double a)); ++__func__(double sinpi(double a) noexcept (true)); + +-__func__(double cospi(double a)); ++__func__(double cospi(double a) noexcept (true)); + + __func__(void sincospi(double a, double *sptr, double *cptr)); +@@ -6004,10 +6004,10 @@ + __func__(double erfcx(double a)); + +-__func__(float rsqrtf(float a)); ++__func__(float rsqrtf(float a) noexcept (true)); + + __func__(float rcbrtf(float a)); + +-__func__(float sinpif(float a)); ++__func__(float sinpif(float a) noexcept (true)); + +-__func__(float cospif(float a)); ++__func__(float cospif(float a) noexcept (true)); + + __func__(void sincospif(float a, float *sptr, float *cptr)); diff --git a/packaging/sunshine.rb b/packaging/sunshine.rb index 80e15f3481c..159778a86b8 100644 --- a/packaging/sunshine.rb +++ b/packaging/sunshine.rb @@ -1,6 +1,6 @@ require "language/node" -class @PROJECT_NAME@ < Formula +class Sunshine < Formula # conflicts_with "sunshine", because: "sunshine and sunshine-beta cannot be installed at the same time" desc "@PROJECT_DESCRIPTION@" homepage "@PROJECT_HOMEPAGE_URL@" @@ -29,35 +29,18 @@ class @PROJECT_NAME@ < Formula depends_on "cmake" => :build depends_on "doxygen" => :build depends_on "graphviz" => :build - depends_on "ninja" => :build depends_on "node" => :build depends_on "pkgconf" => :build depends_on "curl" depends_on "miniupnpc" depends_on "openssl" depends_on "opus" + depends_on "boost" => :recommended depends_on "icu4c" => :recommended on_linux do - # the "build" dependencies are for libayatana-appindicator - depends_on "at-spi2-core" => :build - depends_on "cairo" => :build - depends_on "fontconfig" => :build - depends_on "freetype" => :build - depends_on "fribidi" => :build - depends_on "gettext" => :build - depends_on "gobject-introspection" => :build - depends_on "graphite2" => :build - depends_on "gtk+3" => :build - depends_on "harfbuzz" => :build - depends_on "intltool" => :build - depends_on "libepoxy" => :build - depends_on "libxdamage" => :build - depends_on "libxkbcommon" => :build - depends_on "pango" => :build - depends_on "perl" => :build - depends_on "pixman" => :build depends_on "avahi" + depends_on "gnu-which" depends_on "libayatana-appindicator" depends_on "libcap" depends_on "libdrm" @@ -76,128 +59,16 @@ class @PROJECT_NAME@ < Formula depends_on "pulseaudio" depends_on "systemd" depends_on "wayland" + end - # resources that do not have brew packages - resource "libayatana-appindicator" do - url "https://github.com/AyatanaIndicators/libayatana-appindicator/archive/refs/tags/0.5.94.tar.gz" - sha256 "884a6bc77994c0b58c961613ca4c4b974dc91aa0f804e70e92f38a542d0d0f90" - end - - resource "libdbusmenu" do - url "https://launchpad.net/libdbusmenu/16.04/16.04.0/+download/libdbusmenu-16.04.0.tar.gz" - sha256 "b9cc4a2acd74509435892823607d966d424bd9ad5d0b00938f27240a1bfa878a" - - patch 'From 729546c51806a1b3ea6cb6efb7a115b1baa811f1 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Stefan=20Br=C3=BCns?= -Date: Mon, 18 Nov 2019 19:58:53 +0100 -Subject: [PATCH 1/1] Fix HAVE_VALGRIND AM_CONDITIONAL - -The AM_CONDITIONAL should also be run with --disable-tests, otherwise -HAVE_VALGRIND is undefined. ---- - configure | 4 ++-- - configure.ac | 2 +- - 2 files changed, 3 insertions(+), 3 deletions(-) - -diff --git a/configure b/configure -index 831a3bb..8913b9b 100644 ---- a/configure -+++ b/configure -@@ -14801,6 +14801,8 @@ else - { $as_echo "$as_me:${as_lineno-$LINENO}: result: yes" >&5 - $as_echo "yes" >&6; } - have_valgrind=yes -+fi -+ - fi - if test "x$have_valgrind" = "xyes"; then - HAVE_VALGRIND_TRUE= -@@ -14811,8 +14813,6 @@ else - fi - - --fi -- - - - -diff --git a/configure.ac b/configure.ac -index ace54d1..cbd38a6 100644 ---- a/configure.ac -+++ b/configure.ac -@@ -120,8 +120,8 @@ PKG_CHECK_MODULES(DBUSMENUTESTS, json-glib-1.0 >= $JSON_GLIB_REQUIRED_VERSION - [have_tests=yes] - ) - PKG_CHECK_MODULES(DBUSMENUTESTSVALGRIND, valgrind, have_valgrind=yes, have_valgrind=no) --AM_CONDITIONAL([HAVE_VALGRIND], [test "x$have_valgrind" = "xyes"]) - ]) -+AM_CONDITIONAL([HAVE_VALGRIND], [test "x$have_valgrind" = "xyes"]) - - AC_SUBST(DBUSMENUTESTS_CFLAGS) - AC_SUBST(DBUSMENUTESTS_LIBS) --- -2.46.2 - - -' - end - - resource "ayatana-ido" do - url "https://github.com/AyatanaIndicators/ayatana-ido/archive/refs/tags/0.10.4.tar.gz" - sha256 "bd59abd5f1314e411d0d55ce3643e91cef633271f58126be529de5fb71c5ab38" - - patch 'From 8a09e6ad33c58c017c0c8fd756da036fc39428ea Mon Sep 17 00:00:00 2001 -From: Alexander Koskovich -Date: Sun, 29 Sep 2024 13:47:54 -0400 -Subject: [PATCH 1/1] Make introspection configurable - ---- - CMakeLists.txt | 1 + - src/CMakeLists.txt | 4 ++++ - 2 files changed, 5 insertions(+) - -diff --git a/CMakeLists.txt b/CMakeLists.txt -index 0e13fcd..f3e9ec0 100644 ---- a/CMakeLists.txt -+++ b/CMakeLists.txt -@@ -12,6 +12,7 @@ endif(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) - option(ENABLE_TESTS "Enable all tests and checks" OFF) - option(ENABLE_COVERAGE "Enable coverage reports (includes enabling all tests and checks)" OFF) - option(ENABLE_WERROR "Treat all build warnings as errors" OFF) -+option(ENABLE_INTROSPECTION "Enable introspection" ON) - - if(ENABLE_COVERAGE) - set(ENABLE_TESTS ON) -diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt -index 5b3638d..aca9481 100644 ---- a/src/CMakeLists.txt -+++ b/src/CMakeLists.txt -@@ -108,6 +108,8 @@ install(TARGETS "ayatana-ido3-0.4" LIBRARY DESTINATION "${CMAKE_INSTALL_FULL_LIB - - # AyatanaIdo3-0.4.gir - -+if (ENABLE_INTROSPECTION) -+ - find_package(GObjectIntrospection REQUIRED QUIET) - - if (INTROSPECTION_FOUND) -@@ -183,3 +185,5 @@ if (INTROSPECTION_FOUND) - endif () - - endif () -+ -+endif () --- -2.46.2 - - -' - end + fails_with :clang do + build 1400 + cause "Requires C++23 support" + end - resource "libayatana-indicator" do - url "https://github.com/AyatanaIndicators/libayatana-indicator/archive/refs/tags/0.9.4.tar.gz" - sha256 "a18d3c682e29afd77db24366f8475b26bda22b0e16ff569a2ec71cd6eb4eac95" - end + fails_with :gcc do + version "12" # fails with GCC 12.x and earlier + cause "Requires C++23 support" end def install @@ -247,15 +118,6 @@ def install end args << "-DCUDA_FAIL_ON_MISSING=OFF" if OS.linux? - args << "-DSUNSHINE_ENABLE_TRAY=OFF" if OS.mac? - - system "cmake", "-S", ".", "-B", "build", "-G", "Unix Makefiles", - *std_cmake_args, - *args - - system "make", "-C", "build" - system "make", "-C", "build", "install" - bin.install "build/tests/test_sunshine" system "cmake", "-S", ".", "-B", "build", "-G", "Unix Makefiles", *std_cmake_args, diff --git a/scripts/linux_build.sh b/scripts/linux_build.sh index abab475414b..2c10065fcfc 100755 --- a/scripts/linux_build.sh +++ b/scripts/linux_build.sh @@ -1,8 +1,16 @@ #!/bin/bash set -e +# Version requirements - centralized for easy maintenance +cmake_min="3.25.0" +target_cmake_version="3.30.1" +doxygen_min="1.10.0" +_doxygen_min="${doxygen_min//\./_}" # Convert dots to underscores for URL +doxygen_max="1.12.0" + # Default value for arguments appimage_build=0 +cuda_patches=0 num_processors=$(nproc) publisher_name="Third Party Publisher" publisher_website="" @@ -13,6 +21,46 @@ skip_libva=0 skip_package=0 sudo_cmd="sudo" ubuntu_test_repo=0 +step="all" + +# Reusable function to detect nvcc path +function detect_nvcc_path() { + local nvcc_path="" + + # First check for system-installed CUDA + nvcc_path=$(command -v nvcc 2>/dev/null) || true + if [ -n "$nvcc_path" ]; then + echo "$nvcc_path" + return 0 + fi + + # Then check for locally installed CUDA in build directory + if [ -f "${build_dir}/cuda/bin/nvcc" ]; then + echo "${build_dir}/cuda/bin/nvcc" + return 0 + fi + + # No CUDA found + return 1 +} + +# Reusable function to setup NVM environment +function setup_nvm_environment() { + # Only setup NVM if it should be used for this distro + if [ "$nvm_node" == 1 ]; then + # Check if NVM is installed and source it + if [ -f "$HOME/.nvm/nvm.sh" ]; then + # shellcheck source=/dev/null + source "$HOME/.nvm/nvm.sh" + # Use the default node version installed by NVM + nvm use default 2>/dev/null || nvm use node 2>/dev/null || true + echo "Using NVM Node.js version: $(node --version 2>/dev/null || echo 'not available')" + echo "Using NVM npm version: $(npm --version 2>/dev/null || echo 'not available')" + else + echo "NVM not found, using system Node.js if available" + fi + fi +} function _usage() { local exit_code=$1 @@ -28,6 +76,7 @@ Options: -h, --help Display this help message. -s, --sudo-off Disable sudo command. --appimage-build Compile for AppImage, this will not create the AppImage, just the executable. + --cuda-patches Apply cuda patches. --num-processors The number of processors to use for compilation. Default is the value of 'nproc'. --publisher-name The name of the publisher (not developer) of the application. --publisher-website The URL of the publisher's website. @@ -38,6 +87,16 @@ Options: --skip-libva Skip libva installation. This will automatically be enabled if passing --appimage-build. --skip-package Skip creating DEB, or RPM package. --ubuntu-test-repo Install ppa:ubuntu-toolchain-r/test repo on Ubuntu. + --step Which step(s) to run: deps, cmake, validation, build, package, cleanup, or all (default: all) + +Steps: + deps Install dependencies only + cmake Run cmake configure only + validation Run validation commands only + build Build the project only + package Create packages only + cleanup Cleanup alternatives and backups only + all Run all steps (default) EOF exit "$exit_code" @@ -55,6 +114,9 @@ while getopts ":hs-:" opt; do appimage_build=1 skip_libva=1 ;; + cuda-patches) + cuda_patches=1 + ;; num-processors=*) num_processors="${OPTARG#*=}" ;; @@ -73,6 +135,9 @@ while getopts ":hs-:" opt; do skip-package) skip_package=1 ;; sudo-off) sudo_cmd="" ;; ubuntu-test-repo) ubuntu_test_repo=1 ;; + step=*) + step="${OPTARG#*=}" + ;; *) echo "Invalid option: --${OPTARG}" 1>&2 _usage 1 @@ -147,6 +212,7 @@ function add_debian_based_deps() { "cmake" "desktop-file-utils" "doxygen" + "file" "flex" # required if we need to compile doxygen "gcc-${gcc_version}" "g++-${gcc_version}" @@ -163,6 +229,8 @@ function add_debian_based_deps() { "libopus-dev" "libpulse-dev" "libssl-dev" + "libsystemd-dev" + "libudev-dev" "libwayland-dev" # Wayland "libx11-dev" # X11 "libxcb-shm0-dev" # X11 @@ -173,6 +241,7 @@ function add_debian_based_deps() { "libxtst-dev" # X11 "ninja-build" "npm" # web-ui + "systemd" "udev" "wget" # necessary for cuda install with `run` file "xvfb" # necessary for headless unit testing @@ -185,19 +254,24 @@ function add_debian_based_deps() { fi } +function add_test_ppa() { + if [ "$ubuntu_test_repo" == 1 ]; then + $package_install_command "software-properties-common" + ${sudo_cmd} add-apt-repository ppa:ubuntu-toolchain-r/test -y + fi +} + function add_debian_deps() { + add_test_ppa add_debian_based_deps dependencies+=( "libayatana-appindicator3-dev" + "systemd-dev" ) } function add_ubuntu_deps() { - if [ "$ubuntu_test_repo" == 1 ]; then - # allow newer gcc - ${sudo_cmd} add-apt-repository ppa:ubuntu-toolchain-r/test -y - fi - + add_test_ppa add_debian_based_deps dependencies+=( "libappindicator3-dev" @@ -252,16 +326,14 @@ function add_fedora_deps() { } function install_cuda() { - nvcc_path=$(command -v nvcc 2>/dev/null) || true - if [ -n "$nvcc_path" ]; then - echo "found system cuda" + # Check if CUDA is already available + if detect_nvcc_path > /dev/null 2>&1; then return fi - # check if we need to install cuda - if [ -f "${build_dir}/cuda/bin/nvcc" ]; then - nvcc_path="${build_dir}/cuda/bin/nvcc" - echo "found local cuda" - return + + local cuda_override_arg="" + if [ "$distro" == "fedora" ]; then + cuda_override_arg="--override" fi local cuda_prefix="https://developer.download.nvidia.com/compute/cuda/" @@ -295,9 +367,26 @@ function install_cuda() { echo "cuda url: ${url}" wget "$url" --progress=bar:force:noscroll -q --show-progress -O "${build_dir}/cuda.run" chmod a+x "${build_dir}/cuda.run" - "${build_dir}/cuda.run" --silent --toolkit --toolkitpath="${build_dir}/cuda" --no-opengl-libs --no-man-page --no-drm + "${build_dir}/cuda.run" --silent --toolkit --toolkitpath="${build_dir}/cuda" --no-opengl-libs --no-man-page --no-drm "$cuda_override_arg" rm "${build_dir}/cuda.run" - nvcc_path="${build_dir}/cuda/bin/nvcc" + + # run cuda patches + if [ "$cuda_patches" == 1 ]; then + echo "Applying CUDA patches" + local patch_dir="${script_dir}/../packaging/linux/patches/${architecture}" + if [ -d "$patch_dir" ]; then + for patch in "$patch_dir"/*.patch; do + echo "Applying patch: $patch" + patch -p2 \ + --backup \ + --directory="${build_dir}/cuda" \ + --verbose \ + < "$patch" + done + else + echo "No patches found for architecture: $architecture" + fi + fi } function check_version() { @@ -334,36 +423,8 @@ if [[ "$(printf '%s\n' "$installed_version" "$min_version" | sort -V | head -n1) fi } -function run_install() { - # prepare CMAKE args - cmake_args=( - "-B=build" - "-G=Ninja" - "-S=." - "-DBUILD_WERROR=ON" - "-DCMAKE_BUILD_TYPE=Release" - "-DCMAKE_INSTALL_PREFIX=/usr" - "-DSUNSHINE_ASSETS_DIR=share/sunshine" - "-DSUNSHINE_EXECUTABLE_PATH=/usr/bin/sunshine" - "-DSUNSHINE_ENABLE_WAYLAND=ON" - "-DSUNSHINE_ENABLE_X11=ON" - "-DSUNSHINE_ENABLE_DRM=ON" - ) - - if [ "$appimage_build" == 1 ]; then - cmake_args+=("-DSUNSHINE_BUILD_APPIMAGE=ON") - fi - - # Publisher metadata - if [ -n "$publisher_name" ]; then - cmake_args+=("-DSUNSHINE_PUBLISHER_NAME='${publisher_name}'") - fi - if [ -n "$publisher_website" ]; then - cmake_args+=("-DSUNSHINE_PUBLISHER_WEBSITE='${publisher_website}'") - fi - if [ -n "$publisher_issue_url" ]; then - cmake_args+=("-DSUNSHINE_PUBLISHER_ISSUE_URL='${publisher_issue_url}'") - fi +function run_step_deps() { + echo "Running step: Install dependencies" # Update the package list $package_update_command @@ -386,37 +447,11 @@ function run_install() { # shellcheck source=/dev/null source ~/.bashrc - gcc_alternative_files=( - "gcc" - "g++" - "gcov" - "gcc-ar" - "gcc-ranlib" - ) - #set gcc version based on distros - if [ "$distro" == "arch" ]; then - export CC=gcc-14 - export CXX=g++-14 - elif [ "$distro" == "debian" ] || [ "$distro" == "ubuntu" ]; then - for file in "${gcc_alternative_files[@]}"; do - file_path="/etc/alternatives/$file" - if [ -e "$file_path" ]; then - ${sudo_cmd} mv "$file_path" "$file_path.bak" - fi - done - - ${sudo_cmd} update-alternatives --install \ - /usr/bin/gcc gcc /usr/bin/gcc-${gcc_version} 100 \ - --slave /usr/bin/g++ g++ /usr/bin/g++-${gcc_version} \ - --slave /usr/bin/gcov gcov /usr/bin/gcov-${gcc_version} \ - --slave /usr/bin/gcc-ar gcc-ar /usr/bin/gcc-ar-${gcc_version} \ - --slave /usr/bin/gcc-ranlib gcc-ranlib /usr/bin/gcc-ranlib-${gcc_version} - fi + export CC=gcc-${gcc_version} + export CXX=g++-${gcc_version} # compile cmake if the version is too low - cmake_min="3.25.0" - target_cmake_version="3.30.1" if ! check_version "cmake" "$cmake_min" "inf"; then cmake_prefix="https://github.com/Kitware/CMake/releases/download/v" if [ "$architecture" == "x86_64" ]; then @@ -433,9 +468,6 @@ function run_install() { fi # compile doxygen if version is too low - doxygen_min="1.10.0" - _doxygen_min="1_10_0" - doxygen_max="1.12.0" if ! check_version "doxygen" "$doxygen_min" "$doxygen_max"; then if [ "${SUNSHINE_COMPILE_DOXYGEN}" == "true" ]; then echo "Compiling doxygen" @@ -451,7 +483,7 @@ function run_install() { popd else echo "Doxygen version not in range, skipping docs" - cmake_args+=("-DBUILD_DOCS=OFF") + # Note: cmake_args will be set in cmake step fi fi @@ -470,8 +502,69 @@ function run_install() { # run the cuda install if [ "$skip_cuda" == 0 ]; then install_cuda + fi +} + +function run_step_cmake() { + echo "Running step: CMake configure" + + # Setup NVM environment if needed (for web UI builds) + setup_nvm_environment + + # Detect CUDA path using the reusable function + nvcc_path="" + if [ "$skip_cuda" == 0 ]; then + nvcc_path=$(detect_nvcc_path) + fi + + #set gcc version based on distros + export CC=gcc-${gcc_version} + export CXX=g++-${gcc_version} + + # prepare CMAKE args + cmake_args=( + "-B=build" + "-G=Ninja" + "-S=." + "-DBUILD_WERROR=ON" + "-DCMAKE_BUILD_TYPE=Release" + "-DCMAKE_INSTALL_PREFIX=/usr" + "-DSUNSHINE_ASSETS_DIR=share/sunshine" + "-DSUNSHINE_EXECUTABLE_PATH=/usr/bin/sunshine" + "-DSUNSHINE_ENABLE_WAYLAND=ON" + "-DSUNSHINE_ENABLE_X11=ON" + "-DSUNSHINE_ENABLE_DRM=ON" + ) + + if [ "$appimage_build" == 1 ]; then + cmake_args+=("-DSUNSHINE_BUILD_APPIMAGE=ON") + fi + + # Publisher metadata + if [ -n "$publisher_name" ]; then + cmake_args+=("-DSUNSHINE_PUBLISHER_NAME='${publisher_name}'") + fi + if [ -n "$publisher_website" ]; then + cmake_args+=("-DSUNSHINE_PUBLISHER_WEBSITE='${publisher_website}'") + fi + if [ -n "$publisher_issue_url" ]; then + cmake_args+=("-DSUNSHINE_PUBLISHER_ISSUE_URL='${publisher_issue_url}'") + fi + + # Handle doxygen docs flag + if ! check_version "doxygen" "$doxygen_min" "$doxygen_max"; then + if [ "${SUNSHINE_COMPILE_DOXYGEN}" != "true" ]; then + cmake_args+=("-DBUILD_DOCS=OFF") + fi + fi + + # Handle CUDA + if [ "$skip_cuda" == 0 ]; then cmake_args+=("-DSUNSHINE_ENABLE_CUDA=ON") - cmake_args+=("-DCMAKE_CUDA_COMPILER:PATH=$nvcc_path") + if [ -n "$nvcc_path" ]; then + cmake_args+=("-DCMAKE_CUDA_COMPILER:PATH=$nvcc_path") + cmake_args+=("-DCMAKE_CUDA_HOST_COMPILER=gcc-${gcc_version}") + fi else cmake_args+=("-DSUNSHINE_ENABLE_CUDA=OFF") fi @@ -481,6 +574,10 @@ function run_install() { echo "cmake args:" echo "${cmake_args[@]}" cmake "${cmake_args[@]}" +} + +function run_step_validation() { + echo "Running step: Validation" # Run appstream validation, etc. appstreamcli validate "build/dev.lizardbyte.app.Sunshine.metainfo.xml" @@ -489,9 +586,20 @@ function run_install() { if [ "$appimage_build" == 0 ]; then desktop-file-validate "build/dev.lizardbyte.app.Sunshine.terminal.desktop" fi +} + +function run_step_build() { + echo "Running step: Build" + + # Setup NVM environment if needed (for web UI builds) + setup_nvm_environment # Build the project ninja -C "build" +} + +function run_step_package() { + echo "Running step: Package" # Create the package if [ "$skip_package" == 0 ]; then @@ -501,19 +609,12 @@ function run_install() { cpack -G RPM --config ./build/CPackConfig.cmake fi fi +} - if [ "$skip_cleanup" == 0 ]; then - # Restore the original gcc alternatives - if [ "$distro" == "debian" ] || [ "$distro" == "ubuntu" ]; then - for file in "${gcc_alternative_files[@]}"; do - if [ -e "/etc/alternatives/$file.bak" ]; then - ${sudo_cmd} mv "/etc/alternatives/$file.bak" "/etc/alternatives/$file" - else - ${sudo_cmd} rm "/etc/alternatives/$file" - fi - done - fi +function run_step_cleanup() { + echo "Running step: Cleanup" + if [ "$skip_cleanup" == 0 ]; then # restore the math-vector.h file if [ "$architecture" == "aarch64" ] && [ -n "$math_vector_file" ]; then ${sudo_cmd} mv -f "$math_vector_file.bak" "$math_vector_file" @@ -521,6 +622,42 @@ function run_install() { fi } +function run_install() { + case "$step" in + deps) + run_step_deps + ;; + cmake) + run_step_cmake + ;; + validation) + run_step_validation + ;; + build) + run_step_build + ;; + package) + run_step_package + ;; + cleanup) + run_step_cleanup + ;; + all) + run_step_deps + run_step_cmake + run_step_validation + run_step_build + run_step_package + run_step_cleanup + ;; + *) + echo "Invalid step: $step" + echo "Valid steps are: deps, cmake, validation, build, package, cleanup, all" + exit 1 + ;; + esac +} + # Determine the OS and call the appropriate function cat /etc/os-release @@ -536,27 +673,26 @@ elif grep -q "Debian GNU/Linux 12 (bookworm)" /etc/os-release; then version="12" package_update_command="${sudo_cmd} apt-get update" package_install_command="${sudo_cmd} apt-get install -y" - cuda_version="12.0.0" - cuda_build="525.60.13" - gcc_version="12" - nvm_node=0 -elif grep -q "PLATFORM_ID=\"platform:f40\"" /etc/os-release; then - distro="fedora" - version="40" - package_update_command="${sudo_cmd} dnf update -y" - package_install_command="${sudo_cmd} dnf install -y" - cuda_version=12.6.3 - cuda_build=560.35.05 + cuda_version="12.9.1" + cuda_build="575.57.08" gcc_version="13" nvm_node=0 - dev_tools_group="Development Tools" +elif grep -q "Debian GNU/Linux 13 (trixie)" /etc/os-release; then + distro="debian" + version="13" + package_update_command="${sudo_cmd} apt-get update" + package_install_command="${sudo_cmd} apt-get install -y" + cuda_version="12.9.1" + cuda_build="575.57.08" + gcc_version="14" + nvm_node=0 elif grep -q "PLATFORM_ID=\"platform:f41\"" /etc/os-release; then distro="fedora" version="41" package_update_command="${sudo_cmd} dnf update -y" package_install_command="${sudo_cmd} dnf install -y" - cuda_version=12.6.3 - cuda_build=560.35.05 + cuda_version="12.9.1" + cuda_build="575.57.08" gcc_version="13" nvm_node=0 dev_tools_group="development-tools" @@ -565,8 +701,8 @@ elif grep -q "PLATFORM_ID=\"platform:f42\"" /etc/os-release; then version="42" package_update_command="${sudo_cmd} dnf update -y" package_install_command="${sudo_cmd} dnf install -y" - cuda_version=12.8.1 - cuda_build=570.124.06 + cuda_version="12.9.1" + cuda_build="575.57.08" gcc_version="14" nvm_node=0 dev_tools_group="development-tools" @@ -575,27 +711,27 @@ elif grep -q "Ubuntu 22.04" /etc/os-release; then version="22.04" package_update_command="${sudo_cmd} apt-get update" package_install_command="${sudo_cmd} apt-get install -y" - cuda_version="11.8.0" - cuda_build="520.61.05" - gcc_version="11" + cuda_version="12.9.1" + cuda_build="575.57.08" + gcc_version="13" nvm_node=1 elif grep -q "Ubuntu 24.04" /etc/os-release; then distro="ubuntu" version="24.04" package_update_command="${sudo_cmd} apt-get update" package_install_command="${sudo_cmd} apt-get install -y" - cuda_version="11.8.0" - cuda_build="520.61.05" - gcc_version="11" - nvm_node=0 + cuda_version="12.9.1" + cuda_build="575.57.08" + gcc_version="14" + nvm_node=1 elif grep -q "Ubuntu 25.04" /etc/os-release; then distro="ubuntu" version="25.04" package_update_command="${sudo_cmd} apt-get update" package_install_command="${sudo_cmd} apt-get install -y" - cuda_version="11.8.0" - cuda_build="520.61.05" - gcc_version="11" + cuda_version="12.9.1" + cuda_build="575.57.08" + gcc_version="14" nvm_node=0 else echo "Unsupported Distro or Version" @@ -613,9 +749,18 @@ if [ "$architecture" != "x86_64" ] && [ "$architecture" != "aarch64" ]; then exit 1 fi +# export variables for github actions ci +if [ -f "$GITHUB_ENV" ]; then + { + echo "CC=gcc-${gcc_version}" + echo "CXX=g++-${gcc_version}" + echo "GCC_VERSION=${gcc_version}" + } >> "$GITHUB_ENV" +fi + # get directory of this script script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -build_dir="$script_dir/../build" +build_dir=$(readlink -f "$script_dir/../build") echo "Script Directory: $script_dir" echo "Build Directory: $build_dir" mkdir -p "$build_dir" diff --git a/src/audio.cpp b/src/audio.cpp index 0d287071a25..4d306b48ddd 100644 --- a/src/audio.cpp +++ b/src/audio.cpp @@ -193,7 +193,8 @@ namespace audio { } auto frame_size = config.packetDuration * stream.sampleRate / 1000; - auto mic = control->microphone(stream.mapping, stream.channelCount, stream.sampleRate, frame_size); + bool continuous_audio = config.flags[config_t::CONTINUOUS_AUDIO]; + auto mic = control->microphone(stream.mapping, stream.channelCount, stream.sampleRate, frame_size, continuous_audio); if (!mic) { return; } @@ -230,7 +231,7 @@ namespace audio { BOOST_LOG(info) << "Reinitializing audio capture"sv; mic.reset(); do { - mic = control->microphone(stream.mapping, stream.channelCount, stream.sampleRate, frame_size); + mic = control->microphone(stream.mapping, stream.channelCount, stream.sampleRate, frame_size, continuous_audio); if (!mic) { BOOST_LOG(warning) << "Couldn't re-initialize audio input"sv; } diff --git a/src/audio.h b/src/audio.h index 2afb42e5f70..763cc9886dd 100644 --- a/src/audio.h +++ b/src/audio.h @@ -45,6 +45,7 @@ namespace audio { HIGH_QUALITY, ///< High quality audio HOST_AUDIO, ///< Host audio CUSTOM_SURROUND_PARAMS, ///< Custom surround parameters + CONTINUOUS_AUDIO, ///< Continuous audio MAX_FLAGS ///< Maximum number of flags }; diff --git a/src/config.cpp b/src/config.cpp index 17faacb4070..8e231a1f826 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -554,6 +554,7 @@ namespace config { true, // back as touchpad click enabled (manual DS4 only) true, // client gamepads with motion events are emulated as DS4 true, // client gamepads with touchpads are emulated as DS4 + true, // ds5_inputtino_randomize_mac true, // keyboard enabled true, // mouse enabled @@ -577,6 +578,7 @@ namespace config { "ipv4", // Address family platf::appdata().string() + "/sunshine.log", // log file false, // notify_pre_releases + true, // system_tray {}, // prep commands }; @@ -1043,6 +1045,11 @@ namespace config { // TODO: Android can possibly support this if (!fs::exists(stream.file_apps.c_str())) { fs::copy_file(SUNSHINE_ASSETS_DIR "/apps.json", stream.file_apps); + fs::permissions( + stream.file_apps, + fs::perms::owner_read | fs::perms::owner_write, + fs::perm_options::add + ); } #endif @@ -1216,6 +1223,7 @@ namespace config { bool_f(vars, "ds4_back_as_touchpad_click", input.ds4_back_as_touchpad_click); bool_f(vars, "motion_as_ds4", input.motion_as_ds4); bool_f(vars, "touchpad_as_ds4", input.touchpad_as_ds4); + bool_f(vars, "ds5_inputtino_randomize_mac", input.ds5_inputtino_randomize_mac); bool_f(vars, "mouse", input.mouse); bool_f(vars, "keyboard", input.keyboard); @@ -1227,6 +1235,7 @@ namespace config { bool_f(vars, "native_pen_touch", input.native_pen_touch); bool_f(vars, "notify_pre_releases", sunshine.notify_pre_releases); + bool_f(vars, "system_tray", sunshine.system_tray); int port = sunshine.port; int_between_f(vars, "port"s, port, {1024 + nvhttp::PORT_HTTPS, 65535 - rtsp_stream::RTSP_SETUP_PORT}); @@ -1250,6 +1259,7 @@ namespace config { "en_US"sv, // English (US) "es"sv, // Spanish "fr"sv, // French + "hu"sv, // Hungarian "it"sv, // Italian "ja"sv, // Japanese "ko"sv, // Korean @@ -1260,6 +1270,7 @@ namespace config { "sv"sv, // Swedish "tr"sv, // Turkish "uk"sv, // Ukrainian + "vi"sv, // Vietnamese "zh"sv, // Chinese "zh_TW"sv, // Chinese (Traditional) }); @@ -1421,7 +1432,7 @@ namespace config { if (!service_ctrl::is_service_running()) { // If the service isn't running, relaunch ourselves as admin to start it WCHAR executable[MAX_PATH]; - GetModuleFileNameW(NULL, executable, ARRAYSIZE(executable)); + GetModuleFileNameW(nullptr, executable, ARRAYSIZE(executable)); SHELLEXECUTEINFOW shell_exec_info {}; shell_exec_info.cbSize = sizeof(shell_exec_info); diff --git a/src/config.h b/src/config.h index 287efaf5a0a..7ba1bd23452 100644 --- a/src/config.h +++ b/src/config.h @@ -193,6 +193,7 @@ namespace config { bool ds4_back_as_touchpad_click; bool motion_as_ds4; bool touchpad_as_ds4; + bool ds5_inputtino_randomize_mac; bool keyboard; bool mouse; @@ -255,6 +256,7 @@ namespace config { std::string log_file; bool notify_pre_releases; + bool system_tray; std::vector prep_cmds; }; diff --git a/src/confighttp.cpp b/src/confighttp.cpp index 3495c9f1584..0a4841509a1 100644 --- a/src/confighttp.cpp +++ b/src/confighttp.cpp @@ -8,6 +8,7 @@ // standard includes #include +#include #include #include @@ -701,7 +702,7 @@ namespace confighttp { if (const int max_index = static_cast(apps_node.size()) - 1; max_index < 0) { error = "No applications to delete"; } else { - error = "'index' out of range, max index is "s + std::to_string(max_index); + error = std::format("'index' {} out of range, max index is {}", index, max_index); } bad_request(response, request, error); return; @@ -718,7 +719,7 @@ namespace confighttp { proc::refresh(config::stream.file_apps); output_tree["status"] = true; - output_tree["result"] = "application " + std::to_string(index) + " deleted"; + output_tree["result"] = std::format("application {} deleted", index); send_response(response, output_tree); } catch (std::exception &e) { BOOST_LOG(warning) << "DeleteApp: "sv << e.what(); diff --git a/src/entry_handler.cpp b/src/entry_handler.cpp index 9ac0764959f..8c74127316f 100644 --- a/src/entry_handler.cpp +++ b/src/entry_handler.cpp @@ -4,6 +4,7 @@ */ // standard includes #include +#include #include #include @@ -25,13 +26,11 @@ extern "C" { using namespace std::literals; -void launch_ui() { - std::string url = "https://localhost:" + std::to_string(net::map_port(confighttp::PORT_HTTPS)); - platf::open_url(url); -} - -void launch_ui_with_path(std::string path) { - std::string url = "https://localhost:" + std::to_string(net::map_port(confighttp::PORT_HTTPS)) + path; +void launch_ui(const std::optional &path) { + std::string url = std::format("https://localhost:{}", static_cast(net::map_port(confighttp::PORT_HTTPS))); + if (path) { + url += *path; + } platf::open_url(url); } @@ -192,8 +191,8 @@ namespace service_ctrl { } private: - SC_HANDLE scm_handle = NULL; - SC_HANDLE service_handle = NULL; + SC_HANDLE scm_handle = nullptr; + SC_HANDLE service_handle = nullptr; }; bool is_service_running() { diff --git a/src/entry_handler.h b/src/entry_handler.h index a83fed292cf..8d5a03e7132 100644 --- a/src/entry_handler.h +++ b/src/entry_handler.h @@ -14,19 +14,13 @@ /** * @brief Launch the Web UI. + * @param path Optional path to append to the base URL. * @examples * launch_ui(); + * launch_ui("/pin"); * @examples_end */ -void launch_ui(); - -/** - * @brief Launch the Web UI at a specific endpoint. - * @examples - * launch_ui_with_path("/pin"); - * @examples_end - */ -void launch_ui_with_path(std::string path); +void launch_ui(const std::optional &path = std::nullopt); /** * @brief Functions for handling command line arguments. diff --git a/src/main.cpp b/src/main.cpp index ed0945d8e56..79e770c1be5 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -88,6 +88,33 @@ WINAPI BOOL ConsoleCtrlHandler(DWORD type) { } #endif +#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 +constexpr bool tray_is_enabled = true; +#else +constexpr bool tray_is_enabled = false; +#endif + +void mainThreadLoop(const std::shared_ptr> &shutdown_event) { + bool run_loop = false; + + // Conditions that would require the main thread event loop +#ifndef _WIN32 + run_loop = tray_is_enabled && config::sunshine.system_tray; // On Windows, tray runs in separate thread, so no main loop needed for tray +#endif + + if (!run_loop) { + BOOST_LOG(info) << "No main thread features enabled, skipping event loop"sv; + // Wait for shutdown + shutdown_event->view(); + return; + } + + // Main thread event loop + BOOST_LOG(info) << "Starting main loop"sv; + while (system_tray::process_tray_events() == 0); + BOOST_LOG(info) << "Main loop has exited"sv; +} + int main(int argc, char *argv[]) { lifetime::argv = argv; @@ -157,7 +184,7 @@ int main(int argc, char *argv[]) { BOOST_LOG(error) << "Display device session failed to initialize"sv; } -#ifdef WIN32 +#ifdef _WIN32 // Modify relevant NVIDIA control panel settings if the system has corresponding gpu if (nvprefs_instance.load()) { // Restore global settings to the undo file left by improper termination of sunshine.exe @@ -186,7 +213,7 @@ int main(int argc, char *argv[]) { wnd_class.lpszClassName = "SunshineSessionMonitorClass"; wnd_class.lpfnWndProc = SessionMonitorWindowProc; if (!RegisterClassA(&wnd_class)) { - session_monitor_hwnd_promise.set_value(NULL); + session_monitor_hwnd_promise.set_value(nullptr); BOOST_LOG(error) << "Failed to register session monitor window class"sv << std::endl; return; } @@ -246,11 +273,6 @@ int main(int argc, char *argv[]) { task_pool.start(1); -#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 - // create tray thread and detach it - system_tray::run_tray(); -#endif - // Create signal handler after logging has been initialized auto shutdown_event = mail::man->event(mail::shutdown); on_signal(SIGINT, [&force_shutdown, &display_device_deinit_guard, shutdown_event]() { @@ -263,7 +285,10 @@ int main(int argc, char *argv[]) { }; force_shutdown = task_pool.pushDelayed(task, 10s).task_id; + // Break out of the main loop shutdown_event->raise(true); + system_tray::end_tray(); + display_device_deinit_guard = nullptr; }); @@ -277,7 +302,10 @@ int main(int argc, char *argv[]) { }; force_shutdown = task_pool.pushDelayed(task, 10s).task_id; + // Break out of the main loop shutdown_event->raise(true); + system_tray::end_tray(); + display_device_deinit_guard = nullptr; }); @@ -350,8 +378,21 @@ int main(int argc, char *argv[]) { } #endif - // Wait for shutdown - shutdown_event->view(); + if (tray_is_enabled && config::sunshine.system_tray) { + BOOST_LOG(info) << "Starting system tray"sv; +#ifdef _WIN32 + // TODO: Windows has a weird bug where when running as a service and on the first Windows boot, + // he tray icon would not appear even though Sunshine is running correctly otherwise. + // Restarting the service would allow the icon to appear normally. + // For now we will keep the Windows tray icon on a separate thread. + // Ideally, we would run the system tray on the main thread for all platforms. + system_tray::init_tray_threaded(); +#else + system_tray::init_tray(); +#endif + } + + mainThreadLoop(shutdown_event); httpThread.join(); configThread.join(); @@ -360,12 +401,7 @@ int main(int argc, char *argv[]) { task_pool.stop(); task_pool.join(); - // stop system tray -#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 - system_tray::end_tray(); -#endif - -#ifdef WIN32 +#ifdef _WIN32 // Restore global NVIDIA control panel settings if (nvprefs_instance.owning_undo_file() && nvprefs_instance.load()) { nvprefs_instance.restore_global_profile(); diff --git a/src/nvenc/nvenc_base.cpp b/src/nvenc/nvenc_base.cpp index bcd12ca1e77..c7ddb962d7f 100644 --- a/src/nvenc/nvenc_base.cpp +++ b/src/nvenc/nvenc_base.cpp @@ -5,6 +5,9 @@ // this include #include "nvenc_base.h" +// standard includes +#include + // local includes #include "src/config.h" #include "src/logging.h" @@ -222,6 +225,11 @@ namespace nvenc { init_params.darHeight = encoder_params.height; init_params.frameRateNum = client_config.framerate; init_params.frameRateDen = 1; + if (client_config.framerateX100 > 0) { + AVRational fps = video::framerateX100_to_rational(client_config.framerateX100); + init_params.frameRateNum = fps.num; + init_params.frameRateDen = fps.den; + } 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))) { @@ -427,7 +435,7 @@ namespace nvenc { extra += " two-pass"; } if (config.vbv_percentage_increase > 0 && get_encoder_cap(NV_ENC_CAPS_SUPPORT_CUSTOM_VBV_BUF_SIZE)) { - extra += " vbv+" + std::to_string(config.vbv_percentage_increase); + extra += std::format(" vbv+{}", config.vbv_percentage_increase); } if (encoder_params.rfi) { extra += " rfi"; @@ -439,7 +447,7 @@ namespace nvenc { extra += " spatial-aq"; } if (enc_config.rcParams.enableMinQP) { - extra += " qpmin=" + std::to_string(enc_config.rcParams.minQP.qpInterP); + extra += std::format(" qpmin={}", enc_config.rcParams.minQP.qpInterP); } if (config.insert_filler_data) { extra += " filler-data"; diff --git a/src/nvenc/nvenc_d3d11.cpp b/src/nvenc/nvenc_d3d11.cpp index 74670acdb5e..1b749f925e1 100644 --- a/src/nvenc/nvenc_d3d11.cpp +++ b/src/nvenc/nvenc_d3d11.cpp @@ -12,13 +12,13 @@ namespace nvenc { nvenc_d3d11::nvenc_d3d11(NV_ENC_DEVICE_TYPE device_type): nvenc_base(device_type) { - async_event_handle = CreateEvent(NULL, FALSE, FALSE, NULL); + async_event_handle = CreateEvent(nullptr, FALSE, FALSE, nullptr); } nvenc_d3d11::~nvenc_d3d11() { if (dll) { FreeLibrary(dll); - dll = NULL; + dll = nullptr; } if (async_event_handle) { CloseHandle(async_event_handle); @@ -36,7 +36,7 @@ namespace nvenc { constexpr auto dll_name = "nvEncodeAPI.dll"; #endif - if ((dll = LoadLibraryEx(dll_name, NULL, LOAD_LIBRARY_SEARCH_SYSTEM32))) { + if ((dll = LoadLibraryEx(dll_name, nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32))) { if (auto create_instance = (decltype(NvEncodeAPICreateInstance) *) GetProcAddress(dll, "NvEncodeAPICreateInstance")) { auto new_nvenc = std::make_unique(); new_nvenc->version = min_struct_version(NV_ENCODE_API_FUNCTION_LIST_VER); @@ -55,7 +55,7 @@ namespace nvenc { if (dll) { FreeLibrary(dll); - dll = NULL; + dll = nullptr; } return false; diff --git a/src/nvenc/nvenc_d3d11.h b/src/nvenc/nvenc_d3d11.h index efacb607f65..19facee0218 100644 --- a/src/nvenc/nvenc_d3d11.h +++ b/src/nvenc/nvenc_d3d11.h @@ -39,7 +39,7 @@ namespace nvenc { bool wait_for_async_event(uint32_t timeout_ms) override; private: - HMODULE dll = NULL; + HMODULE dll = nullptr; }; } // namespace nvenc diff --git a/src/nvenc/nvenc_d3d11_on_cuda.cpp b/src/nvenc/nvenc_d3d11_on_cuda.cpp index 02436e696b3..b915b32921c 100644 --- a/src/nvenc/nvenc_d3d11_on_cuda.cpp +++ b/src/nvenc/nvenc_d3d11_on_cuda.cpp @@ -63,7 +63,7 @@ namespace nvenc { constexpr auto dll_name = "nvcuda.dll"; - if ((cuda_functions.dll = LoadLibraryEx(dll_name, NULL, LOAD_LIBRARY_SEARCH_SYSTEM32))) { + if ((cuda_functions.dll = LoadLibraryEx(dll_name, nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32))) { auto load_function = [&](T &location, auto symbol) -> bool { location = (T) GetProcAddress(cuda_functions.dll, symbol); return location != nullptr; diff --git a/src/nvenc/nvenc_d3d11_on_cuda.h b/src/nvenc/nvenc_d3d11_on_cuda.h index 1c912f50a2f..102e18097d1 100644 --- a/src/nvenc/nvenc_d3d11_on_cuda.h +++ b/src/nvenc/nvenc_d3d11_on_cuda.h @@ -56,7 +56,7 @@ namespace nvenc { autopop_context push_context(); - HMODULE dll = NULL; + HMODULE dll = nullptr; const ID3D11DevicePtr d3d_device; ID3D11Texture2DPtr d3d_input_texture; diff --git a/src/nvhttp.cpp b/src/nvhttp.cpp index 226bf5fb784..9e9b626e50e 100644 --- a/src/nvhttp.cpp +++ b/src/nvhttp.cpp @@ -7,6 +7,7 @@ // standard includes #include +#include #include #include @@ -39,6 +40,8 @@ using namespace std::literals; namespace nvhttp { + static constexpr std::string_view EMPTY_PROPERTY_TREE_ERROR_MSG = "Property tree is empty. Probably, control flow got interrupted by an unexpected C++ exception. This is a bug in Sunshine. Moonlight-qt will report Malformed XML (missing root element)."sv; + namespace fs = std::filesystem; namespace pt = boost::property_tree; @@ -154,7 +157,7 @@ namespace nvhttp { std::string get_arg(const args_t &args, const char *name, const char *default_value = nullptr) { auto it = args.find(name); if (it == std::end(args)) { - if (default_value != NULL) { + if (default_value != nullptr) { return std::string(default_value); } @@ -306,6 +309,7 @@ namespace nvhttp { launch_session->enable_sops = util::from_view(get_arg(args, "sops", "0")); launch_session->surround_info = util::from_view(get_arg(args, "surroundAudioInfo", "196610")); launch_session->surround_params = (get_arg(args, "surroundParams", "")); + launch_session->continuous_audio = util::from_view(get_arg(args, "continuousAudio", "0")); launch_session->gcmap = util::from_view(get_arg(args, "gcmap", "0")); launch_session->enable_hdr = util::from_view(get_arg(args, "hdrMode", "0")); @@ -636,7 +640,7 @@ namespace nvhttp { tree.put("root..status_code", 400); tree.put( "root..status_message", - "Pin must be 4 digits, " + std::to_string(pin.size()) + " provided" + std::format("Pin must be 4 digits, {} provided", pin.size()) ); return false; } @@ -757,7 +761,7 @@ namespace nvhttp { tree.put("root.ExternalIP", config::nvhttp.external_ip); tree.put("root.LocalIP", config::nvhttp.external_ip); } - + auto current_appid = proc::proc.running(); tree.put("root.PairStatus", pair_status); tree.put("root.currentgame", current_appid); @@ -819,6 +823,10 @@ namespace nvhttp { auto g = util::fail_guard([&]() { std::ostringstream data; + if (tree.empty()) { + BOOST_LOG(error) << EMPTY_PROPERTY_TREE_ERROR_MSG; + } + pt::write_xml(data, tree); response->write(data.str()); response->close_connection_after_response = true; @@ -901,7 +909,15 @@ namespace nvhttp { } tree.put("root..status_code", 200); - tree.put("root.sessionUrl0", launch_session->rtsp_url_scheme + net::addr_to_url_escaped_string(request->local_endpoint().address()) + ':' + std::to_string(net::map_port(rtsp_stream::RTSP_SETUP_PORT))); + tree.put( + "root.sessionUrl0", + std::format( + "{}{}:{}", + launch_session->rtsp_url_scheme, + net::addr_to_url_escaped_string(request->local_endpoint().address()), + static_cast(net::map_port(rtsp_stream::RTSP_SETUP_PORT)) + ) + ); tree.put("root.gamesession", 1); rtsp_stream::launch_session_raise(launch_session); @@ -917,6 +933,10 @@ namespace nvhttp { auto g = util::fail_guard([&]() { std::ostringstream data; + if (tree.empty()) { + BOOST_LOG(error) << EMPTY_PROPERTY_TREE_ERROR_MSG; + } + pt::write_xml(data, tree); response->write(data.str()); response->close_connection_after_response = true; @@ -983,7 +1003,15 @@ namespace nvhttp { } tree.put("root..status_code", 200); - tree.put("root.sessionUrl0", launch_session->rtsp_url_scheme + net::addr_to_url_escaped_string(request->local_endpoint().address()) + ':' + std::to_string(net::map_port(rtsp_stream::RTSP_SETUP_PORT))); + tree.put( + "root.sessionUrl0", + std::format( + "{}{}:{}", + launch_session->rtsp_url_scheme, + net::addr_to_url_escaped_string(request->local_endpoint().address()), + static_cast(net::map_port(rtsp_stream::RTSP_SETUP_PORT)) + ) + ); tree.put("root.resume", 1); rtsp_stream::launch_session_raise(launch_session); diff --git a/src/platform/common.h b/src/platform/common.h index 28704bb128e..5ba57027221 100644 --- a/src/platform/common.h +++ b/src/platform/common.h @@ -15,7 +15,7 @@ #include #ifndef _WIN32 #include - #include + #include #endif // local includes @@ -554,7 +554,7 @@ namespace platf { public: virtual int set_sink(const std::string &sink) = 0; - virtual std::unique_ptr microphone(const std::uint8_t *mapping, int channels, std::uint32_t sample_rate, std::uint32_t frame_size) = 0; + virtual std::unique_ptr microphone(const std::uint8_t *mapping, int channels, std::uint32_t sample_rate, std::uint32_t frame_size, bool continuous) = 0; /** * @brief Check if the audio sink is available in the system. diff --git a/src/platform/linux/audio.cpp b/src/platform/linux/audio.cpp index 0e53e939b2a..a2df7bb05be 100644 --- a/src/platform/linux/audio.cpp +++ b/src/platform/linux/audio.cpp @@ -440,7 +440,7 @@ namespace platf { return monitor_name; } - std::unique_ptr microphone(const std::uint8_t *mapping, int channels, std::uint32_t sample_rate, std::uint32_t frame_size) override { + std::unique_ptr microphone(const std::uint8_t *mapping, int channels, std::uint32_t sample_rate, std::uint32_t frame_size, bool continuous_audio) override { // Sink choice priority: // 1. Config sink // 2. Last sink swapped to (Usually virtual in this case) diff --git a/src/platform/linux/cuda.cpp b/src/platform/linux/cuda.cpp index 3a17b9cf1fa..5b3bb695b9e 100644 --- a/src/platform/linux/cuda.cpp +++ b/src/platform/linux/cuda.cpp @@ -202,7 +202,7 @@ namespace cuda { return sws.load_ram(img, tex.array) || sws.convert(frame->data[0], frame->data[1], frame->linesize[0], frame->linesize[1], tex_obj(tex), stream.get()); } - int set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) { + int set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) override { if (cuda_t::set_frame(frame, hw_frames_ctx)) { return -1; } @@ -929,7 +929,7 @@ namespace cuda { return platf::capture_e::ok; } - std::unique_ptr make_avcodec_encode_device(platf::pix_fmt_e pix_fmt) { + std::unique_ptr make_avcodec_encode_device(platf::pix_fmt_e pix_fmt) override { return ::cuda::make_avcodec_encode_device(width, height, true); } diff --git a/src/platform/linux/cuda.cu b/src/platform/linux/cuda.cu index 6d288ef8d46..e48dc3a0dc1 100644 --- a/src/platform/linux/cuda.cu +++ b/src/platform/linux/cuda.cu @@ -330,7 +330,7 @@ namespace cuda { } void sws_t::apply_colorspace(const video::sunshine_colorspace_t &colorspace) { - auto color_p = video::color_vectors_from_colorspace(colorspace); + auto color_p = video::color_vectors_from_colorspace(colorspace, true); CU_CHECK_IGNORE(cudaMemcpy(color_matrix.get(), color_p, sizeof(video::color_t), cudaMemcpyHostToDevice), "Couldn't copy color matrix to cuda"); } diff --git a/src/platform/linux/cuda.h b/src/platform/linux/cuda.h index c7f5daac4b6..40957581748 100644 --- a/src/platform/linux/cuda.h +++ b/src/platform/linux/cuda.h @@ -16,8 +16,8 @@ #include "src/video_colorspace.h" namespace platf { - class avcodec_encode_device_t; - class img_t; + struct avcodec_encode_device_t; + struct img_t; } // namespace platf namespace cuda { diff --git a/src/platform/linux/graphics.cpp b/src/platform/linux/graphics.cpp index 245addb6a0a..96cf95d1c9d 100644 --- a/src/platform/linux/graphics.cpp +++ b/src/platform/linux/graphics.cpp @@ -698,7 +698,7 @@ namespace egl { } void sws_t::apply_colorspace(const video::sunshine_colorspace_t &colorspace) { - auto color_p = video::color_vectors_from_colorspace(colorspace); + auto color_p = video::color_vectors_from_colorspace(colorspace, true); std::string_view members[] { util::view(color_p->color_vec_y), @@ -811,7 +811,7 @@ namespace egl { gl::ctx.UseProgram(sws.program[1].handle()); gl::ctx.Uniform1fv(loc_width_i, 1, &width_i); - auto color_p = video::color_vectors_from_colorspace(video::colorspace_e::rec601, false); + auto color_p = video::color_vectors_from_colorspace({video::colorspace_e::rec601, false, 8}, true); std::pair members[] { std::make_pair("color_vec_y", util::view(color_p->color_vec_y)), std::make_pair("color_vec_u", util::view(color_p->color_vec_u)), diff --git a/src/platform/linux/input/inputtino_gamepad.cpp b/src/platform/linux/input/inputtino_gamepad.cpp index 438146317c6..7e782b59b21 100644 --- a/src/platform/linux/input/inputtino_gamepad.cpp +++ b/src/platform/linux/input/inputtino_gamepad.cpp @@ -42,8 +42,15 @@ namespace platf::gamepad { .version = 0x8111}); } - auto create_ds5() { - return inputtino::PS5Joypad::create({.name = "Sunshine PS5 (virtual) pad", .vendor_id = 0x054C, .product_id = 0x0CE6, .version = 0x8111}); + auto create_ds5(int globalIndex) { + std::string device_mac = ""; // Inputtino checks empty() to generate a random MAC + + if (!config::input.ds5_inputtino_randomize_mac && globalIndex >= 0 && globalIndex <= 255) { + // Generate private virtual device MAC based on gamepad globalIndex between 0 (00) and 255 (ff) + device_mac = std::format("02:00:00:00:00:{:02x}", globalIndex); + } + + return inputtino::PS5Joypad::create({.name = "Sunshine PS5 (virtual) pad", .vendor_id = 0x054C, .product_id = 0x0CE6, .version = 0x8111, .device_phys = device_mac, .device_uniq = device_mac}); } int alloc(input_raw_t *raw, const gamepad_id_t &id, const gamepad_arrival_t &metadata, feedback_queue_t feedback_queue) { @@ -138,7 +145,7 @@ namespace platf::gamepad { } case DualSenseWired: { - auto ds5 = create_ds5(); + auto ds5 = create_ds5(id.globalIndex); if (ds5) { (*ds5).set_on_rumble(on_rumble_fn); (*ds5).set_on_led([feedback_queue, idx = id.clientRelativeIndex, gamepad](int r, int g, int b) { @@ -267,7 +274,7 @@ namespace platf::gamepad { return gps; } - auto ds5 = create_ds5(); + auto ds5 = create_ds5(-1); // Index -1 will result in a random MAC virtual device, which is fine for probing auto switchPro = create_switch(); auto xOne = create_xbox_one(); diff --git a/src/platform/linux/misc.cpp b/src/platform/linux/misc.cpp index 9da873372d2..30ca8e08ee4 100644 --- a/src/platform/linux/misc.cpp +++ b/src/platform/linux/misc.cpp @@ -10,14 +10,22 @@ // standard includes #include +#include #include +#include // platform includes #include #include #include +#include #include #include +#include + +#ifdef __FreeBSD__ + #include // For sockaddr_dl, LLADDR, and AF_LINK +#endif // lib includes #include @@ -41,9 +49,19 @@ #define SUNSHINE_GNUC_EXTENSION #endif +#ifndef SOL_IP + #define SOL_IP IPPROTO_IP +#endif +#ifndef SOL_IPV6 + #define SOL_IPV6 IPPROTO_IPV6 +#endif +#ifndef SOL_UDP + #define SOL_UDP IPPROTO_UDP +#endif + using namespace std::literals; namespace fs = std::filesystem; -namespace bp = boost::process; +namespace bp = boost::process::v1; window_system_e window_system; @@ -214,6 +232,40 @@ namespace platf { std::string get_mac_address(const std::string_view &address) { auto ifaddrs = get_ifaddrs(); + +#ifdef __FreeBSD__ + // On FreeBSD, we need to find the interface name first, then look for its AF_LINK entry + std::string interface_name; + for (auto pos = ifaddrs.get(); pos != nullptr; pos = pos->ifa_next) { + if (pos->ifa_addr && address == from_sockaddr(pos->ifa_addr)) { + interface_name = pos->ifa_name; + break; + } + } + + if (!interface_name.empty()) { + // Find the AF_LINK entry for this interface to get MAC address + for (auto pos = ifaddrs.get(); pos != nullptr; pos = pos->ifa_next) { + if (pos->ifa_addr && pos->ifa_addr->sa_family == AF_LINK && + interface_name == pos->ifa_name) { + auto sdl = (struct sockaddr_dl *) pos->ifa_addr; + auto mac = (unsigned char *) LLADDR(sdl); + + // Format MAC address as XX:XX:XX:XX:XX:XX + std::ostringstream mac_stream; + mac_stream << std::hex << std::setfill('0'); + for (int i = 0; i < sdl->sdl_alen; i++) { + if (i > 0) { + mac_stream << ':'; + } + mac_stream << std::setw(2) << (int) mac[i]; + } + return mac_stream.str(); + } + } + } +#else + // On Linux, read MAC address from sysfs for (auto pos = ifaddrs.get(); pos != nullptr; pos = pos->ifa_next) { if (pos->ifa_addr && address == from_sockaddr(pos->ifa_addr)) { std::ifstream mac_file("/sys/class/net/"s + pos->ifa_name + "/address"); @@ -224,6 +276,7 @@ namespace platf { } } } +#endif BOOST_LOG(warning) << "Unable to find MAC address for "sv << address; return "00:00:00:00:00:00"s; @@ -377,7 +430,12 @@ namespace platf { } union { +#ifdef IP_PKTINFO char buf[CMSG_SPACE(sizeof(uint16_t)) + std::max(CMSG_SPACE(sizeof(struct in_pktinfo)), CMSG_SPACE(sizeof(struct in6_pktinfo)))]; +#elif defined(IP_SENDSRCADDR) + // FreeBSD uses IP_SENDSRCADDR with struct in_addr instead of IP_PKTINFO with struct in_pktinfo + char buf[CMSG_SPACE(sizeof(uint16_t)) + std::max(CMSG_SPACE(sizeof(struct in_addr)), CMSG_SPACE(sizeof(struct in6_pktinfo)))]; +#endif struct cmsghdr alignment; } cmbuf = {}; // Must be zeroed for CMSG_NXTHDR() @@ -403,6 +461,7 @@ namespace platf { pktinfo_cm->cmsg_len = CMSG_LEN(sizeof(pktInfo)); memcpy(CMSG_DATA(pktinfo_cm), &pktInfo, sizeof(pktInfo)); } else { +#ifdef IP_PKTINFO struct in_pktinfo pktInfo; struct sockaddr_in saddr_v4 = to_sockaddr(send_info.source_address.to_v4(), 0); @@ -415,6 +474,18 @@ namespace platf { pktinfo_cm->cmsg_type = IP_PKTINFO; pktinfo_cm->cmsg_len = CMSG_LEN(sizeof(pktInfo)); memcpy(CMSG_DATA(pktinfo_cm), &pktInfo, sizeof(pktInfo)); +#elif defined(IP_SENDSRCADDR) + // FreeBSD uses IP_SENDSRCADDR with struct in_addr instead of IP_PKTINFO + struct sockaddr_in saddr_v4 = to_sockaddr(send_info.source_address.to_v4(), 0); + struct in_addr src_addr = saddr_v4.sin_addr; + + cmbuflen += CMSG_SPACE(sizeof(src_addr)); + + pktinfo_cm->cmsg_level = IPPROTO_IP; + pktinfo_cm->cmsg_type = IP_SENDSRCADDR; + pktinfo_cm->cmsg_len = CMSG_LEN(sizeof(src_addr)); + memcpy(CMSG_DATA(pktinfo_cm), &src_addr, sizeof(src_addr)); +#endif } auto const max_iovs_per_msg = send_info.payload_buffers.size() + (send_info.headers ? 1 : 0); @@ -507,8 +578,8 @@ namespace platf { { // If GSO is not supported, use sendmmsg() instead. - struct mmsghdr msgs[send_info.block_count]; - struct iovec iovs[send_info.block_count * (send_info.headers ? 2 : 1)]; + std::vector msgs(send_info.block_count); + std::vector iovs(send_info.block_count * (send_info.headers ? 2 : 1)); int iov_idx = 0; for (size_t i = 0; i < send_info.block_count; i++) { msgs[i].msg_len = 0; @@ -584,7 +655,12 @@ namespace platf { } union { +#ifdef IP_PKTINFO char buf[std::max(CMSG_SPACE(sizeof(struct in_pktinfo)), CMSG_SPACE(sizeof(struct in6_pktinfo)))]; +#elif defined(IP_SENDSRCADDR) + // FreeBSD uses IP_SENDSRCADDR with struct in_addr instead of IP_PKTINFO with struct in_pktinfo + char buf[std::max(CMSG_SPACE(sizeof(struct in_addr)), CMSG_SPACE(sizeof(struct in6_pktinfo)))]; +#endif struct cmsghdr alignment; } cmbuf; @@ -608,6 +684,7 @@ namespace platf { pktinfo_cm->cmsg_len = CMSG_LEN(sizeof(pktInfo)); memcpy(CMSG_DATA(pktinfo_cm), &pktInfo, sizeof(pktInfo)); } else { +#ifdef IP_PKTINFO struct in_pktinfo pktInfo; struct sockaddr_in saddr_v4 = to_sockaddr(send_info.source_address.to_v4(), 0); @@ -620,6 +697,18 @@ namespace platf { pktinfo_cm->cmsg_type = IP_PKTINFO; pktinfo_cm->cmsg_len = CMSG_LEN(sizeof(pktInfo)); memcpy(CMSG_DATA(pktinfo_cm), &pktInfo, sizeof(pktInfo)); +#elif defined(IP_SENDSRCADDR) + // FreeBSD uses IP_SENDSRCADDR with struct in_addr instead of IP_PKTINFO + struct sockaddr_in saddr_v4 = to_sockaddr(send_info.source_address.to_v4(), 0); + struct in_addr src_addr = saddr_v4.sin_addr; + + cmbuflen += CMSG_SPACE(sizeof(src_addr)); + + pktinfo_cm->cmsg_level = IPPROTO_IP; + pktinfo_cm->cmsg_type = IP_SENDSRCADDR; + pktinfo_cm->cmsg_len = CMSG_LEN(sizeof(src_addr)); + memcpy(CMSG_DATA(pktinfo_cm), &src_addr, sizeof(src_addr)); +#endif } struct iovec iovs[2]; @@ -753,6 +842,10 @@ namespace platf { // reset SO_PRIORITY back to 0. // // 6 is the highest priority that can be used without SYS_CAP_ADMIN. +#ifndef SO_PRIORITY + // FreeBSD doesn't support SO_PRIORITY, so we skip this + BOOST_LOG(debug) << "SO_PRIORITY not supported on this platform, skipping traffic priority setting"; +#else int priority = data_type == qos_data_type_e::audio ? 6 : 5; if (setsockopt(sockfd, SOL_SOCKET, SO_PRIORITY, &priority, sizeof(priority)) == 0) { // Reset SO_PRIORITY to 0 when QoS is disabled @@ -760,6 +853,7 @@ namespace platf { } else { BOOST_LOG(error) << "Failed to set SO_PRIORITY: "sv << errno; } +#endif return std::make_unique(sockfd, reset_options); } diff --git a/src/platform/linux/vaapi.cpp b/src/platform/linux/vaapi.cpp index 88444d10cbc..e0cc7930dfb 100644 --- a/src/platform/linux/vaapi.cpp +++ b/src/platform/linux/vaapi.cpp @@ -4,6 +4,7 @@ */ // standard includes #include +#include #include #include @@ -190,7 +191,7 @@ namespace va { return VAProfileH264High; } else if (ctx->codec_id == AV_CODEC_ID_HEVC) { switch (ctx->profile) { - case FF_PROFILE_HEVC_REXT: + case AV_PROFILE_HEVC_REXT: switch (av_pix_fmt_desc_get(ctx->sw_pix_fmt)->comp[0].depth) { case 10: return VAProfileHEVCMain444_10; @@ -198,16 +199,16 @@ namespace va { return VAProfileHEVCMain444; } break; - case FF_PROFILE_HEVC_MAIN_10: + case AV_PROFILE_HEVC_MAIN_10: return VAProfileHEVCMain10; - case FF_PROFILE_HEVC_MAIN: + case AV_PROFILE_HEVC_MAIN: return VAProfileHEVCMain; } } else if (ctx->codec_id == AV_CODEC_ID_AV1) { switch (ctx->profile) { - case FF_PROFILE_AV1_HIGH: + case AV_PROFILE_AV1_HIGH: return VAProfileAV1Profile1; - case FF_PROFILE_AV1_MAIN: + case AV_PROFILE_AV1_MAIN: return VAProfileAV1Profile0; } } @@ -574,7 +575,7 @@ namespace va { if (!display) { char string[1024]; - auto bytes = readlink(("/proc/self/fd/" + std::to_string(fd)).c_str(), string, sizeof(string)); + auto bytes = readlink(std::format("/proc/self/fd/{}", fd).c_str(), string, sizeof(string)); std::string_view render_device {string, (std::size_t) bytes}; diff --git a/src/platform/macos/microphone.mm b/src/platform/macos/microphone.mm index 06b9c19a899..fb702916f5d 100644 --- a/src/platform/macos/microphone.mm +++ b/src/platform/macos/microphone.mm @@ -49,7 +49,7 @@ int set_sink(const std::string &sink) override { return 0; } - std::unique_ptr microphone(const std::uint8_t *mapping, int channels, std::uint32_t sample_rate, std::uint32_t frame_size) override { + std::unique_ptr microphone(const std::uint8_t *mapping, int channels, std::uint32_t sample_rate, std::uint32_t frame_size, bool continuous_audio) override { auto mic = std::make_unique(); const char *audio_sink = ""; diff --git a/src/platform/windows/audio.cpp b/src/platform/windows/audio.cpp index 20e600acaa9..98af1e75bc8 100644 --- a/src/platform/windows/audio.cpp +++ b/src/platform/windows/audio.cpp @@ -4,8 +4,11 @@ */ #define INITGUID +// standard includes +#include + // platform includes -#include +#include #include #include #include @@ -168,28 +171,27 @@ namespace { waveformat.SubFormat == KSDATAFORMAT_SUBTYPE_PCM ? "S" : "UNKNOWN"; - result += std::to_string(waveformat.Samples.wValidBitsPerSample) + " " + - std::to_string(waveformat.Format.nSamplesPerSec) + " "; + result += std::format("{} {} ", static_cast(waveformat.Samples.wValidBitsPerSample), static_cast(waveformat.Format.nSamplesPerSec)); switch (waveformat.dwChannelMask) { - case (waveformat_mask_stereo): + case waveformat_mask_stereo: result += "2.0"; break; - case (waveformat_mask_surround51_with_backspeakers): + case waveformat_mask_surround51_with_backspeakers: result += "5.1"; break; - case (waveformat_mask_surround51_with_sidespeakers): + case waveformat_mask_surround51_with_sidespeakers: result += "5.1 (sidespeakers)"; break; - case (waveformat_mask_surround71): + case waveformat_mask_surround71: result += "7.1"; break; default: - result += std::to_string(waveformat.Format.nChannels) + " channels (unrecognized)"; + result += std::format("{} channels (unrecognized)", static_cast(waveformat.Format.nChannels)); break; } @@ -375,7 +377,7 @@ namespace platf::audio { *ppvInterface = (IMMNotificationClient *) this; return S_OK; } else { - *ppvInterface = NULL; + *ppvInterface = nullptr; return E_NOINTERFACE; } } @@ -430,7 +432,11 @@ namespace platf::audio { // Refill the sample buffer if needed while (sample_buf_pos - std::begin(sample_buf) < sample_size) { auto capture_result = _fill_buffer(); - if (capture_result != capture_e::ok) { + if (capture_result == capture_e::timeout && continuous_audio) { + // Write silence to sample_buf + std::fill_n(sample_buf_pos, sample_size, 0.0f); + sample_buf_pos += sample_size; + } else if (capture_result != capture_e::ok) { return capture_result; } } @@ -445,7 +451,7 @@ namespace platf::audio { return capture_e::ok; } - int init(std::uint32_t sample_rate, std::uint32_t frame_size, std::uint32_t channels_out) { + int init(std::uint32_t sample_rate, std::uint32_t frame_size, std::uint32_t channels_out, bool continuous) { audio_event.reset(CreateEventA(nullptr, FALSE, FALSE, nullptr)); if (!audio_event) { BOOST_LOG(error) << "Couldn't create Event handle"sv; @@ -506,6 +512,7 @@ namespace platf::audio { REFERENCE_TIME default_latency; audio_client->GetDevicePeriod(&default_latency, nullptr); default_latency_ms = default_latency / 1000; + continuous_audio = continuous; std::uint32_t frames; status = audio_client->GetBufferSize(&frames); @@ -676,8 +683,9 @@ namespace platf::audio { util::buffer_t sample_buf; float *sample_buf_pos; int channels; + bool continuous_audio; - HANDLE mmcss_task_handle = NULL; + HANDLE mmcss_task_handle = nullptr; }; class audio_control_t: public ::platf::audio_control_t { @@ -759,10 +767,10 @@ namespace platf::audio { return std::nullopt; } - std::unique_ptr microphone(const std::uint8_t *mapping, int channels, std::uint32_t sample_rate, std::uint32_t frame_size) override { + std::unique_ptr microphone(const std::uint8_t *mapping, int channels, std::uint32_t sample_rate, std::uint32_t frame_size, bool continuous_audio) override { auto mic = std::make_unique(); - if (mic->init(sample_rate, frame_size, channels)) { + if (mic->init(sample_rate, frame_size, channels, continuous_audio)) { return nullptr; } diff --git a/src/platform/windows/display.h b/src/platform/windows/display.h index 74c4be7c22c..a1f2b96fe43 100644 --- a/src/platform/windows/display.h +++ b/src/platform/windows/display.h @@ -12,7 +12,7 @@ #include #include #include -#include +#include // local includes #include "src/platform/common.h" @@ -173,6 +173,7 @@ namespace platf::dxgi { int height_before_rotation; int client_frame_rate; + DXGI_RATIONAL client_frame_rate_strict; DXGI_FORMAT capture_format; D3D_FEATURE_LEVEL feature_level; diff --git a/src/platform/windows/display_base.cpp b/src/platform/windows/display_base.cpp index 6698566207f..06df0c54e4c 100644 --- a/src/platform/windows/display_base.cpp +++ b/src/platform/windows/display_base.cpp @@ -121,7 +121,13 @@ namespace platf::dxgi { display->display_refresh_rate = dup_desc.ModeDesc.RefreshRate; double display_refresh_rate_decimal = (double) display->display_refresh_rate.Numerator / display->display_refresh_rate.Denominator; BOOST_LOG(info) << "Display refresh rate [" << display_refresh_rate_decimal << "Hz]"; - BOOST_LOG(info) << "Requested frame rate [" << display->client_frame_rate << "fps]"; + if (display->client_frame_rate_strict.Numerator > 0) { + int num = display->client_frame_rate_strict.Numerator; + int den = display->client_frame_rate_strict.Denominator; + BOOST_LOG(info) << "Requested frame rate [" << num << "/" << den << " exactly " << av_q2d(AVRational {num, den}) << " fps]"; + } else { + BOOST_LOG(info) << "Requested frame rate [" << display->client_frame_rate << "fps]"; + } display->display_refresh_rate_rounded = lround(display_refresh_rate_decimal); return 0; } @@ -196,6 +202,10 @@ namespace platf::dxgi { capture_e display_base_t::capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) { auto adjust_client_frame_rate = [&]() -> DXGI_RATIONAL { + // Use exactly the requested rate if the client sent an X100 value + if (client_frame_rate_strict.Numerator > 0) { + return client_frame_rate_strict; + } // Adjust capture frame interval when display refresh rate is not integral but very close to requested fps. if (display_refresh_rate.Denominator > 1) { DXGI_RATIONAL candidate = display_refresh_rate; @@ -601,12 +611,12 @@ namespace platf::dxgi { LUID val; if (OpenProcessToken(GetCurrentProcess(), flags, &token) && - !!LookupPrivilegeValue(NULL, SE_INC_BASE_PRIORITY_NAME, &val)) { + !!LookupPrivilegeValue(nullptr, SE_INC_BASE_PRIORITY_NAME, &val)) { tp.PrivilegeCount = 1; tp.Privileges[0].Luid = val; tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; - if (!AdjustTokenPrivileges(token, false, &tp, sizeof(tp), NULL, NULL)) { + if (!AdjustTokenPrivileges(token, false, &tp, sizeof(tp), nullptr, nullptr)) { BOOST_LOG(warning) << "Could not set privilege to increase GPU priority"; } } @@ -705,6 +715,12 @@ namespace platf::dxgi { } client_frame_rate = config.framerate; + client_frame_rate_strict = {0, 0}; + if (config.framerateX100 > 0) { + AVRational fps = ::video::framerateX100_to_rational(config.framerateX100); + client_frame_rate_strict = DXGI_RATIONAL {static_cast(fps.num), static_cast(fps.den)}; + } + dxgi::output6_t output6 {}; status = output->QueryInterface(IID_IDXGIOutput6, (void **) &output6); if (SUCCEEDED(status)) { @@ -918,20 +934,20 @@ namespace platf::dxgi { "DXGI_FORMAT_A8P8", "DXGI_FORMAT_B4G4R4A4_UNORM", - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, "DXGI_FORMAT_P208", "DXGI_FORMAT_V208", @@ -1043,8 +1059,9 @@ namespace platf { return {}; } - dxgi::adapter_t adapter; - for (int x = 0; factory->EnumAdapters1(x, &adapter) != DXGI_ERROR_NOT_FOUND; ++x) { + dxgi::adapter_t::pointer adapter_p; + for (int x = 0; factory->EnumAdapters1(x, &adapter_p) != DXGI_ERROR_NOT_FOUND; ++x) { + dxgi::adapter_t adapter {adapter_p}; DXGI_ADAPTER_DESC1 adapter_desc; adapter->GetDesc1(&adapter_desc); diff --git a/src/platform/windows/display_vram.cpp b/src/platform/windows/display_vram.cpp index 7a8b07058d6..cf6b4c43d5c 100644 --- a/src/platform/windows/display_vram.cpp +++ b/src/platform/windows/display_vram.cpp @@ -7,7 +7,7 @@ // platform includes #include -#include +#include extern "C" { #include @@ -457,12 +457,12 @@ namespace platf::dxgi { } void apply_colorspace(const ::video::sunshine_colorspace_t &colorspace) { - auto color_vectors = ::video::color_vectors_from_colorspace(colorspace); + auto color_vectors = ::video::color_vectors_from_colorspace(colorspace, true); if (format == DXGI_FORMAT_AYUV || format == DXGI_FORMAT_R16_UINT || format == DXGI_FORMAT_Y410) { - color_vectors = ::video::new_color_vectors_from_colorspace(colorspace); + color_vectors = ::video::color_vectors_from_colorspace(colorspace, false); } if (!color_vectors) { @@ -775,7 +775,7 @@ namespace platf::dxgi { BOOST_LOG(warning) << "Failed to increase encoding GPU thread priority. Please run application as administrator for optimal performance."; } - auto default_color_vectors = ::video::color_vectors_from_colorspace(::video::colorspace_e::rec601, false); + auto default_color_vectors = ::video::color_vectors_from_colorspace({::video::colorspace_e::rec601, false, 8}, true); if (!default_color_vectors) { BOOST_LOG(error) << "Missing color vectors for Rec. 601"sv; return -1; @@ -1739,7 +1739,7 @@ namespace platf::dxgi { img->data = nullptr; if (img->encoder_texture_handle) { CloseHandle(img->encoder_texture_handle); - img->encoder_texture_handle = NULL; + img->encoder_texture_handle = nullptr; } // Initialize format-dependent fields diff --git a/src/platform/windows/display_wgc.cpp b/src/platform/windows/display_wgc.cpp index 5b31dcae246..15cab763708 100644 --- a/src/platform/windows/display_wgc.cpp +++ b/src/platform/windows/display_wgc.cpp @@ -13,7 +13,7 @@ // Gross hack to work around MINGW-packages#22160 #define ____FIReference_1_boolean_INTERFACE_DEFINED__ -#include +#include #include #include #include @@ -137,6 +137,15 @@ namespace platf::dxgi { } catch (winrt::hresult_error &e) { BOOST_LOG(warning) << "Screen capture may not be fully supported on this device for this release of Windows: failed to disable border around capture area: [0x"sv << util::hex(e.code()).to_string_view() << ']'; } + try { + if (winrt::ApiInformation::IsPropertyPresent(L"Windows.Graphics.Capture.GraphicsCaptureSession", L"MinUpdateInterval")) { + capture_session.MinUpdateInterval(4ms); // 250Hz + } else { + BOOST_LOG(warning) << "Can't set MinUpdateInterval on this version of Windows"; + } + } catch (winrt::hresult_error &e) { + BOOST_LOG(warning) << "Screen capture may be capped to 60fps on this device for this release of Windows: failed to set MinUpdateInterval: [0x"sv << util::hex(e.code()).to_string_view() << ']'; + } try { capture_session.StartCapture(); } catch (winrt::hresult_error &e) { diff --git a/src/platform/windows/input.cpp b/src/platform/windows/input.cpp index 50f8aab8b04..85273609ec5 100644 --- a/src/platform/windows/input.cpp +++ b/src/platform/windows/input.cpp @@ -5,7 +5,7 @@ #define WINVER 0x0A00 // platform includes -#include +#include // standard includes #include @@ -22,13 +22,6 @@ #include "src/logging.h" #include "src/platform/common.h" -#ifdef __MINGW32__ -DECLARE_HANDLE(HSYNTHETICPOINTERDEVICE); -WINUSERAPI HSYNTHETICPOINTERDEVICE WINAPI CreateSyntheticPointerDevice(POINTER_INPUT_TYPE pointerType, ULONG maxCount, POINTER_FEEDBACK_MODE mode); -WINUSERAPI BOOL WINAPI InjectSyntheticPointerInput(HSYNTHETICPOINTERDEVICE device, CONST POINTER_TYPE_INFO *pointerInfo, UINT32 count); -WINUSERAPI VOID WINAPI DestroySyntheticPointerDevice(HSYNTHETICPOINTERDEVICE device); -#endif - namespace platf { using namespace std::literals; @@ -293,7 +286,7 @@ namespace platf { if (gamepad.repeat_task) { task_pool.cancel(gamepad.repeat_task); - gamepad.repeat_task = 0; + gamepad.repeat_task = nullptr; } if (gamepad.gp && vigem_target_is_attached(gamepad.gp.get())) { @@ -1452,7 +1445,7 @@ namespace platf { // Cancel any pending updates. We will requeue one here when we're finished. if (gamepad.repeat_task) { task_pool.cancel(gamepad.repeat_task); - gamepad.repeat_task = 0; + gamepad.repeat_task = nullptr; } if (gamepad.gp && vigem_target_is_attached(gamepad.gp.get())) { @@ -1598,8 +1591,8 @@ namespace platf { uint16_t y = touch.y * 943; uint8_t touchData[] = { (uint8_t) (x & 0xFF), // Low 8 bits of X - (uint8_t) (((x >> 8) & 0x0F) | ((y & 0x0F) << 4)), // High 4 bits of X and low 4 bits of Y - (uint8_t) (((y >> 4) & 0xFF)) // High 8 bits of Y + (uint8_t) ((x >> 8 & 0x0F) | (y & 0x0F) << 4), // High 4 bits of X and low 4 bits of Y + (uint8_t) (y >> 4 & 0xFF) // High 8 bits of Y }; report.sCurrentTouch.bPacketCounter++; diff --git a/src/platform/windows/misc.cpp b/src/platform/windows/misc.cpp index c931b0ad175..a921c3cc144 100644 --- a/src/platform/windows/misc.cpp +++ b/src/platform/windows/misc.cpp @@ -22,13 +22,13 @@ #include #include #include -#include -#include -#include -#include +#include +#include +#include +#include #include -#include -#include +#include +#include #include // clang-format on @@ -117,7 +117,7 @@ namespace platf { std::filesystem::path appdata() { WCHAR sunshine_path[MAX_PATH]; - GetModuleFileNameW(NULL, sunshine_path, _countof(sunshine_path)); + GetModuleFileNameW(nullptr, sunshine_path, _countof(sunshine_path)); return std::filesystem::path {sunshine_path}.remove_filename() / L"config"sv; } @@ -410,16 +410,16 @@ namespace platf { LPPROC_THREAD_ATTRIBUTE_LIST allocate_proc_thread_attr_list(DWORD attribute_count) { SIZE_T size; - InitializeProcThreadAttributeList(NULL, attribute_count, 0, &size); + InitializeProcThreadAttributeList(nullptr, attribute_count, 0, &size); auto list = (LPPROC_THREAD_ATTRIBUTE_LIST) HeapAlloc(GetProcessHeap(), 0, size); - if (list == NULL) { - return NULL; + if (list == nullptr) { + return nullptr; } if (!InitializeProcThreadAttributeList(list, attribute_count, 0, &size)) { HeapFree(GetProcessHeap(), 0, list); - return NULL; + return nullptr; } return list; @@ -518,7 +518,7 @@ namespace platf { // Allocate a process attribute list with space for 2 elements startup_info.lpAttributeList = allocate_proc_thread_attr_list(2); - if (startup_info.lpAttributeList == NULL) { + if (startup_info.lpAttributeList == nullptr) { // If the allocation failed, set ec to an appropriate error code and return the structure ec = std::make_error_code(std::errc::not_enough_memory); return startup_info; @@ -530,7 +530,7 @@ namespace platf { // Populate std handles if the caller gave us a log file to use startup_info.StartupInfo.dwFlags |= STARTF_USESTDHANDLES; - startup_info.StartupInfo.hStdInput = NULL; + startup_info.StartupInfo.hStdInput = nullptr; startup_info.StartupInfo.hStdOutput = log_file_handle; startup_info.StartupInfo.hStdError = log_file_handle; @@ -539,7 +539,7 @@ namespace platf { // // Note: The value we point to here must be valid for the lifetime of the attribute list, // so we need to point into the STARTUPINFO instead of our log_file_variable on the stack. - UpdateProcThreadAttribute(startup_info.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, &startup_info.StartupInfo.hStdOutput, sizeof(startup_info.StartupInfo.hStdOutput), NULL, NULL); + UpdateProcThreadAttribute(startup_info.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, &startup_info.StartupInfo.hStdOutput, sizeof(startup_info.StartupInfo.hStdOutput), nullptr, nullptr); } if (job) { @@ -547,7 +547,7 @@ namespace platf { // // Note: The value we point to here must be valid for the lifetime of the attribute list, // so we take a HANDLE* instead of just a HANDLE to use the caller's stack storage. - UpdateProcThreadAttribute(startup_info.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_JOB_LIST, job, sizeof(*job), NULL, NULL); + UpdateProcThreadAttribute(startup_info.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_JOB_LIST, job, sizeof(*job), nullptr, nullptr); } return startup_info; @@ -555,11 +555,11 @@ namespace platf { /** * @brief This function overrides HKEY_CURRENT_USER and HKEY_CLASSES_ROOT using the provided token. - * @param token The primary token identifying the user to use, or `NULL` to restore original keys. + * @param token The primary token identifying the user to use, or `nullptr` to restore original keys. * @return `true` if the override or restore operation was successful. */ bool override_per_user_predefined_keys(HANDLE token) { - HKEY user_classes_root = NULL; + HKEY user_classes_root = nullptr; if (token) { auto err = RegOpenUserClassesRoot(token, 0, GENERIC_ALL, &user_classes_root); if (err != ERROR_SUCCESS) { @@ -573,14 +573,14 @@ namespace platf { } }); - HKEY user_key = NULL; + HKEY user_key = nullptr; if (token) { impersonate_current_user(token, [&]() { // RegOpenCurrentUser() doesn't take a token. It assumes we're impersonating the desired user. auto err = RegOpenCurrentUser(GENERIC_ALL, &user_key); if (err != ERROR_SUCCESS) { BOOST_LOG(error) << "Failed to open user key for target user: "sv << err; - user_key = NULL; + user_key = nullptr; } }); if (!user_key) { @@ -602,7 +602,7 @@ namespace platf { err = RegOverridePredefKey(HKEY_CURRENT_USER, user_key); if (err != ERROR_SUCCESS) { BOOST_LOG(error) << "Failed to override HKEY_CURRENT_USER: "sv << err; - RegOverridePredefKey(HKEY_CLASSES_ROOT, NULL); + RegOverridePredefKey(HKEY_CLASSES_ROOT, nullptr); return false; } @@ -671,7 +671,7 @@ namespace platf { * @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 token The user token currently being impersonated or `nullptr` 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(). */ @@ -757,7 +757,7 @@ namespace platf { } // Reset per-user keys back to the original value - override_per_user_predefined_keys(NULL); + override_per_user_predefined_keys(nullptr); } if (res != S_OK) { @@ -972,7 +972,7 @@ namespace platf { 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, creation_flags); - ret = CreateProcessAsUserW(user_token, NULL, (LPWSTR) wcmd.c_str(), NULL, NULL, !!(startup_info.StartupInfo.dwFlags & STARTF_USESTDHANDLES), creation_flags, env_block.data(), start_dir.empty() ? NULL : start_dir.c_str(), (LPSTARTUPINFOW) &startup_info, &process_info); + ret = CreateProcessAsUserW(user_token, nullptr, (LPWSTR) wcmd.c_str(), nullptr, nullptr, !!(startup_info.StartupInfo.dwFlags & STARTF_USESTDHANDLES), creation_flags, env_block.data(), start_dir.empty() ? nullptr : start_dir.c_str(), (LPSTARTUPINFOW) &startup_info, &process_info); }); } // Otherwise, launch the process using CreateProcessW() @@ -995,8 +995,8 @@ namespace platf { } std::wstring env_block = create_environment_block(cloned_env); - std::wstring wcmd = resolve_command_string(cmd, start_dir, NULL, creation_flags); - ret = CreateProcessW(NULL, (LPWSTR) wcmd.c_str(), NULL, NULL, !!(startup_info.StartupInfo.dwFlags & STARTF_USESTDHANDLES), creation_flags, env_block.data(), start_dir.empty() ? NULL : start_dir.c_str(), (LPSTARTUPINFOW) &startup_info, &process_info); + std::wstring wcmd = resolve_command_string(cmd, start_dir, nullptr, creation_flags); + ret = CreateProcessW(nullptr, (LPWSTR) wcmd.c_str(), nullptr, nullptr, !!(startup_info.StartupInfo.dwFlags & STARTF_USESTDHANDLES), creation_flags, env_block.data(), start_dir.empty() ? nullptr : start_dir.c_str(), (LPSTARTUPINFOW) &startup_info, &process_info); } // Use the results of the launch to create a bp::child object @@ -1052,7 +1052,7 @@ namespace platf { static std::once_flag load_wlanapi_once_flag; std::call_once(load_wlanapi_once_flag, []() { // wlanapi.dll is not installed by default on Windows Server, so we load it dynamically - HMODULE wlanapi = LoadLibraryExA("wlanapi.dll", NULL, LOAD_LIBRARY_SEARCH_SYSTEM32); + HMODULE wlanapi = LoadLibraryExA("wlanapi.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32); if (!wlanapi) { BOOST_LOG(debug) << "wlanapi.dll is not available on this OS"sv; return; @@ -1129,7 +1129,7 @@ namespace platf { fn_WlanFreeMemory(wlan_interface_list); } else { fn_WlanCloseHandle(wlan_handle, nullptr); - wlan_handle = NULL; + wlan_handle = nullptr; } } } @@ -1200,7 +1200,7 @@ namespace platf { startup_info.StartupInfo.cb = sizeof(startup_info); WCHAR executable[MAX_PATH]; - if (GetModuleFileNameW(NULL, executable, ARRAYSIZE(executable)) == 0) { + if (GetModuleFileNameW(nullptr, executable, ARRAYSIZE(executable)) == 0) { auto winerr = GetLastError(); BOOST_LOG(fatal) << "Failed to get Sunshine path: "sv << winerr; return; @@ -1220,7 +1220,7 @@ namespace platf { void restart() { // If we're running standalone, we have to respawn ourselves via CreateProcess(). // If we're running from the service, we should just exit and let it respawn us. - if (GetConsoleWindow() != NULL) { + if (GetConsoleWindow() != nullptr) { // Avoid racing with the new process by waiting until we're exiting to start it. atexit(restart_on_exit); } @@ -1538,7 +1538,7 @@ namespace platf { } virtual ~qos_t() { - if (!fn_QOSRemoveSocketFromFlow(qos_handle, (SOCKET) NULL, flow_id, 0)) { + if (!fn_QOSRemoveSocketFromFlow(qos_handle, (SOCKET) nullptr, flow_id, 0)) { auto winerr = GetLastError(); BOOST_LOG(warning) << "QOSRemoveSocketFromFlow() failed: "sv << winerr; } @@ -1570,7 +1570,7 @@ namespace platf { static std::once_flag load_qwave_once_flag; std::call_once(load_qwave_once_flag, []() { // qWAVE is not installed by default on Windows Server, so we load it dynamically - HMODULE qwave = LoadLibraryExA("qwave.dll", NULL, LOAD_LIBRARY_SEARCH_SYSTEM32); + HMODULE qwave = LoadLibraryExA("qwave.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32); if (!qwave) { BOOST_LOG(debug) << "qwave.dll is not available on this OS"sv; return; @@ -1788,11 +1788,11 @@ namespace platf { } operator bool() override { - return timer != NULL; + return timer != nullptr; } private: - HANDLE timer = NULL; + HANDLE timer = nullptr; }; std::unique_ptr create_high_precision_timer() { diff --git a/src/platform/windows/misc.h b/src/platform/windows/misc.h index 30d85376604..ba58c877052 100644 --- a/src/platform/windows/misc.h +++ b/src/platform/windows/misc.h @@ -9,7 +9,7 @@ #include // platform includes -#include +#include #include namespace platf { diff --git a/src/platform/windows/nvprefs/driver_settings.cpp b/src/platform/windows/nvprefs/driver_settings.cpp index 57aa809fde0..8cefd7932fb 100644 --- a/src/platform/windows/nvprefs/driver_settings.cpp +++ b/src/platform/windows/nvprefs/driver_settings.cpp @@ -60,7 +60,7 @@ namespace nvprefs { void driver_settings_t::destroy() { if (session_handle) { NvAPI_DRS_DestroySession(session_handle); - session_handle = 0; + session_handle = nullptr; } NvAPI_Unload(); } @@ -105,7 +105,7 @@ namespace nvprefs { if (swapchain_data) { NvAPI_Status status; - NvDRSProfileHandle profile_handle = 0; + NvDRSProfileHandle profile_handle = nullptr; status = NvAPI_DRS_GetBaseProfile(session_handle, &profile_handle); if (status != NVAPI_OK) { nvapi_error_message(status); @@ -168,7 +168,7 @@ namespace nvprefs { return true; } - NvDRSProfileHandle profile_handle = 0; + NvDRSProfileHandle profile_handle = nullptr; status = NvAPI_DRS_GetBaseProfile(session_handle, &profile_handle); if (status != NVAPI_OK) { nvapi_error_message(status); @@ -224,7 +224,7 @@ namespace nvprefs { NvAPI_UnicodeString profile_name = {}; fill_nvapi_string(profile_name, sunshine_application_profile_name); - NvDRSProfileHandle profile_handle = 0; + NvDRSProfileHandle profile_handle = nullptr; status = NvAPI_DRS_FindProfileByName(session_handle, profile_name, &profile_handle); if (status != NVAPI_OK) { diff --git a/src/platform/windows/nvprefs/driver_settings.h b/src/platform/windows/nvprefs/driver_settings.h index e4649d1d502..f62cf4bdc70 100644 --- a/src/platform/windows/nvprefs/driver_settings.h +++ b/src/platform/windows/nvprefs/driver_settings.h @@ -36,7 +36,7 @@ namespace nvprefs { bool check_and_modify_application_profile(bool &modified); private: - NvDRSSessionHandle session_handle = 0; + NvDRSSessionHandle session_handle = nullptr; }; } // namespace nvprefs diff --git a/src/platform/windows/nvprefs/nvapi_opensource_wrapper.cpp b/src/platform/windows/nvprefs/nvapi_opensource_wrapper.cpp index b4c04f6aadd..d03c6bbb08c 100644 --- a/src/platform/windows/nvprefs/nvapi_opensource_wrapper.cpp +++ b/src/platform/windows/nvprefs/nvapi_opensource_wrapper.cpp @@ -15,7 +15,7 @@ namespace { std::map interfaces; - HMODULE dll = NULL; + HMODULE dll = nullptr; template NvAPI_Status call_interface(const char *name, Args... args) { @@ -47,7 +47,7 @@ NvAPI_Initialize() { auto dll_name = "nvapi.dll"; #endif - if ((dll = LoadLibraryEx(dll_name, NULL, LOAD_LIBRARY_SEARCH_SYSTEM32))) { + if ((dll = LoadLibraryEx(dll_name, nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32))) { if (auto query_interface = (decltype(nvapi_QueryInterface) *) GetProcAddress(dll, "nvapi_QueryInterface")) { for (const auto &item : nvapi_interface_table) { interfaces[item.func] = query_interface(item.id); @@ -64,7 +64,7 @@ NVAPI_INTERFACE NvAPI_Unload() { if (dll) { interfaces.clear(); FreeLibrary(dll); - dll = NULL; + dll = nullptr; } return NVAPI_OK; } diff --git a/src/platform/windows/nvprefs/nvprefs_common.h b/src/platform/windows/nvprefs/nvprefs_common.h index baf4c0fbdbf..61a7a6eb05d 100644 --- a/src/platform/windows/nvprefs/nvprefs_common.h +++ b/src/platform/windows/nvprefs/nvprefs_common.h @@ -7,8 +7,8 @@ // platform includes // disable clang-format header reordering // clang-format off -#include -#include +#include +#include // clang-format on // local includes @@ -21,7 +21,7 @@ namespace nvprefs { explicit operator bool() const { auto handle = get(); - return handle != NULL && handle != INVALID_HANDLE_VALUE; + return handle != nullptr && handle != INVALID_HANDLE_VALUE; } }; diff --git a/src/platform/windows/nvprefs/undo_file.cpp b/src/platform/windows/nvprefs/undo_file.cpp index ada737c8208..477af03399c 100644 --- a/src/platform/windows/nvprefs/undo_file.cpp +++ b/src/platform/windows/nvprefs/undo_file.cpp @@ -51,7 +51,7 @@ namespace nvprefs { std::optional undo_file_t::open_existing_file(std::filesystem::path file_path, bool &access_denied) { undo_file_t file; - file.file_handle.reset(CreateFileW(file_path.c_str(), GENERIC_READ | DELETE, 0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL)); + file.file_handle.reset(CreateFileW(file_path.c_str(), GENERIC_READ | DELETE, 0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)); if (file.file_handle) { access_denied = false; return file; @@ -64,7 +64,7 @@ namespace nvprefs { std::optional undo_file_t::create_new_file(std::filesystem::path file_path) { undo_file_t file; - file.file_handle.reset(CreateFileW(file_path.c_str(), GENERIC_WRITE | STANDARD_RIGHTS_ALL, 0, nullptr, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL)); + file.file_handle.reset(CreateFileW(file_path.c_str(), GENERIC_WRITE | STANDARD_RIGHTS_ALL, 0, nullptr, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, nullptr)); if (file.file_handle) { // give GENERIC_READ, GENERIC_WRITE and DELETE permissions to Users group diff --git a/src/platform/windows/publish.cpp b/src/platform/windows/publish.cpp index 4c3c6f2421c..e93001a4fc7 100644 --- a/src/platform/windows/publish.cpp +++ b/src/platform/windows/publish.cpp @@ -3,12 +3,12 @@ * @brief Definitions for Windows mDNS service registration. */ // platform includes -// winsock2.h must be included before windows.h +// WinSock2.h must be included before Windows.h // clang-format off -#include -#include +#include +#include // clang-format on -#include +#include #include // local includes diff --git a/src/platform/windows/windows.rc b/src/platform/windows/windows.rc index 1bcd9dfd5ef..7de76b9301c 100644 --- a/src/platform/windows/windows.rc +++ b/src/platform/windows/windows.rc @@ -1,13 +1,6 @@ /** -<<<<<<<< HEAD:src/platform/windows/windows.rc * @file src/platform/windows/windows.rc * @brief Windows resource file. -======== - * @file src/platform/windows/windows.rc.in - * @brief Windows resource file template. - * @note The final `windows.rc` is generated from this file during the CMake build. - * @todo Use CMake definitions directly, instead of configuring this file. ->>>>>>>> master:src/platform/windows/windows.rc.in */ #include "winver.h" diff --git a/src/process.cpp b/src/process.cpp index c009fda3ca1..1bf41c594f9 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include @@ -97,11 +98,17 @@ namespace proc { boost::filesystem::path find_working_directory(const std::string &cmd, boost::process::v1::environment &env) { // Parse the raw command string into parts to get the actual command portion + std::vector parts; + try { #ifdef _WIN32 - auto parts = boost::program_options::split_winmain(cmd); + parts = boost::program_options::split_winmain(cmd); #else - auto parts = boost::program_options::split_unix(cmd); + parts = boost::program_options::split_unix(cmd); #endif + } catch (boost::escaped_list_error &err) { + BOOST_LOG(error) << "Boost failed to parse command ["sv << cmd << "] because " << err.what(); + return boost::filesystem::path(); + } if (parts.empty()) { BOOST_LOG(error) << "Unable to parse command: "sv << cmd; return boost::filesystem::path(); @@ -158,7 +165,7 @@ namespace proc { _env["SUNSHINE_CLIENT_GCMAP"] = std::to_string(launch_session->gcmap); _env["SUNSHINE_CLIENT_HOST_AUDIO"] = launch_session->host_audio ? "true" : "false"; _env["SUNSHINE_CLIENT_ENABLE_SOPS"] = launch_session->enable_sops ? "true" : "false"; - int channelCount = launch_session->surround_info & (65535); + int channelCount = launch_session->surround_info & 65535; switch (channelCount) { case 2: _env["SUNSHINE_CLIENT_AUDIO_CONFIGURATION"] = "2.0"; @@ -217,10 +224,14 @@ namespace proc { } } - child.wait(); + child.wait(ec); + if (ec) { + BOOST_LOG(error) << '[' << cmd.do_cmd << "] wait failed with error code ["sv << ec << ']'; + return -1; + } auto ret = child.exit_code(); - if (ret != 0 && ec != std::errc::permission_denied) { - BOOST_LOG(error) << '[' << cmd.do_cmd << "] failed with code ["sv << ret << ']'; + if (ret != 0) { + BOOST_LOG(error) << '[' << cmd.do_cmd << "] exited with code ["sv << ret << ']'; return -1; } } diff --git a/src/rtsp.cpp b/src/rtsp.cpp index cd43dd0c5c2..f5909eb8743 100644 --- a/src/rtsp.cpp +++ b/src/rtsp.cpp @@ -12,6 +12,7 @@ extern "C" { // standard includes #include #include +#include #include #include #include @@ -864,7 +865,7 @@ namespace rtsp_stream { session_option.next = &port_option; // Moonlight merely requires 'server_port=' - auto port_value = "server_port=" + std::to_string(port); + auto port_value = std::format("server_port={}", static_cast(port)); port_option.option = const_cast("Transport"); port_option.content = port_value.data(); @@ -951,6 +952,7 @@ namespace rtsp_stream { args.try_emplace("x-ss-general.encryptionEnabled"sv, "0"sv); args.try_emplace("x-ss-video[0].chromaSamplingType"sv, "0"sv); args.try_emplace("x-ss-video[0].intraRefresh"sv, "0"sv); + args.try_emplace("x-nv-video[0].clientRefreshRateX100"sv, "0"sv); stream::config_t config; @@ -980,6 +982,7 @@ namespace rtsp_stream { config.monitor.height = util::from_view(args.at("x-nv-video[0].clientViewportHt"sv)); config.monitor.width = util::from_view(args.at("x-nv-video[0].clientViewportWd"sv)); config.monitor.framerate = util::from_view(args.at("x-nv-video[0].maxFPS"sv)); + config.monitor.framerateX100 = util::from_view(args.at("x-nv-video[0].clientRefreshRateX100"sv)); config.monitor.bitrate = util::from_view(args.at("x-nv-vqos[0].bw.maximumBitrateKbps"sv)); config.monitor.slicesPerFrame = util::from_view(args.at("x-nv-video[0].videoEncoderSlicesPerFrame"sv)); config.monitor.numRefFrames = util::from_view(args.at("x-nv-video[0].maxNumReferenceFrames"sv)); @@ -1029,6 +1032,10 @@ namespace rtsp_stream { } config.audio.flags[audio::config_t::CUSTOM_SURROUND_PARAMS] = valid; } + if (session.continuous_audio) { + BOOST_LOG(info) << "Client requested continuous audio"sv; + config.audio.flags[audio::config_t::CONTINUOUS_AUDIO] = true; + } // If the client sent a configured bitrate, we will choose the actual bitrate ourselves // by using FEC percentage and audio quality settings. If the calculated bitrate ends up diff --git a/src/rtsp.h b/src/rtsp.h index 2303b96b6b3..6dfa16e12e8 100644 --- a/src/rtsp.h +++ b/src/rtsp.h @@ -32,6 +32,7 @@ namespace rtsp_stream { int appid; int surround_info; std::string surround_params; + bool continuous_audio; bool enable_hdr; bool enable_sops; diff --git a/src/stream.cpp b/src/stream.cpp index e6bb5ebfde2..6bfd5d9772a 100644 --- a/src/stream.cpp +++ b/src/stream.cpp @@ -520,7 +520,12 @@ namespace stream { // for other communications to the client. This is necessary to ensure // proper routing on multi-homed hosts. auto local_address = platf::from_sockaddr((sockaddr *) &peer->localAddress.address); - session_p->localAddress = boost::asio::ip::make_address(local_address); + try { + session_p->localAddress = boost::asio::ip::make_address(local_address); + } catch (const boost::system::system_error &e) { + BOOST_LOG(error) << "boost::system::system_error in address parsing: " << e.what() << " (code: " << e.code() << ")"sv; + throw; + } BOOST_LOG(debug) << "Control local address ["sv << local_address << ']'; BOOST_LOG(debug) << "Control peer address ["sv << peer_addr << ':' << peer_port << ']'; diff --git a/src/system_tray.cpp b/src/system_tray.cpp index f792c741cc9..7d938bbbd32 100644 --- a/src/system_tray.cpp +++ b/src/system_tray.cpp @@ -13,7 +13,7 @@ #define TRAY_ICON_PLAYING WEB_DIR "images/sunshine-playing.ico" #define TRAY_ICON_PAUSING WEB_DIR "images/sunshine-pausing.ico" #define TRAY_ICON_LOCKED WEB_DIR "images/sunshine-locked.ico" - #elif defined(__linux__) || defined(linux) || defined(__linux) + #elif defined(__linux__) || defined(linux) || defined(__linux) || defined(__FreeBSD__) #define TRAY_ICON SUNSHINE_TRAY_PREFIX "-tray" #define TRAY_ICON_PLAYING SUNSHINE_TRAY_PREFIX "-playing" #define TRAY_ICON_PAUSING SUNSHINE_TRAY_PREFIX "-pausing" @@ -27,8 +27,12 @@ #endif // standard includes + #include + #include #include + #include #include + #include // lib includes #include @@ -47,44 +51,44 @@ using namespace std::literals; // system_tray namespace namespace system_tray { - static std::atomic tray_initialized = false; + static std::atomic tray_initialized = false; - void tray_open_ui_cb(struct tray_menu *item) { + void tray_open_ui_cb([[maybe_unused]] struct tray_menu *item) { BOOST_LOG(info) << "Opening UI from system tray"sv; launch_ui(); } - void tray_donate_github_cb(struct tray_menu *item) { + void tray_donate_github_cb([[maybe_unused]] struct tray_menu *item) { platf::open_url("https://github.com/sponsors/LizardByte"); } - void tray_donate_patreon_cb(struct tray_menu *item) { + void tray_donate_patreon_cb([[maybe_unused]] struct tray_menu *item) { platf::open_url("https://www.patreon.com/LizardByte"); } - void tray_donate_paypal_cb(struct tray_menu *item) { + void tray_donate_paypal_cb([[maybe_unused]] struct tray_menu *item) { platf::open_url("https://www.paypal.com/paypalme/ReenigneArcher"); } - void tray_reset_display_device_config_cb(struct tray_menu *item) { + void tray_reset_display_device_config_cb([[maybe_unused]] struct tray_menu *item) { BOOST_LOG(info) << "Resetting display device config from system tray"sv; std::ignore = display_device::reset_persistence(); } - void tray_restart_cb(struct tray_menu *item) { + void tray_restart_cb([[maybe_unused]] struct tray_menu *item) { BOOST_LOG(info) << "Restarting from system tray"sv; platf::restart(); } - void tray_quit_cb(struct tray_menu *item) { + void tray_quit_cb([[maybe_unused]] struct tray_menu *item) { BOOST_LOG(info) << "Quitting from system tray"sv; #ifdef _WIN32 // If we're running in a service, return a special status to // tell it to terminate too, otherwise it will just respawn us. - if (GetConsoleWindow() == NULL) { + if (GetConsoleWindow() == nullptr) { lifetime::exit_sunshine(ERROR_SHUTDOWN_IN_PROGRESS, true); return; } @@ -123,7 +127,7 @@ namespace system_tray { .allIconPaths = {TRAY_ICON, TRAY_ICON_LOCKED, TRAY_ICON_PLAYING, TRAY_ICON_PAUSING}, }; - int system_tray() { + int init_tray() { #ifdef _WIN32 // If we're running as SYSTEM, Explorer.exe will not have permission to open our thread handle // to monitor for thread termination. If Explorer fails to open our thread, our tray icon @@ -189,39 +193,28 @@ namespace system_tray { if (tray_init(&tray) < 0) { BOOST_LOG(warning) << "Failed to create system tray"sv; return 1; - } else { - BOOST_LOG(info) << "System tray created"sv; } + BOOST_LOG(info) << "System tray created"sv; tray_initialized = true; - while (tray_loop(1) == 0) { - BOOST_LOG(debug) << "System tray loop"sv; - } - return 0; } - void run_tray() { - // create the system tray - #if defined(__APPLE__) || defined(__MACH__) - // macOS requires that UI elements be created on the main thread - // creating tray using dispatch queue does not work, although the code doesn't actually throw any (visible) errors - - // dispatch_async(dispatch_get_main_queue(), ^{ - // system_tray(); - // }); - - BOOST_LOG(info) << "system_tray() is not yet implemented for this platform."sv; - #else // Windows, Linux - // create tray in separate thread - std::thread tray_thread(system_tray); - tray_thread.detach(); - #endif + int process_tray_events() { + if (!tray_initialized) { + BOOST_LOG(error) << "System tray is not initialized"sv; + return 1; + } + + // Block until an event is processed or tray_quit() is called + return tray_loop(1); } int end_tray() { - tray_initialized = false; - tray_exit(); + if (tray_initialized) { + tray_initialized = false; + tray_exit(); + } return 0; } @@ -230,18 +223,18 @@ namespace system_tray { return; } - tray.notification_title = NULL; - tray.notification_text = NULL; - tray.notification_cb = NULL; - tray.notification_icon = NULL; + tray.notification_title = nullptr; + tray.notification_text = nullptr; + tray.notification_cb = nullptr; + tray.notification_icon = nullptr; tray.icon = TRAY_ICON_PLAYING; tray_update(&tray); tray.icon = TRAY_ICON_PLAYING; tray.notification_title = "Stream Started"; - char msg[256]; - snprintf(msg, std::size(msg), "Streaming started for %s", app_name.c_str()); - tray.notification_text = msg; - tray.tooltip = msg; + + static std::string msg = std::format("Streaming started for {}", app_name); + tray.notification_text = msg.c_str(); + tray.tooltip = msg.c_str(); tray.notification_icon = TRAY_ICON_PLAYING; tray_update(&tray); } @@ -251,18 +244,18 @@ namespace system_tray { return; } - tray.notification_title = NULL; - tray.notification_text = NULL; - tray.notification_cb = NULL; - tray.notification_icon = NULL; + tray.notification_title = nullptr; + tray.notification_text = nullptr; + tray.notification_cb = nullptr; + tray.notification_icon = nullptr; tray.icon = TRAY_ICON_PAUSING; tray_update(&tray); - char msg[256]; - snprintf(msg, std::size(msg), "Streaming paused for %s", app_name.c_str()); + + static std::string msg = std::format("Streaming paused for {}", app_name); tray.icon = TRAY_ICON_PAUSING; tray.notification_title = "Stream Paused"; - tray.notification_text = msg; - tray.tooltip = msg; + tray.notification_text = msg.c_str(); + tray.tooltip = msg.c_str(); tray.notification_icon = TRAY_ICON_PAUSING; tray_update(&tray); } @@ -272,18 +265,18 @@ namespace system_tray { return; } - tray.notification_title = NULL; - tray.notification_text = NULL; - tray.notification_cb = NULL; - tray.notification_icon = NULL; + tray.notification_title = nullptr; + tray.notification_text = nullptr; + tray.notification_cb = nullptr; + tray.notification_icon = nullptr; tray.icon = TRAY_ICON; tray_update(&tray); - char msg[256]; - snprintf(msg, std::size(msg), "Application %s successfully stopped", app_name.c_str()); + + static std::string msg = std::format("Application {} successfully stopped", app_name); tray.icon = TRAY_ICON; tray.notification_icon = TRAY_ICON; tray.notification_title = "Application Stopped"; - tray.notification_text = msg; + tray.notification_text = msg.c_str(); tray.tooltip = PROJECT_NAME; tray_update(&tray); } @@ -293,10 +286,10 @@ namespace system_tray { return; } - tray.notification_title = NULL; - tray.notification_text = NULL; - tray.notification_cb = NULL; - tray.notification_icon = NULL; + tray.notification_title = nullptr; + tray.notification_text = nullptr; + tray.notification_cb = nullptr; + tray.notification_icon = nullptr; tray.icon = TRAY_ICON; tray_update(&tray); tray.icon = TRAY_ICON; @@ -305,10 +298,42 @@ namespace system_tray { tray.notification_icon = TRAY_ICON_LOCKED; tray.tooltip = PROJECT_NAME; tray.notification_cb = []() { - launch_ui_with_path("/pin"); + launch_ui("/pin"); }; tray_update(&tray); } + // Threading functions available on all platforms + static void tray_thread_worker() { + BOOST_LOG(info) << "System tray thread started"sv; + + // Initialize the tray in this thread + if (init_tray() != 0) { + BOOST_LOG(error) << "Failed to initialize tray in thread"sv; + return; + } + + // Main tray event loop + while (process_tray_events() == 0); + + BOOST_LOG(info) << "System tray thread ended"sv; + } + + int init_tray_threaded() { + try { + auto tray_thread = std::thread(tray_thread_worker); + + // The tray thread doesn't require strong lifetime management. + // It will exit asynchronously when tray_exit() is called. + tray_thread.detach(); + + BOOST_LOG(info) << "System tray thread initialized successfully"sv; + return 0; + } catch (const std::exception &e) { + BOOST_LOG(error) << "Failed to create tray thread: " << e.what(); + return 1; + } + } + } // namespace system_tray #endif diff --git a/src/system_tray.h b/src/system_tray.h index 00a17f92cf4..2a1cd1b433a 100644 --- a/src/system_tray.h +++ b/src/system_tray.h @@ -12,56 +12,55 @@ namespace system_tray { * @brief Callback for opening the UI from the system tray. * @param item The tray menu item. */ - void tray_open_ui_cb(struct tray_menu *item); + void tray_open_ui_cb([[maybe_unused]] struct tray_menu *item); /** * @brief Callback for opening GitHub Sponsors from the system tray. * @param item The tray menu item. */ - void tray_donate_github_cb(struct tray_menu *item); + void tray_donate_github_cb([[maybe_unused]] struct tray_menu *item); /** * @brief Callback for opening Patreon from the system tray. * @param item The tray menu item. */ - void tray_donate_patreon_cb(struct tray_menu *item); + void tray_donate_patreon_cb([[maybe_unused]] struct tray_menu *item); /** * @brief Callback for opening PayPal donation from the system tray. * @param item The tray menu item. */ - void tray_donate_paypal_cb(struct tray_menu *item); + void tray_donate_paypal_cb([[maybe_unused]] struct tray_menu *item); /** * @brief Callback for resetting display device configuration. * @param item The tray menu item. */ - void tray_reset_display_device_config_cb(struct tray_menu *item); + void tray_reset_display_device_config_cb([[maybe_unused]] struct tray_menu *item); /** * @brief Callback for restarting Sunshine from the system tray. * @param item The tray menu item. */ - void tray_restart_cb(struct tray_menu *item); + void tray_restart_cb([[maybe_unused]] struct tray_menu *item); /** * @brief Callback for exiting Sunshine from the system tray. * @param item The tray menu item. */ - void tray_quit_cb(struct tray_menu *item); + void tray_quit_cb([[maybe_unused]] struct tray_menu *item); /** - * @brief Create the system tray. - * @details This function has an endless loop, so it should be run in a separate thread. - * @return 1 if the system tray failed to create, otherwise 0 once the tray has been terminated. + * @brief Initializes the system tray without starting a loop. + * @return 0 if initialization was successful, non-zero otherwise. */ - int system_tray(); + int init_tray(); /** - * @brief Run the system tray with platform specific options. - * @todo macOS requires that UI elements be created on the main thread, so the system tray is not currently implemented for macOS. + * @brief Processes a single tray event iteration. + * @return 0 if processing was successful, non-zero otherwise. */ - int run_tray(); + int process_tray_events(); /** * @brief Exit the system tray. @@ -91,4 +90,10 @@ namespace system_tray { * @brief Spawns a notification for PIN Pairing. Clicking it opens the PIN Web UI Page */ void update_tray_require_pin(); + + /** + * @brief Initializes and runs the system tray in a separate thread. + * @return 0 if initialization was successful, non-zero otherwise. + */ + int init_tray_threaded(); } // namespace system_tray diff --git a/src/video.cpp b/src/video.cpp index 4db957b3e72..55bd322ad5d 100644 --- a/src/video.cpp +++ b/src/video.cpp @@ -315,6 +315,12 @@ namespace video { avcodec_encode_session_t(avcodec_encode_session_t &&other) noexcept = default; ~avcodec_encode_session_t() { + // Flush any remaining frames in the encoder + if (avcodec_send_frame(avcodec_ctx.get(), nullptr) == 0) { + packet_raw_avcodec pkt; + while (avcodec_receive_packet(avcodec_ctx.get(), pkt.av_packet) == 0); + } + // Order matters here because the context relies on the hwdevice still being valid avcodec_ctx.reset(); device.reset(); @@ -536,7 +542,7 @@ namespace video { {"forced-idr"s, 1}, {"zerolatency"s, 1}, {"surfaces"s, 1}, - {"filler_data"s, false}, + {"cbr_padding"s, false}, {"preset"s, &config::video.nv_legacy.preset}, {"tune"s, NV_ENC_TUNING_INFO_ULTRA_LOW_LATENCY}, {"rc"s, NV_ENC_PARAMS_RC_CBR}, @@ -557,6 +563,7 @@ namespace video { {"forced-idr"s, 1}, {"zerolatency"s, 1}, {"surfaces"s, 1}, + {"cbr_padding"s, false}, {"preset"s, &config::video.nv_legacy.preset}, {"tune"s, NV_ENC_TUNING_INFO_ULTRA_LOW_LATENCY}, {"rc"s, NV_ENC_PARAMS_RC_CBR}, @@ -582,6 +589,7 @@ namespace video { {"forced-idr"s, 1}, {"zerolatency"s, 1}, {"surfaces"s, 1}, + {"cbr_padding"s, false}, {"preset"s, &config::video.nv_legacy.preset}, {"tune"s, NV_ENC_TUNING_INFO_ULTRA_LOW_LATENCY}, {"rc"s, NV_ENC_PARAMS_RC_CBR}, @@ -730,6 +738,7 @@ namespace video { {"filler_data"s, false}, {"forced_idr"s, 1}, {"latency"s, "lowest_latency"s}, + {"async_depth"s, 1}, {"skip_frame"s, 0}, {"log_to_dbg"s, []() { return config::sunshine.min_log_level < 2 ? 1 : 0; @@ -753,6 +762,7 @@ namespace video { {"filler_data"s, false}, {"forced_idr"s, 1}, {"latency"s, 1}, + {"async_depth"s, 1}, {"skip_frame"s, 0}, {"log_to_dbg"s, []() { return config::sunshine.min_log_level < 2 ? 1 : 0; @@ -791,6 +801,7 @@ namespace video { {"filler_data"s, false}, {"forced_idr"s, 1}, {"latency"s, 1}, + {"async_depth"s, 1}, {"frame_skipping"s, 0}, {"log_to_dbg"s, []() { return config::sunshine.min_log_level < 2 ? 1 : 0; @@ -886,7 +897,7 @@ namespace video { H264_ONLY | PARALLEL_ENCODING | ALWAYS_REPROBE | YUV444_SUPPORT }; -#ifdef __linux__ +#if defined(__linux__) || defined(linux) || defined(__linux) || defined(__FreeBSD__) encoder_t vaapi { "vaapi"sv, std::make_unique( @@ -1021,7 +1032,7 @@ namespace video { &quicksync, &amdvce, #endif -#ifdef __linux__ +#if defined(__linux__) || defined(linux) || defined(__linux) || defined(__FreeBSD__) &vaapi, #endif #ifdef __APPLE__ @@ -1521,27 +1532,32 @@ namespace video { ctx->height = config.height; ctx->time_base = AVRational {1, config.framerate}; ctx->framerate = AVRational {config.framerate, 1}; + if (config.framerateX100 > 0) { + AVRational fps = video::framerateX100_to_rational(config.framerateX100); + ctx->framerate = fps; + ctx->time_base = AVRational {fps.den, fps.num}; + } switch (config.videoFormat) { case 0: // 10-bit h264 encoding is not supported by our streaming protocol assert(!config.dynamicRange); - ctx->profile = (config.chromaSamplingType == 1) ? FF_PROFILE_H264_HIGH_444_PREDICTIVE : FF_PROFILE_H264_HIGH; + ctx->profile = (config.chromaSamplingType == 1) ? AV_PROFILE_H264_HIGH_444_PREDICTIVE : AV_PROFILE_H264_HIGH; break; case 1: if (config.chromaSamplingType == 1) { // HEVC uses the same RExt profile for both 8 and 10 bit YUV 4:4:4 encoding - ctx->profile = FF_PROFILE_HEVC_REXT; + ctx->profile = AV_PROFILE_HEVC_REXT; } else { - ctx->profile = config.dynamicRange ? FF_PROFILE_HEVC_MAIN_10 : FF_PROFILE_HEVC_MAIN; + ctx->profile = config.dynamicRange ? AV_PROFILE_HEVC_MAIN_10 : AV_PROFILE_HEVC_MAIN; } break; case 2: // AV1 supports both 8 and 10 bit encoding with the same Main profile // but YUV 4:4:4 sampling requires High profile - ctx->profile = (config.chromaSamplingType == 1) ? FF_PROFILE_AV1_HIGH : FF_PROFILE_AV1_MAIN; + ctx->profile = (config.chromaSamplingType == 1) ? AV_PROFILE_AV1_HIGH : AV_PROFILE_AV1_MAIN; break; } @@ -2451,8 +2467,8 @@ namespace video { encoder.av1.capabilities.set(); // First, test encoder viability - config_t config_max_ref_frames {1920, 1080, 60, 1000, 1, 1, 1, 0, 0, 0}; - config_t config_autoselect {1920, 1080, 60, 1000, 1, 0, 1, 0, 0, 0}; + config_t config_max_ref_frames {1920, 1080, 60, 6000, 1000, 1, 1, 1, 0, 0, 0}; + config_t config_autoselect {1920, 1080, 60, 6000, 1000, 1, 0, 1, 0, 0, 0}; // If the encoder isn't supported at all (not even H.264), bail early reset_display(disp, encoder.platform_formats->dev_type, output_name, config_autoselect); @@ -2547,14 +2563,14 @@ namespace video { { // H.264 is special because encoders may support YUV 4:4:4 without supporting 10-bit color depth if (encoder.flags & YUV444_SUPPORT) { - config_t config_h264_yuv444 {1920, 1080, 60, 1000, 1, 0, 1, 0, 0, 1}; + config_t config_h264_yuv444 {1920, 1080, 60, 6000, 1000, 1, 0, 1, 0, 0, 1}; encoder.h264[encoder_t::YUV444] = disp->is_codec_supported(encoder.h264.name, config_h264_yuv444) && validate_config(disp, encoder, config_h264_yuv444) >= 0; } else { encoder.h264[encoder_t::YUV444] = false; } - const config_t generic_hdr_config = {1920, 1080, 60, 1000, 1, 0, 3, 1, 1, 0}; + const config_t generic_hdr_config = {1920, 1080, 60, 6000, 1000, 1, 0, 3, 1, 1, 0}; // Reset the display since we're switching from SDR to HDR reset_display(disp, encoder.platform_formats->dev_type, output_name, generic_hdr_config); diff --git a/src/video.h b/src/video.h index a966c53e6c3..8dbf76e27bd 100644 --- a/src/video.h +++ b/src/video.h @@ -24,6 +24,7 @@ namespace video { int width; // Video width in pixels int height; // Video height in pixels int framerate; // Requested framerate, used in individual frame bitrate budget calculation + int framerateX100; // Optional field for streaming at NTSC or similar rates e.g. 59.94 = 5994 int bitrate; // Video bitrate in kilobits (1000 bits) for requested framerate int slicesPerFrame; // Number of slices per frame int numRefFrames; // Max number of reference frames @@ -221,7 +222,7 @@ namespace video { extern encoder_t quicksync; #endif -#ifdef __linux__ +#if defined(__linux__) || defined(linux) || defined(__linux) || defined(__FreeBSD__) extern encoder_t vaapi; #endif @@ -352,4 +353,25 @@ namespace video { * @warning This is only safe to call when there is no client actively streaming. */ int probe_encoders(); + + // Several NTSC standard refresh rates are hardcoded here, because their + // true rate requires a denominator of 1001. ffmpeg's av_d2q() would assume it could + // reduce 29.97 to 2997/100 but this would be slightly wrong. We also include + // support for 23.976 film in case someone wants to stream a film at the perfect + // framerate. + inline AVRational framerateX100_to_rational(const int framerateX100) { + if (framerateX100 % 2997 == 0) { + // Multiples of NTSC 29.97 e.g. 59.94, 119.88 + return AVRational {(framerateX100 / 2997) * 30000, 1001}; + } + switch (framerateX100) { + case 2397: // the other weird NTSC framerate, assume these want 23.976 film + case 2398: + return AVRational {24000, 1001}; + default: + // any other fractional rate can be reduced by ffmpeg. Max is set to 1 << 26 based on docs: + // "rational numbers with |num| <= 1<<26 && |den| <= 1<<26 can be recovered exactly from their double representation" + return av_d2q((double) framerateX100 / 100.0f, 1 << 26); + } + } } // namespace video diff --git a/src/video_colorspace.cpp b/src/video_colorspace.cpp index 24522235b3e..674c053a2c5 100644 --- a/src/video_colorspace.cpp +++ b/src/video_colorspace.cpp @@ -120,66 +120,9 @@ namespace video { return avcodec_colorspace; } - const color_t *color_vectors_from_colorspace(const sunshine_colorspace_t &colorspace) { - return color_vectors_from_colorspace(colorspace.colorspace, colorspace.full_range); - } - - const color_t *color_vectors_from_colorspace(colorspace_e colorspace, bool full_range) { - using float2 = float[2]; - auto make_color_matrix = [](float Cr, float Cb, const float2 &range_Y, const float2 &range_UV) -> color_t { - float Cg = 1.0f - Cr - Cb; - - float Cr_i = 1.0f - Cr; - float Cb_i = 1.0f - Cb; - - float shift_y = range_Y[0] / 255.0f; - float shift_uv = range_UV[0] / 255.0f; - - float scale_y = (range_Y[1] - range_Y[0]) / 255.0f; - float scale_uv = (range_UV[1] - range_UV[0]) / 255.0f; - return { - {Cr, Cg, Cb, 0.0f}, - {-(Cr * 0.5f / Cb_i), -(Cg * 0.5f / Cb_i), 0.5f, 0.5f}, - {0.5f, -(Cg * 0.5f / Cr_i), -(Cb * 0.5f / Cr_i), 0.5f}, - {scale_y, shift_y}, - {scale_uv, shift_uv}, - }; - }; - - static const color_t colors[] { - make_color_matrix(0.299f, 0.114f, {16.0f, 235.0f}, {16.0f, 240.0f}), // BT601 MPEG - make_color_matrix(0.299f, 0.114f, {0.0f, 255.0f}, {0.0f, 255.0f}), // BT601 JPEG - make_color_matrix(0.2126f, 0.0722f, {16.0f, 235.0f}, {16.0f, 240.0f}), // BT709 MPEG - make_color_matrix(0.2126f, 0.0722f, {0.0f, 255.0f}, {0.0f, 255.0f}), // BT709 JPEG - make_color_matrix(0.2627f, 0.0593f, {16.0f, 235.0f}, {16.0f, 240.0f}), // BT2020 MPEG - make_color_matrix(0.2627f, 0.0593f, {0.0f, 255.0f}, {0.0f, 255.0f}), // BT2020 JPEG - }; - - const color_t *result = nullptr; - - switch (colorspace) { - case colorspace_e::rec601: - default: - result = &colors[0]; - break; - case colorspace_e::rec709: - result = &colors[2]; - break; - case colorspace_e::bt2020: - case colorspace_e::bt2020sdr: - result = &colors[4]; - break; - }; - - if (full_range) { - result++; - } - - return result; - } - - const color_t *new_color_vectors_from_colorspace(const sunshine_colorspace_t &colorspace) { - constexpr auto generate_color_vectors = [](const sunshine_colorspace_t &colorspace) -> color_t { + const color_t *color_vectors_from_colorspace(const sunshine_colorspace_t &colorspace, bool unorm_output) { + constexpr auto generate_color_vectors = [](const sunshine_colorspace_t &colorspace, bool unorm_output) -> color_t { + // "Table 4 – Interpretation of matrix coefficients (MatrixCoefficients) value" section of ITU-T H.273 double Kr, Kb; switch (colorspace.colorspace) { case colorspace_e::rec601: @@ -202,7 +145,7 @@ namespace video { double y_mult, y_add; double uv_mult, uv_add; - // "Matrix coefficients" section of ITU-T H.273 + // "8.3 Matrix coefficients" section of ITU-T H.273 if (colorspace.full_range) { y_mult = (1 << colorspace.bit_depth) - 1; y_add = 0; @@ -215,9 +158,17 @@ namespace video { uv_add = (1 << (colorspace.bit_depth - 8)) * 128; } - // For rounding - y_add += 0.5; - uv_add += 0.5; + if (unorm_output) { + const double unorm_range = (1 << colorspace.bit_depth) - 1; + y_mult /= unorm_range; + y_add /= unorm_range; + uv_mult /= unorm_range; + uv_add /= unorm_range; + } else { + // For rounding + y_add += 0.5f; + uv_add += 0.5f; + } color_t color_vectors; @@ -246,18 +197,31 @@ namespace video { }; static constexpr color_t colors[] = { - generate_color_vectors({colorspace_e::rec601, false, 8}), - generate_color_vectors({colorspace_e::rec601, true, 8}), - generate_color_vectors({colorspace_e::rec601, false, 10}), - generate_color_vectors({colorspace_e::rec601, true, 10}), - generate_color_vectors({colorspace_e::rec709, false, 8}), - generate_color_vectors({colorspace_e::rec709, true, 8}), - generate_color_vectors({colorspace_e::rec709, false, 10}), - generate_color_vectors({colorspace_e::rec709, true, 10}), - generate_color_vectors({colorspace_e::bt2020, false, 8}), - generate_color_vectors({colorspace_e::bt2020, true, 8}), - generate_color_vectors({colorspace_e::bt2020, false, 10}), - generate_color_vectors({colorspace_e::bt2020, true, 10}), + generate_color_vectors({colorspace_e::rec601, false, 8}, false), + generate_color_vectors({colorspace_e::rec601, true, 8}, false), + generate_color_vectors({colorspace_e::rec601, false, 10}, false), + generate_color_vectors({colorspace_e::rec601, true, 10}, false), + generate_color_vectors({colorspace_e::rec709, false, 8}, false), + generate_color_vectors({colorspace_e::rec709, true, 8}, false), + generate_color_vectors({colorspace_e::rec709, false, 10}, false), + generate_color_vectors({colorspace_e::rec709, true, 10}, false), + generate_color_vectors({colorspace_e::bt2020, false, 8}, false), + generate_color_vectors({colorspace_e::bt2020, true, 8}, false), + generate_color_vectors({colorspace_e::bt2020, false, 10}, false), + generate_color_vectors({colorspace_e::bt2020, true, 10}, false), + + generate_color_vectors({colorspace_e::rec601, false, 8}, true), + generate_color_vectors({colorspace_e::rec601, true, 8}, true), + generate_color_vectors({colorspace_e::rec601, false, 10}, true), + generate_color_vectors({colorspace_e::rec601, true, 10}, true), + generate_color_vectors({colorspace_e::rec709, false, 8}, true), + generate_color_vectors({colorspace_e::rec709, true, 8}, true), + generate_color_vectors({colorspace_e::rec709, false, 10}, true), + generate_color_vectors({colorspace_e::rec709, true, 10}, true), + generate_color_vectors({colorspace_e::bt2020, false, 8}, true), + generate_color_vectors({colorspace_e::bt2020, true, 8}, true), + generate_color_vectors({colorspace_e::bt2020, false, 10}, true), + generate_color_vectors({colorspace_e::bt2020, true, 10}, true), }; const color_t *result = nullptr; @@ -282,6 +246,9 @@ namespace video { if (colorspace.full_range) { result += 1; } + if (unorm_output) { + result += 12; + } return result; } diff --git a/src/video_colorspace.h b/src/video_colorspace.h index eaa6c019f26..db0e11077f5 100644 --- a/src/video_colorspace.h +++ b/src/video_colorspace.h @@ -48,20 +48,15 @@ namespace video { float range_uv[2]; }; - const color_t *color_vectors_from_colorspace(const sunshine_colorspace_t &colorspace); - - const color_t *color_vectors_from_colorspace(colorspace_e colorspace, bool full_range); - /** - * @brief New version of `color_vectors_from_colorspace()` function that better adheres to the standards. - * Returned vectors are used to perform RGB->YUV conversion. - * Unlike its predecessor, color vectors will produce output in `UINT` range, not `UNORM` range. - * Input is still in `UNORM` range. Returned vectors won't modify color primaries and color - * transfer function. + * @brief Get static RGB->YUV color conversion matrix. + * This matrix expects RGB input in UNORM (0.0 to 1.0) range and doesn't perform any + * gamut mapping or gamma correction. * @param colorspace Targeted YUV colorspace. + * @param unorm_output Whether the matrix should produce output in UNORM or UINT range. * @return `const color_t*` that contains RGB->YUV transformation vectors. * Components `range_y` and `range_uv` are there for backwards compatibility * and can be ignored in the computation. */ - const color_t *new_color_vectors_from_colorspace(const sunshine_colorspace_t &colorspace); + const color_t *color_vectors_from_colorspace(const sunshine_colorspace_t &colorspace, bool unorm_output); } // namespace video diff --git a/src_assets/bsd/misc/+POST_INSTALL b/src_assets/bsd/misc/+POST_INSTALL new file mode 100644 index 00000000000..239343d29f5 --- /dev/null +++ b/src_assets/bsd/misc/+POST_INSTALL @@ -0,0 +1,64 @@ +#!/bin/sh + +# FreeBSD post-install script for Sunshine +# This script sets up the necessary permissions for virtual input devices + +echo "Configuring permissions for virtual input devices..." + +# Create the 'input' group if it doesn't exist +if ! pw groupshow input >/dev/null 2>&1; then + echo "Creating 'input' group..." + pw groupadd input + if [ $? -eq 0 ]; then + echo "Successfully created 'input' group." + else + echo "Warning: Failed to create 'input' group. You may need to create it manually." + fi +else + echo "'input' group already exists." +fi + +# Set permissions on /dev/uinput if it exists +if [ -e /dev/uinput ]; then + echo "Setting permissions on /dev/uinput..." + chown root:input /dev/uinput + chmod 660 /dev/uinput + echo "Permissions set on /dev/uinput." +else + echo "Note: /dev/uinput does not exist. It will be created when needed." +fi + +# Create devfs rules for persistent permissions +echo "Creating devfs rules for persistent permissions..." +DEVFS_RULESET_FILE="/etc/devfs.rules" +RULESET_NUM=47989 + +# Check if our rules already exist +if ! grep -q "\[sunshine=$RULESET_NUM\]" "$DEVFS_RULESET_FILE" 2>/dev/null; then + cat >> "$DEVFS_RULESET_FILE" << EOF + +[sunshine=$RULESET_NUM] +add path 'uinput' mode 0660 group input +EOF + echo "Devfs rules added to $DEVFS_RULESET_FILE" +else + echo "Devfs rules already exist in $DEVFS_RULESET_FILE" +fi + +# Apply the devfs ruleset immediately (without waiting for reboot) +echo "Applying devfs ruleset to current system..." +if [ -e /dev/uinput ]; then + devfs -m /dev rule -s $RULESET_NUM apply +fi + +echo "" +echo "Post-installation configuration complete!" +echo "" +echo "IMPORTANT: To use virtual input devices (keyboard, mouse, gamepads)," +echo "you must add your user to the 'input' group:" +echo "" +echo " pw groupmod input -m \$USER" +echo "" +echo "After adding yourself to the group, log out and log back in for the" +echo "changes to take effect." +echo "" diff --git a/src_assets/bsd/misc/+PRE_DEINSTALL b/src_assets/bsd/misc/+PRE_DEINSTALL new file mode 100644 index 00000000000..232e7c8b8f2 --- /dev/null +++ b/src_assets/bsd/misc/+PRE_DEINSTALL @@ -0,0 +1,32 @@ +#!/bin/sh + +# FreeBSD pre-deinstall script for Sunshine +# This script cleans up configuration added during installation + +echo "Cleaning up Sunshine configuration..." + +# Remove devfs rules +DEVFS_RULESET_FILE="/etc/devfs.rules" +RULESET_NUM=47989 + +# Remove rules from /etc/devfs.rules +if [ -f "$DEVFS_RULESET_FILE" ]; then + if grep -q "\[sunshine=$RULESET_NUM\]" "$DEVFS_RULESET_FILE"; then + echo "Removing devfs rules from $DEVFS_RULESET_FILE..." + # Remove the [sunshine=47989] section and its rules (match the section header and the next line) + sed -i.bak '/^\[sunshine='"$RULESET_NUM"'\]$/,/^add path.*uinput/d' "$DEVFS_RULESET_FILE" + echo "Devfs rules removed from file." + fi +fi + +echo "Removing devfs ruleset from memory..." +devfs rule -s $RULESET_NUM delset 2>/dev/null || true + +# Note: We intentionally do NOT: +# - Remove the 'input' group (other software may use it) + +echo "Cleanup complete." +echo "" +echo "NOTE: The 'input' group has not been removed as other software may use it." +echo "If you wish to remove it manually, run: pw groupdel input" +echo "" diff --git a/src_assets/common/assets/web/PlatformLayout.vue b/src_assets/common/assets/web/PlatformLayout.vue index 31064fec620..49cbacc0fa0 100644 --- a/src_assets/common/assets/web/PlatformLayout.vue +++ b/src_assets/common/assets/web/PlatformLayout.vue @@ -12,6 +12,10 @@ const props = defineProps({ + + diff --git a/src_assets/common/assets/web/apps.html b/src_assets/common/assets/web/apps.html index 8b0cb517c57..e1f7c83dd60 100644 --- a/src_assets/common/assets/web/apps.html +++ b/src_assets/common/assets/web/apps.html @@ -331,7 +331,7 @@

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

{{ $t('apps.env_qres_example') }}
cmd /C <{{ $t('apps.env_qres_path') }}>\QRes.exe /X:%SUNSHINE_CLIENT_WIDTH% /Y:%SUNSHINE_CLIENT_HEIGHT% /R:%SUNSHINE_CLIENT_FPS%
-
{{ $t('apps.env_xrandr_example') }} +
{{ $t('apps.env_xrandr_example') }}
sh -c "xrandr --output HDMI-1 --mode \"${SUNSHINE_CLIENT_WIDTH}x${SUNSHINE_CLIENT_HEIGHT}\" --rate ${SUNSHINE_CLIENT_FPS}"
{{ $t('apps.env_displayplacer_example') }} @@ -442,8 +442,8 @@

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

if (resp) { fetch("./api/apps/" + id, { method: "DELETE", - headers: { - "Content-Type": "application/json" + headers: { + "Content-Type": "application/json" }, }).then((r) => { if (r.status === 200) document.location.reload(); diff --git a/src_assets/common/assets/web/config.html b/src_assets/common/assets/web/config.html index 9c1fb5e9dc7..887062d6c69 100644 --- a/src_assets/common/assets/web/config.html +++ b/src_assets/common/assets/web/config.html @@ -136,6 +136,7 @@

{{ $t('config.configuration') }}

"min_log_level": 2, "global_prep_cmd": [], "notify_pre_releases": "disabled", + "system_tray": "enabled", }, }, { @@ -147,6 +148,7 @@

{{ $t('config.configuration') }}

"ds4_back_as_touchpad_click": "enabled", "motion_as_ds4": "enabled", "touchpad_as_ds4": "enabled", + "ds5_inputtino_randomize_mac": "enabled", "back_button_timeout": -1, "keyboard": "enabled", "key_repeat_delay": 500, @@ -165,6 +167,7 @@

{{ $t('config.configuration') }}

options: { "audio_sink": "", "virtual_sink": "", + "stream_audio": "enabled", "install_steam_audio_drivers": "enabled", "adapter_name": "", "output_name": "", @@ -174,10 +177,10 @@

{{ $t('config.configuration') }}

"dd_refresh_rate_option": "auto", "dd_manual_refresh_rate": "", "dd_hdr_option": "auto", + "dd_wa_hdr_toggle_delay": 0, "dd_config_revert_delay": 3000, "dd_config_revert_on_disconnect": "disabled", "dd_mode_remapping": {"mixed": [], "resolution_only": [], "refresh_rate_only": []}, - "dd_wa_hdr_toggle_delay": 0, "max_bitrate": 0, "minimum_fps_target": 0 }, @@ -302,7 +305,7 @@

{{ $t('config.configuration') }}

return el.id !== "vt" && el.id !== "vaapi"; }); } - if (this.platform === "linux") { + if (this.platform === "freebsd" || this.platform === "linux") { this.tabs = this.tabs.filter((el) => { return el.id !== "amd" && el.id !== "qsv" && el.id !== "vt"; }); diff --git a/src_assets/common/assets/web/configs/tabs/Advanced.vue b/src_assets/common/assets/web/configs/tabs/Advanced.vue index bd11adf2892..37191c7e1b5 100644 --- a/src_assets/common/assets/web/configs/tabs/Advanced.vue +++ b/src_assets/common/assets/web/configs/tabs/Advanced.vue @@ -64,6 +64,10 @@ const config = ref(props.config)