diff --git a/.clang-format b/.clang-format index e72a1e27d11..d6da5d945b8 100644 --- a/.clang-format +++ b/.clang-format @@ -6,27 +6,34 @@ # Generated from CLion C/C++ Code Style settings BasedOnStyle: LLVM AccessModifierOffset: -2 -AlignAfterOpenBracket: DontAlign -AlignConsecutiveAssignments: false +AlignAfterOpenBracket: BlockIndent +AlignConsecutiveAssignments: None +AlignEscapedNewlines: DontAlign AlignOperands: Align AllowAllArgumentsOnNextLine: false AllowAllConstructorInitializersOnNextLine: false AllowAllParametersOfDeclarationOnNextLine: false -AllowShortBlocksOnASingleLine: Always +AllowShortBlocksOnASingleLine: Empty AllowShortCaseLabelsOnASingleLine: false -AllowShortFunctionsOnASingleLine: All -AllowShortIfStatementsOnASingleLine: WithoutElse -AllowShortLambdasOnASingleLine: All +AllowShortEnumsOnASingleLine: false +AllowShortFunctionsOnASingleLine: Empty +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: None AllowShortLoopsOnASingleLine: true AlignTrailingComments: false -AlwaysBreakAfterReturnType: All +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: true AlwaysBreakTemplateDeclarations: MultiLine -BreakBeforeBraces: Custom +BinPackArguments: false +BinPackParameters: false +BracedInitializerIndentWidth: 2 BraceWrapping: AfterCaseLabel: false AfterClass: false AfterControlStatement: Never AfterEnum: false + AfterExternBlock: true AfterFunction: false AfterNamespace: false AfterObjCDeclaration: false @@ -36,39 +43,75 @@ BraceWrapping: IndentBraces: false SplitEmptyFunction: false SplitEmptyRecord: true +BreakArrays: true BreakBeforeBinaryOperators: None +BreakBeforeBraces: Attach BreakBeforeTernaryOperators: false BreakConstructorInitializers: AfterColon BreakInheritanceList: AfterColon ColumnLimit: 0 CompactNamespaces: false ContinuationIndentWidth: 2 +Cpp11BracedListStyle: true +EmptyLineAfterAccessModifier: Never +EmptyLineBeforeAccessModifier: Always +ExperimentalAutoDetectBinPacking: true +FixNamespaceComments: true +IncludeBlocks: Regroup +IndentAccessModifiers: false +IndentCaseBlocks: true IndentCaseLabels: true +IndentExternBlock: Indent +IndentGotoLabels: true IndentPPDirectives: BeforeHash IndentWidth: 2 +IndentWrappedFunctionNames: true +InsertBraces: true +InsertNewlineAtEOF: true KeepEmptyLinesAtTheStartOfBlocks: false +LineEnding: LF MaxEmptyLinesToKeep: 1 NamespaceIndentation: All +ObjCBinPackProtocolList: Never ObjCSpaceAfterProperty: true ObjCSpaceBeforeProtocolList: true +PackConstructorInitializers: Never +PenaltyBreakBeforeFirstCallParameter: 1 +PenaltyBreakComment: 1 +PenaltyBreakString: 1 +PenaltyBreakFirstLessLess: 0 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 100000000 PointerAlignment: Right +ReferenceAlignment: Pointer ReflowComments: true +RemoveBracesLLVM: false +RemoveSemicolon: false +SeparateDefinitionBlocks: Always +SortIncludes: CaseInsensitive +SortUsingDeclarations: Lexicographic SpaceAfterCStyleCast: true SpaceAfterLogicalNot: false -SpaceAfterTemplateKeyword: true +SpaceAfterTemplateKeyword: false SpaceBeforeAssignmentOperators: true +SpaceBeforeCaseColon: false SpaceBeforeCpp11BracedList: true SpaceBeforeCtorInitializerColon: false SpaceBeforeInheritanceColon: false +SpaceBeforeJsonColon: false SpaceBeforeParens: ControlStatements SpaceBeforeRangeBasedForLoopColon: true +SpaceBeforeSquareBrackets: false +SpaceInEmptyBlock: false SpaceInEmptyParentheses: false SpacesBeforeTrailingComments: 2 SpacesInAngles: Never SpacesInCStyleCastParentheses: false SpacesInContainerLiterals: false +SpacesInLineCommentPrefix: + Maximum: 3 + Minimum: 1 SpacesInParentheses: false SpacesInSquareBrackets: false TabWidth: 2 -Cpp11BracedListStyle: false UseTab: Never diff --git a/.codeql-prebuild-cpp-Windows.sh b/.codeql-prebuild-cpp-Windows.sh index b0c7b4cca3a..b860c9e82e8 100644 --- a/.codeql-prebuild-cpp-Windows.sh +++ b/.codeql-prebuild-cpp-Windows.sh @@ -4,13 +4,35 @@ set -e # update pacman pacman --noconfirm -Syu +gcc_version="14.2.0-3" + +broken_deps=( + "mingw-w64-ucrt-x86_64-gcc" + "mingw-w64-ucrt-x86_64-gcc-libs" +) + +tarballs="" +for dep in "${broken_deps[@]}"; do + tarball="${dep}-${gcc_version}-any.pkg.tar.zst" + + # download and install working version + wget https://repo.msys2.org/mingw/ucrt64/${tarball} + + tarballs="${tarballs} ${tarball}" +done + +# install broken dependencies +if [ -n "$tarballs" ]; then + pacman -U --noconfirm ${tarballs} +fi + # install dependencies dependencies=( "git" - "mingw-w64-ucrt-x86_64-boost" "mingw-w64-ucrt-x86_64-cmake" "mingw-w64-ucrt-x86_64-cppwinrt" "mingw-w64-ucrt-x86_64-curl-winssl" + "mingw-w64-ucrt-x86_64-MinHook" "mingw-w64-ucrt-x86_64-miniupnpc" "mingw-w64-ucrt-x86_64-nlohmann-json" "mingw-w64-ucrt-x86_64-nodejs" @@ -20,7 +42,8 @@ dependencies=( "mingw-w64-ucrt-x86_64-opus" "mingw-w64-ucrt-x86_64-toolchain" ) -pacman -S --noconfirm "${dependencies[@]}" + +pacman -Syu --noconfirm --ignore="$(IFS=,; echo "${broken_deps[*]}")" "${dependencies[@]}" # build mkdir -p build diff --git a/.codeql-prebuild-cpp-macOS.sh b/.codeql-prebuild-cpp-macOS.sh index 7fbc090aaa5..a21a69c3aa2 100644 --- a/.codeql-prebuild-cpp-macOS.sh +++ b/.codeql-prebuild-cpp-macOS.sh @@ -1,9 +1,12 @@ # install dependencies for C++ analysis set -e +# setup homebrew for x86_64 +/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" +eval "$(/usr/local/bin/brew shellenv)" + # install dependencies dependencies=( - "boost" "cmake" "miniupnpc" "ninja" diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 6e0a87ff333..6356add1f87 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -94,13 +94,14 @@ body: - Linux - AUR (Third Party) - Linux - deb - Linux - Docker + - Linux - Fedora Copr - Linux - flathub/flatpak - Linux - Homebrew - Linux - LizardByte/pacman-repo - Linux - nixpkgs (Third Party) - Linux - pkg.tar.zst - - Linux - Fedora Copr - Linux - solus (Third Party) + - Linux - Unraid (Third Party) - macOS - Homebrew - macOS - Portfile - Windows - Chocolatey (Third Party) @@ -123,7 +124,7 @@ body: - AMD - Apple Silicon - Intel - - Nvidia + - NVIDIA - none (software encoding) - n/a validations: @@ -149,7 +150,7 @@ body: description: The capture method being used. options: - AVCaptureScreen (macOS) - - KMX (Linux) + - KMS (Linux) - NvFBC (Linux) - wlroots (Linux) - X11 (Linux) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index e9faf5e99d5..50d9f85fa31 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -5,9 +5,15 @@ blank_issues_enabled: false contact_links: + - name: Discussions + url: https://github.com/orgs/LizardByte/discussions + about: Community discussions + - name: Questions + url: https://github.com/orgs/LizardByte/discussions + about: Ask questions + - name: Feature Requests + url: https://github.com/orgs/LizardByte/discussions + about: Request new features - name: Support Center url: https://app.lizardbyte.dev/support about: Official LizardByte support - - name: Discussions - url: https://github.com/orgs/LizardByte/discussions - about: Community discussions, questions, and feature requests diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 9bf13e2a5e9..33d0f2c8227 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -1,12 +1,19 @@ --- name: CI +permissions: + contents: read on: pull_request: - branches: [master] - types: [opened, synchronize, reopened] + branches: + - master + types: + - opened + - synchronize + - reopened push: - branches: [master] + branches: + - master workflow_dispatch: concurrency: @@ -17,7 +24,6 @@ jobs: github_env: name: GitHub Env Debug runs-on: ubuntu-latest - steps: - name: Dump github context run: echo "$GITHUB_CONTEXT" @@ -34,6 +40,8 @@ jobs: release_generate_release_notes: ${{ steps.setup_release.outputs.release_generate_release_notes }} release_tag: ${{ steps.setup_release.outputs.release_tag }} release_version: ${{ steps.setup_release.outputs.release_version }} + permissions: + contents: write # read does not work to check squash and merge details runs-on: ubuntu-latest steps: - name: Checkout @@ -41,50 +49,29 @@ jobs: - name: Setup Release id: setup_release - uses: LizardByte/setup-release-action@v2024.919.143601 + uses: LizardByte/setup-release-action@v2025.426.225 with: github_token: ${{ secrets.GITHUB_TOKEN }} - setup_flatpak_matrix: - name: Setup Flatpak Matrix - runs-on: ubuntu-latest - steps: - - name: Set release details - id: flatpak_matrix - # https://www.cynkra.com/blog/2020-12-23-dynamic-gha - run: | - # determine which architectures to build - if [[ "${{ github.event_name }}" == "push" ]]; then - matrix=$(( - echo '{ "arch" : ["x86_64", "aarch64"] }' - ) | jq -c .) - else - matrix=$(( - echo '{ "arch" : ["x86_64"] }' - ) | jq -c .) - fi - - echo $matrix - echo $matrix | jq . - echo "matrix=$matrix" >> $GITHUB_OUTPUT - - outputs: - matrix: ${{ steps.flatpak_matrix.outputs.matrix }} - build_linux_flatpak: + name: Linux Flatpak env: APP_ID: dev.lizardbyte.app.Sunshine NODE_VERSION: "20" PLATFORM_VERSION: "23.08" - name: Linux Flatpak - runs-on: ubuntu-22.04 - needs: [setup_release, setup_flatpak_matrix] + needs: setup_release + runs-on: ${{ matrix.runner }} strategy: fail-fast: false # false to test all, true to fail entire job if any fail - matrix: ${{fromJson(needs.setup_flatpak_matrix.outputs.matrix)}} - + matrix: + include: + - arch: x86_64 + runner: ubuntu-22.04 + - arch: aarch64 + runner: ubuntu-22.04-arm steps: - name: Maximize build space + if: matrix.arch == 'x86_64' uses: easimon/maximize-build-space@v10 with: root-reserve-mb: 10240 @@ -106,12 +93,10 @@ jobs: node-version: ${{ env.NODE_VERSION }} - name: Install npm dependencies - run: | - npm install --package-lock-only + run: npm install --package-lock-only - name: Debug package-lock.json - run: | - cat package-lock.json + run: cat package-lock.json - name: Setup python id: python @@ -126,8 +111,7 @@ jobs: sudo apt-get update -y sudo apt-get install -y \ cmake \ - flatpak \ - qemu-user-static + flatpak sudo su $(whoami) -c "flatpak --user remote-add --if-not-exists flathub \ https://flathub.org/repo/flathub.flatpakrepo" @@ -143,12 +127,10 @@ jobs: - name: flatpak node generator # https://github.com/flatpak/flatpak-builder-tools/blob/master/node/README.md - run: | - flatpak-node-generator npm package-lock.json + run: flatpak-node-generator npm package-lock.json - name: Debug generated-sources.json - run: | - cat generated-sources.json + run: cat generated-sources.json - name: Cache Flatpak build uses: actions/cache@v4 @@ -193,8 +175,7 @@ jobs: - name: Debug Manifest working-directory: build - run: | - cat ${APP_ID}.yml + run: cat ${APP_ID}.yml - name: Build Linux Flatpak working-directory: build @@ -227,56 +208,27 @@ jobs: - name: Lint Flatpak working-directory: build run: | + exceptions_file="${{ github.workspace }}/packaging/linux/flatpak/exceptions.json" + echo "Linting flatpak manifest" flatpak run --command=flatpak-builder-lint org.flatpak.Builder \ - manifest ${APP_ID}.yml > _flatpak-lint-exceptions_manifest.json || true + --exceptions \ + --user-exceptions "${exceptions_file}" \ + manifest \ + ${APP_ID}.yml echo "Linting flatpak repo" # TODO: add arg # --mirror-screenshots-url=https://dl.flathub.org/media \ flatpak run --command=flatpak-builder-lint org.flatpak.Builder \ - repo repo > _flatpak-lint-exceptions_repo.json || true - - checks=(manifest repo) - exit_code=0 - - # check if files are equal - for check in "${checks[@]}"; do - echo "Validating $check" - - # load baseline and result files - baseline="${{ github.workspace }}/packaging/linux/flatpak/flatpak-lint-baseline_${check}.json" - result="_flatpak-lint-exceptions_${check}.json" - - # Extract errors from both JSON files - readarray -t result_errors < <(jq -r '.errors[]' "$result") - readarray -t baseline_errors < <(jq -r '.errors[]' "$baseline") - - # Loop through result errors and check against baseline errors - for error in "${result_errors[@]}"; do - if printf '%s\n' "${baseline_errors[@]}" | grep -q -F "$error"; then - echo "::warning:: '$error'" - else - echo "::error:: '$error'" - exit_code=1 - fi - done - done - - # if exit code is not 0, print results - if [ $exit_code -ne 0 ]; then - echo "Manifest lint results:" - cat _flatpak-lint-exceptions_manifest.json - echo "Repo lint results:" - cat _flatpak-lint-exceptions_repo.json - fi - - # exit with the correct code - exit $exit_code + --exceptions \ + --user-exceptions "${exceptions_file}" \ + repo \ + repo - name: Package Flathub repo archive # copy files required to generate the Flathub repo - if: ${{ matrix.arch == 'x86_64' }} + if: matrix.arch == 'x86_64' run: | mkdir -p flathub/modules cp ./build/generated-sources.json ./flathub/ @@ -296,10 +248,11 @@ jobs: with: name: sunshine-linux-flatpak-${{ matrix.arch }} path: artifacts/ + if-no-files-found: error - name: Create/Update GitHub Release - if: ${{ needs.setup_release.outputs.publish_release == 'true' }} - uses: LizardByte/create-release-action@v2024.919.143026 + if: needs.setup_release.outputs.publish_release == 'true' + uses: LizardByte/create-release-action@v2025.426.1549 with: allowUpdates: true body: ${{ needs.setup_release.outputs.release_body }} @@ -312,7 +265,7 @@ jobs: build_linux: name: Linux ${{ matrix.type }} runs-on: ubuntu-${{ matrix.dist }} - needs: [setup_release] + needs: setup_release strategy: fail-fast: false # false to test all, true to fail entire job if any fail matrix: @@ -320,7 +273,6 @@ jobs: - type: AppImage EXTRA_ARGS: '--appimage-build' dist: 22.04 - steps: - name: Maximize build space uses: easimon/maximize-build-space@v10 @@ -395,14 +347,13 @@ jobs: --ubuntu-test-repo ${{ matrix.EXTRA_ARGS }} - name: Set AppImage Version - if: | - matrix.type == 'AppImage' + if: matrix.type == 'AppImage' run: | version=${{ needs.setup_release.outputs.release_tag }} echo "VERSION=${version}" >> $GITHUB_ENV - name: Package Linux - AppImage - if: ${{ matrix.type == 'AppImage' }} + if: matrix.type == 'AppImage' working-directory: build run: | # install sunshine to the DESTDIR @@ -447,7 +398,7 @@ jobs: rm -rf ./build/cuda - name: Verify AppImage - if: ${{ matrix.type == 'AppImage' }} + if: matrix.type == 'AppImage' run: | wget https://github.com/TheAssassin/appimagelint/releases/download/continuous/appimagelint-x86_64.AppImage chmod +x appimagelint-x86_64.AppImage @@ -459,6 +410,7 @@ jobs: with: name: sunshine-linux-${{ matrix.type }}-${{ matrix.dist }} path: artifacts/ + if-no-files-found: error - name: Install test deps run: | @@ -479,12 +431,14 @@ jobs: Xvfb ${DISPLAY} -screen 0 1024x768x24 & sleep 5 # give Xvfb time to start - ./test_sunshine --gtest_color=yes + ./test_sunshine --gtest_color=yes --gtest_output=xml:test_results.xml - name: Generate gcov report - # any except canceled or skipped - if: always() && (steps.test.outcome == 'success' || steps.test.outcome == 'failure') id: test_report + # any except canceled or skipped + if: >- + always() && + (steps.test.outcome == 'success' || steps.test.outcome == 'failure') working-directory: build run: | ${{ steps.python.outputs.python-path }} -m pip install gcovr @@ -496,6 +450,22 @@ jobs: --xml-pretty \ -o coverage.xml + - name: Upload test results to Codecov + # any except canceled or skipped + if: >- + always() && + (steps.test.outcome == 'success' || steps.test.outcome == 'failure') && + startsWith(github.repository, 'LizardByte/') + uses: codecov/test-results-action@v1 + with: + disable_search: true + fail_ci_if_error: true + files: ./build/tests/test_results.xml + flags: ${{ runner.os }} + handle_no_reports_found: true + token: ${{ secrets.CODECOV_TOKEN }} + verbose: true + - name: Upload coverage # any except canceled or skipped if: >- @@ -512,8 +482,8 @@ jobs: verbose: true - name: Create/Update GitHub Release - if: ${{ needs.setup_release.outputs.publish_release == 'true' }} - uses: LizardByte/create-release-action@v2024.919.143026 + if: needs.setup_release.outputs.publish_release == 'true' + uses: LizardByte/create-release-action@v2025.426.1549 with: allowUpdates: true body: ${{ needs.setup_release.outputs.release_body }} @@ -524,7 +494,8 @@ jobs: token: ${{ secrets.GH_BOT_TOKEN }} build_homebrew: - needs: [setup_release] + name: Homebrew (${{ matrix.os_name }}-${{ matrix.os_version }}${{ matrix.release == true && ' (Release)' || '' }}) + needs: setup_release strategy: fail-fast: false # false to test all, true to fail entire job if any fail matrix: @@ -540,28 +511,36 @@ jobs: - os_version: "latest" # this job will only configure the formula for release, no validation os_name: "ubuntu" release: true - name: Homebrew (${{ matrix.os_name }}-${{ matrix.os_version }}${{ matrix.release == true && ' (Release)' || '' }}) runs-on: ${{ matrix.os_name }}-${{ matrix.os_version }} - steps: - name: Checkout uses: actions/checkout@v4 - - name: Fix python + - name: Fix homebrew python if: matrix.os_name == 'macos' && matrix.os_version == '13' run: | rm '/usr/local/bin/2to3' rm '/usr/local/bin/2to3-3.12' rm '/usr/local/bin/idle3' rm '/usr/local/bin/idle3.12' + rm '/usr/local/bin/idle3.13' rm '/usr/local/bin/pydoc3' rm '/usr/local/bin/pydoc3.12' + rm '/usr/local/bin/pydoc3.13' rm '/usr/local/bin/python3' - rm '/usr/local/bin/python3-config' rm '/usr/local/bin/python3.12' + rm '/usr/local/bin/python3.13' + rm '/usr/local/bin/python3-config' rm '/usr/local/bin/python3.12-config' + rm '/usr/local/bin/python3.13-config' brew install python + - name: Setup python + id: python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Configure formula run: | # variables for formula @@ -620,16 +599,15 @@ jobs: cat ./homebrew/sunshine.rb - name: Upload Artifacts - if: ${{ matrix.release }} + if: matrix.release uses: actions/upload-artifact@v4 with: name: sunshine-homebrew path: homebrew/ + if-no-files-found: error - name: Setup Xvfb - if: | - matrix.release != true && - runner.os == 'Linux' + if: matrix.release != true && runner.os == 'Linux' run: | sudo apt-get update -y sudo apt-get install -y \ @@ -641,9 +619,9 @@ jobs: echo "DISPLAY=${DISPLAY}" >> $GITHUB_ENV - name: Validate Homebrew Formula - if: | - matrix.release != true - uses: LizardByte/homebrew-release-action@v2024.1115.14934 + id: test + if: matrix.release != true + uses: LizardByte/homebrew-release-action@v2025.503.165455 with: formula_file: ${{ github.workspace }}/homebrew/sunshine.rb git_email: ${{ secrets.GH_BOT_EMAIL }} @@ -652,11 +630,70 @@ jobs: token: ${{ secrets.GH_BOT_TOKEN }} validate: true + - name: Generate gcov report + id: test_report + # any except canceled or skipped + # TODO: fix coverage, no .gcno files are being created + # TODO: .gcno files are supposed to be created next to .o files + if: false + # if: >- + # always() && + # matrix.release != true && + # (steps.test.outcome == 'success' || steps.test.outcome == 'failure') + run: | + cp -rf ${{ steps.test.outputs.buildpath }}/build/ ./build/ + cd build + ls -Ra + + ${{ steps.python.outputs.python-path }} -m pip install gcovr + ${{ steps.python.outputs.python-path }} -m gcovr . -r ../src \ + --exclude-noncode-lines \ + --exclude-throw-branches \ + --exclude-unreachable-branches \ + --verbose \ + --xml-pretty \ + -o coverage.xml + + - name: Upload test results to Codecov + # any except canceled or skipped + if: >- + always() && + matrix.release != true && + (steps.test.outcome == 'success' || steps.test.outcome == 'failure') && + startsWith(github.repository, 'LizardByte/') + uses: codecov/test-results-action@v1 + with: + disable_search: true + fail_ci_if_error: true + files: ${{ steps.test.outputs.testpath }}/test_results.xml + flags: ${{ matrix.os_name }}-${{ matrix.os_version }} (Homebrew) + handle_no_reports_found: true + token: ${{ secrets.CODECOV_TOKEN }} + verbose: true + + - name: Upload coverage + # any except canceled or skipped + # TODO: enable this once coverage report is fixed + if: false + # if: >- + # always() && + # matrix.release != true && + # (steps.test_report.outcome == 'success') && + # startsWith(github.repository, 'LizardByte/') + uses: codecov/codecov-action@v5 + with: + disable_search: true + fail_ci_if_error: true + files: ./build/coverage.xml + flags: ${{ matrix.os_name }}-${{ matrix.os_version }} (Homebrew) + token: ${{ secrets.CODECOV_TOKEN }} + verbose: true + - name: Create/Update GitHub Release if: >- matrix.release && needs.setup_release.outputs.publish_release == 'true' - uses: LizardByte/create-release-action@v2024.919.143026 + uses: LizardByte/create-release-action@v2025.426.1549 with: allowUpdates: true artifacts: '${{ github.workspace }}/homebrew/*' @@ -670,8 +707,7 @@ jobs: - name: Patch homebrew formula # create beta version of the formula # don't run this on macOS, as the sed command fails - if: >- - matrix.release + if: matrix.release run: | # variables formula_file="homebrew/sunshine-beta.rb" @@ -692,7 +728,7 @@ jobs: github.repository_owner == 'LizardByte' && matrix.release && needs.setup_release.outputs.publish_release == 'true' - uses: LizardByte/homebrew-release-action@v2024.1115.14934 + uses: LizardByte/homebrew-release-action@v2025.503.165455 with: formula_file: ${{ github.workspace }}/homebrew/sunshine-beta.rb git_email: ${{ secrets.GH_BOT_EMAIL }} @@ -701,266 +737,10 @@ jobs: token: ${{ secrets.GH_BOT_TOKEN }} validate: false - build_mac_port: - needs: [setup_release] - strategy: - fail-fast: false # false to test all, true to fail entire job if any fail - matrix: - include: - # https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#standard-github-hosted-runners-for-public-repositories - # while GitHub has larger macOS runners, they are not available for our repos :( - - os_version: "13" - release: true - - os_version: "14" - name: Macports (macOS-${{ matrix.os_version }}) - runs-on: macos-${{ matrix.os_version }} - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Checkout ports - uses: actions/checkout@v4 - with: - repository: macports/macports-ports - fetch-depth: 64 - path: ports - - - name: Checkout mpbb - uses: actions/checkout@v4 - with: - repository: macports/mpbb - path: mpbb - - - name: Setup Dependencies Macports - run: | - # install dependencies using homebrew - brew install cmake - - - name: Setup python - id: python - uses: actions/setup-python@v5 - with: - python-version: '3.11' - - - name: Configure Portfile - run: | - # variables for Portfile - branch="${{ github.head_ref }}" - commit=${{ needs.setup_release.outputs.release_commit }} - - # check the branch variable - if [ -z "$branch" ] - then - echo "This is a PUSH event" - branch="${{ github.ref_name }}" - build_version=${{ needs.setup_release.outputs.release_tag }} - clone_url=${{ github.event.repository.clone_url }} - else - echo "This is a PR event" - clone_url=${{ github.event.pull_request.head.repo.clone_url }} - fi - echo "Commit: ${commit}" - echo "Clone URL: ${clone_url}" - - mkdir -p build - cmake \ - -B build \ - -S . \ - -DBUILD_VERSION=${build_version} \ - -DGITHUB_BRANCH=${branch} \ - -DGITHUB_COMMIT=${commit} \ - -DGITHUB_CLONE_URL=${clone_url} \ - -DSUNSHINE_CONFIGURE_PORTFILE=ON \ - -DSUNSHINE_CONFIGURE_ONLY=ON - - # copy Portfile to artifacts - mkdir -p artifacts - cp -f ./build/Portfile ./artifacts/ - - # copy Portfile to ports - mkdir -p ./ports/multimedia/Sunshine - cp -f ./build/Portfile ./ports/multimedia/Sunshine/Portfile - - # testing - cat ./artifacts/Portfile - - - name: Bootstrap MacPorts - run: | - . ports/.github/workflows/bootstrap.sh - - # Add getopt, mpbb and the MacPorts paths to $PATH for the subsequent steps. - echo "/opt/mports/bin" >> $GITHUB_PATH - echo "${PWD}/mpbb" >> $GITHUB_PATH - echo "/opt/local/bin" >> $GITHUB_PATH - echo "/opt/local/sbin" >> $GITHUB_PATH - - - name: Run port lint - run: | - port -q lint "Sunshine" - - - name: Build port - env: - subportlist: ${{ steps.subportlist.outputs.subportlist }} - id: build - run: | - subport="Sunshine" - - workdir="/tmp/mpbb/$subport" - mkdir -p "$workdir/logs" - - echo "::group::Installing dependencies" - sudo mpbb \ - --work-dir "$workdir" \ - install-dependencies \ - "$subport" - echo "::endgroup::" - - echo "::group::Installing ${subport}" - sudo mpbb \ - --work-dir "$workdir" \ - install-port \ - --source \ - "$subport" - echo "::endgroup::" - - - name: Build Logs - if: always() - run: | - logfile="/opt/local/var/macports/logs/_Users_runner_work_Sunshine_Sunshine_ports_multimedia_Sunshine/Sunshine/main.log" - cat "$logfile" - sudo mv "${logfile}" "${logfile}.bak" - - - name: Upload Artifacts - if: ${{ matrix.release }} - uses: actions/upload-artifact@v4 - with: - name: sunshine-macports - path: artifacts/ - - - name: Fix permissions - run: | - # https://apple.stackexchange.com/questions/362865/macos-list-apps-authorized-for-full-disk-access - # https://github.com/actions/runner-images/issues/9529 - # https://github.com/actions/runner-images/pull/9530 - - # function to execute sql query for each value - function execute_sql_query { - local value=$1 - local dbPath=$2 - - echo "Executing SQL query for value: $value" - sudo sqlite3 "$dbPath" "INSERT OR IGNORE INTO access VALUES($value);" - } - - # Find all provisioner paths and store them in an array - readarray -t provisioner_paths < <(sudo find /opt /usr -name provisioner) - echo "Provisioner paths: ${provisioner_paths[@]}" - - # Create an empty array - declare -a values=() - - # Loop through the provisioner paths and add them to the values array - for p_path in "${provisioner_paths[@]}"; do - # Adjust the service name and other parameters as needed - values+=("'kTCCServiceAccessibility','${p_path}',1,2,4,1,NULL,NULL,0,'UNUSED',NULL,NULL,1592919552") - values+=("'kTCCServiceScreenCapture','${p_path}',1,2,4,1,NULL,NULL,0,'UNUSED',NULL,0,1687786159") - done - - echo "Values: ${values[@]}" - - if [[ "${{ matrix.os_version }}" == "14" ]]; then - # TCC access table in Sonoma has extra 4 columns: pid, pid_version, boot_uuid, last_reminded - for i in "${!values[@]}"; do - values[$i]="${values[$i]},NULL,NULL,'UNUSED',${values[$i]##*,}" - done - fi - - # system and user databases - dbPaths=( - "/Library/Application Support/com.apple.TCC/TCC.db" - "$HOME/Library/Application Support/com.apple.TCC/TCC.db" - ) - - for value in "${values[@]}"; do - for dbPath in "${dbPaths[@]}"; do - echo "Column names for $dbPath" - echo "-------------------" - sudo sqlite3 "$dbPath" "PRAGMA table_info(access);" - echo "Current permissions for $dbPath" - echo "-------------------" - sudo sqlite3 "$dbPath" "SELECT * FROM access WHERE service='kTCCServiceScreenCapture';" - execute_sql_query "$value" "$dbPath" - echo "Updated permissions for $dbPath" - echo "-------------------" - sudo sqlite3 "$dbPath" "SELECT * FROM access WHERE service='kTCCServiceScreenCapture';" - done - done - - - name: Run tests - id: test - timeout-minutes: 10 - working-directory: - /opt/local/var/macports/build/_Users_runner_work_Sunshine_Sunshine_ports_multimedia_Sunshine/Sunshine/work/build/tests - run: | - sudo ./test_sunshine --gtest_color=yes - - - name: Generate gcov report - # any except canceled or skipped - if: always() && (steps.test.outcome == 'success' || steps.test.outcome == 'failure') - id: test_report - working-directory: - /opt/local/var/macports/build/_Users_runner_work_Sunshine_Sunshine_ports_multimedia_Sunshine/Sunshine/work - run: | - base_dir=$(pwd) - build_dir=${base_dir}/build - - # get the directory name that starts with Sunshine-* - dir=$(ls -d Sunshine-*) - - cd ${build_dir} - ${{ steps.python.outputs.python-path }} -m pip install gcovr - sudo ${{ steps.python.outputs.python-path }} -m gcovr . -r ../${dir}/src \ - --exclude-noncode-lines \ - --exclude-throw-branches \ - --exclude-unreachable-branches \ - --gcov-object-directory $(pwd) \ - --verbose \ - --xml-pretty \ - -o ${{ github.workspace }}/build/coverage.xml - - - name: Upload coverage - # any except canceled or skipped - if: >- - always() && - (steps.test_report.outcome == 'success') && - startsWith(github.repository, 'LizardByte/') - uses: codecov/codecov-action@v5 - with: - disable_search: true - fail_ci_if_error: false # todo: re-enable this when action is fixed - files: ./build/coverage.xml - flags: ${{ runner.os }}-${{ matrix.os_version }} - token: ${{ secrets.CODECOV_TOKEN }} - verbose: true - - - name: Create/Update GitHub Release - if: ${{ needs.setup_release.outputs.publish_release == 'true' }} - uses: LizardByte/create-release-action@v2024.919.143026 - with: - allowUpdates: true - body: ${{ needs.setup_release.outputs.release_body }} - generateReleaseNotes: ${{ needs.setup_release.outputs.release_generate_release_notes }} - name: ${{ needs.setup_release.outputs.release_tag }} - prerelease: true - tag: ${{ needs.setup_release.outputs.release_tag }} - token: ${{ secrets.GH_BOT_TOKEN }} - build_win: name: Windows + needs: setup_release runs-on: windows-2019 - needs: [setup_release] - steps: - name: Checkout uses: actions/checkout@v4 @@ -1072,27 +852,59 @@ jobs: Get-Content -Path monitor_list.txt - name: Setup Dependencies Windows + # if a dependency needs to be pinned, see https://github.com/LizardByte/build-deps/pull/186 uses: msys2/setup-msys2@v2 with: msystem: ucrt64 update: true install: >- - git - mingw-w64-ucrt-x86_64-boost - mingw-w64-ucrt-x86_64-cmake - mingw-w64-ucrt-x86_64-cppwinrt - mingw-w64-ucrt-x86_64-curl-winssl - mingw-w64-ucrt-x86_64-graphviz - mingw-w64-ucrt-x86_64-miniupnpc - mingw-w64-ucrt-x86_64-nlohmann-json - mingw-w64-ucrt-x86_64-nodejs - mingw-w64-ucrt-x86_64-nsis - mingw-w64-ucrt-x86_64-onevpl - mingw-w64-ucrt-x86_64-openssl - mingw-w64-ucrt-x86_64-opus - mingw-w64-ucrt-x86_64-toolchain wget + - name: Update Windows dependencies + env: + gcc_version: "14.2.0-3" + shell: msys2 {0} + run: | + broken_deps=( + "mingw-w64-ucrt-x86_64-gcc" + "mingw-w64-ucrt-x86_64-gcc-libs" + ) + + tarballs="" + for dep in "${broken_deps[@]}"; do + tarball="${dep}-${gcc_version}-any.pkg.tar.zst" + + # download and install working version + wget https://repo.msys2.org/mingw/ucrt64/${tarball} + + tarballs="${tarballs} ${tarball}" + done + + # install broken dependencies + if [ -n "$tarballs" ]; then + pacman -U --noconfirm ${tarballs} + fi + + # install dependencies + dependencies=( + "git" + "mingw-w64-ucrt-x86_64-cmake" + "mingw-w64-ucrt-x86_64-cppwinrt" + "mingw-w64-ucrt-x86_64-curl-winssl" + "mingw-w64-ucrt-x86_64-graphviz" + "mingw-w64-ucrt-x86_64-MinHook" + "mingw-w64-ucrt-x86_64-miniupnpc" + "mingw-w64-ucrt-x86_64-nlohmann-json" + "mingw-w64-ucrt-x86_64-nodejs" + "mingw-w64-ucrt-x86_64-nsis" + "mingw-w64-ucrt-x86_64-onevpl" + "mingw-w64-ucrt-x86_64-openssl" + "mingw-w64-ucrt-x86_64-opus" + "mingw-w64-ucrt-x86_64-toolchain" + ) + + pacman -Syu --noconfirm --ignore="$(IFS=,; echo "${broken_deps[*]}")" "${dependencies[@]}" + - name: Install Doxygen # GCC compiled doxygen has issues when running graphviz env: @@ -1140,6 +952,7 @@ jobs: env: BRANCH: ${{ github.head_ref || github.ref_name }} BUILD_VERSION: ${{ needs.setup_release.outputs.release_tag }} + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} COMMIT: ${{ needs.setup_release.outputs.release_commit }} run: | mkdir -p build @@ -1152,8 +965,7 @@ jobs: -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' \ - -DTESTS_SOFTWARE_ENCODER_UNAVAILABLE='skip' + -DSUNSHINE_PUBLISHER_ISSUE_URL='https://app.lizardbyte.dev/support' ninja -C build - name: Package Windows @@ -1175,12 +987,12 @@ jobs: shell: msys2 {0} working-directory: build/tests run: | - ./test_sunshine.exe --gtest_color=yes + ./test_sunshine.exe --gtest_color=yes --gtest_output=xml:test_results.xml - name: Generate gcov report + id: test_report # any except canceled or skipped if: always() && (steps.test.outcome == 'success' || steps.test.outcome == 'failure') - id: test_report shell: msys2 {0} working-directory: build run: | @@ -1193,6 +1005,22 @@ jobs: --xml-pretty \ -o coverage.xml + - name: Upload test results to Codecov + # any except canceled or skipped + if: >- + always() && + (steps.test.outcome == 'success' || steps.test.outcome == 'failure') && + startsWith(github.repository, 'LizardByte/') + uses: codecov/test-results-action@v1 + with: + disable_search: true + fail_ci_if_error: true + files: ./build/tests/test_results.xml + flags: ${{ runner.os }} + handle_no_reports_found: true + token: ${{ secrets.CODECOV_TOKEN }} + verbose: true + - name: Upload coverage # any except canceled or skipped if: >- @@ -1226,10 +1054,11 @@ jobs: with: name: sunshine-windows path: artifacts/ + if-no-files-found: error - name: Create/Update GitHub Release - if: ${{ needs.setup_release.outputs.publish_release == 'true' }} - uses: LizardByte/create-release-action@v2024.919.143026 + if: needs.setup_release.outputs.publish_release == 'true' + uses: LizardByte/create-release-action@v2025.426.1549 with: allowUpdates: true body: ${{ needs.setup_release.outputs.release_body }} diff --git a/.github/workflows/ci-copr.yml b/.github/workflows/ci-copr.yml index db02c9123dc..d30e4d41c05 100644 --- a/.github/workflows/ci-copr.yml +++ b/.github/workflows/ci-copr.yml @@ -1,5 +1,7 @@ --- name: CI Copr +permissions: + contents: read on: pull_request: @@ -26,7 +28,7 @@ jobs: github_org_owner: LizardByte copr_ownername: lizardbyte auto_update_package: true - job_timeout: 60 + job_timeout: 90 secrets: COPR_BETA_WEBHOOK_TOKEN: ${{ secrets.COPR_BETA_WEBHOOK_TOKEN }} COPR_STABLE_WEBHOOK_TOKEN: ${{ secrets.COPR_STABLE_WEBHOOK_TOKEN }} diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml index fd9677092e9..d307d36931f 100644 --- a/.github/workflows/ci-docker.yml +++ b/.github/workflows/ci-docker.yml @@ -1,5 +1,5 @@ --- -# This action is centrally managed in https://github.com//.github/ +# This workflow is centrally managed in https://github.com//.github/ # Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in # the above-mentioned repo. @@ -19,13 +19,20 @@ # GitHub runner. name: CI Docker +permissions: + contents: read on: pull_request: - branches: [master] - types: [opened, synchronize, reopened] + branches: + - master + types: + - opened + - synchronize + - reopened push: - branches: [master] + branches: + - master workflow_dispatch: concurrency: @@ -97,10 +104,9 @@ jobs: solution: ${{ steps.find_dotnet.outputs.solution }} setup_release: - if: ${{ needs.check_dockerfiles.outputs.dockerfiles }} name: Setup Release - needs: - - check_dockerfiles + if: needs.check_dockerfiles.outputs.dockerfiles + needs: check_dockerfiles outputs: publish_release: ${{ steps.setup_release.outputs.publish_release }} release_body: ${{ steps.setup_release.outputs.release_body }} @@ -108,6 +114,8 @@ jobs: release_generate_release_notes: ${{ steps.setup_release.outputs.release_generate_release_notes }} release_tag: ${{ steps.setup_release.outputs.release_tag }} release_version: ${{ steps.setup_release.outputs.release_version }} + permissions: + contents: write # read does not work to check squash and merge details runs-on: ubuntu-latest steps: - name: Checkout @@ -115,23 +123,24 @@ jobs: - name: Setup Release id: setup_release - uses: LizardByte/setup-release-action@v2024.919.143601 + uses: LizardByte/setup-release-action@v2025.426.225 with: dotnet: ${{ needs.check_dockerfiles.outputs.dotnet }} github_token: ${{ secrets.GITHUB_TOKEN }} docker: - needs: [check_dockerfiles, setup_release] - if: ${{ needs.check_dockerfiles.outputs.dockerfiles }} - runs-on: ubuntu-latest + name: Docker${{ matrix.tag }} + if: needs.check_dockerfiles.outputs.dockerfiles + needs: + - check_dockerfiles + - setup_release permissions: packages: write contents: write + runs-on: ubuntu-22.04 strategy: fail-fast: false matrix: ${{ fromJson(needs.check_dockerfiles.outputs.matrix) }} - name: Docker${{ matrix.tag }} - steps: - name: Maximize build space uses: easimon/maximize-build-space@v10 @@ -256,14 +265,14 @@ jobs: Docker-buildx${{ matrix.tag }}- - name: Log in to Docker Hub - if: ${{ needs.setup_release.outputs.publish_release == 'true' }} # PRs do not have access to secrets + if: needs.setup_release.outputs.publish_release == 'true' # PRs do not have access to secrets uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - name: Log in to the Container registry - if: ${{ needs.setup_release.outputs.publish_release == 'true' }} # PRs do not have access to secrets + if: needs.setup_release.outputs.publish_release == 'true' # PRs do not have access to secrets uses: docker/login-action@v3 with: registry: ghcr.io @@ -271,7 +280,7 @@ jobs: password: ${{ secrets.GH_BOT_TOKEN }} - name: Build artifacts - if: ${{ steps.prepare.outputs.artifacts == 'true' }} + if: steps.prepare.outputs.artifacts == 'true' id: build_artifacts uses: docker/build-push-action@v6 with: @@ -314,27 +323,40 @@ jobs: no-cache-filters: ${{ steps.prepare.outputs.no_cache_filters }} - name: Arrange Artifacts - if: ${{ steps.prepare.outputs.artifacts == 'true' }} + if: steps.prepare.outputs.artifacts == 'true' working-directory: artifacts run: | + # debug directory + echo "Current directory: $(pwd)" + echo "Directory contents: $(ls -Ra)" + # artifacts will be in sub directories named after the docker target platform, e.g. `linux_amd64` # so move files to the artifacts directory # https://unix.stackexchange.com/a/52816 - find ./ -type f -exec mv -t ./ -n '{}' + + find \ + ./ \ + -maxdepth 2 \ + -mindepth 2 \ + -type f \ + -not -name 'provenance.json' \ + -exec mv -t ./ -n '{}' + # remove provenance file rm -f ./provenance.json - name: Upload Artifacts - if: ${{ steps.prepare.outputs.artifacts == 'true' }} + if: steps.prepare.outputs.artifacts == 'true' uses: actions/upload-artifact@v4 with: name: Docker${{ matrix.tag }} path: artifacts/ + if-no-files-found: error - name: Create/Update GitHub Release - if: ${{ needs.setup_release.outputs.publish_release == 'true' && steps.prepare.outputs.artifacts == 'true' }} - uses: LizardByte/create-release-action@v2024.919.143026 + if: > + needs.setup_release.outputs.publish_release == 'true' && + steps.prepare.outputs.artifacts == 'true' + uses: LizardByte/create-release-action@v2025.426.1549 with: allowUpdates: true artifacts: "*artifacts/*" @@ -346,7 +368,9 @@ jobs: token: ${{ secrets.GH_BOT_TOKEN }} - name: Update Docker Hub Description - if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }} + if: > + github.event_name == 'push' && + github.ref == 'refs/heads/master' uses: peter-evans/dockerhub-description@v4 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 60dbef1cc07..c9949dd37b0 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,17 +1,21 @@ --- -# This action is centrally managed in https://github.com//.github/ +# This workflow is centrally managed in https://github.com//.github/ # Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in # the above-mentioned repo. # This workflow will analyze all supported languages in the repository using CodeQL Analysis. name: "CodeQL" +permissions: + contents: read on: push: - branches: ["master"] + branches: + - master pull_request: - branches: ["master"] + branches: + - master schedule: - cron: '00 12 * * 0' # every Sunday at 12:00 UTC @@ -22,14 +26,17 @@ concurrency: jobs: languages: name: Get language matrix - runs-on: ubuntu-latest outputs: matrix: ${{ steps.lang.outputs.result }} continue: ${{ steps.continue.outputs.result }} + runs-on: ubuntu-latest steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Get repo languages - uses: actions/github-script@v7 id: lang + uses: actions/github-script@v7 with: script: | // CodeQL supports ['cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby', 'swift'] @@ -51,32 +58,62 @@ jobs: "include": [] } + // Track languages we've already added to avoid duplicates + const addedLanguages = new Set() + + // Check if workflow files exist to determine if we should add actions language + const fs = require('fs'); + const hasYmlFiles = fs.existsSync('.github/workflows') && + fs.readdirSync('.github/workflows').some(file => file.endsWith('.yml') || file.endsWith('.yaml')); + + // Add actions language if workflow files exist + if (hasYmlFiles) { + console.log('Found GitHub Actions workflow files. Adding actions to the matrix.'); + matrix['include'].push({ + "category": "/language:actions", + "language": "actions", + "name": "actions", + "os": "ubuntu-latest" + }); + } + for (let [key, value] of Object.entries(response.data)) { // remap language if (remap_languages[key.toLowerCase()]) { console.log(`Remapping language: ${key} to ${remap_languages[key.toLowerCase()]}`) key = remap_languages[key.toLowerCase()] } - if (supported_languages.includes(key.toLowerCase())) { - console.log(`Found supported language: ${key}`) + + const normalizedKey = key.toLowerCase() + + if (supported_languages.includes(normalizedKey) && !addedLanguages.has(normalizedKey)) { + // Mark this language as added + addedLanguages.add(normalizedKey) + + console.log(`Found supported language: ${normalizedKey}`) let osList = ['ubuntu-latest']; - if (key.toLowerCase() === 'swift') { + if (normalizedKey === 'swift') { osList = ['macos-latest']; - } else if (key.toLowerCase() === 'cpp') { - // TODO: update macos to latest after the below issue is resolved - // https://github.com/github/codeql-action/issues/2266 - osList = ['macos-13', 'ubuntu-latest', 'windows-latest']; + } else if (normalizedKey === 'cpp') { + osList = ['macos-latest', 'ubuntu-latest', 'windows-latest']; } for (let os of osList) { // set name for matrix - if (osList.length == 1) { - name = key.toLowerCase() - } else { - name = `${key.toLowerCase()}, ${os}` + let name = osList.length === 1 ? normalizedKey : `${normalizedKey}, ${os}` + + // set category for matrix + let category = `/language:${normalizedKey}` + if (normalizedKey === 'cpp') { + category = `/language:cpp-${os.split('-')[0]}` } // add to matrix - matrix['include'].push({"language": key.toLowerCase(), "os": os, "name": name}) + matrix['include'].push({ + "category": category, + "language": normalizedKey, + "name": name, + "os": os + }) } } } @@ -87,8 +124,8 @@ jobs: return matrix - name: Continue - uses: actions/github-script@v7 id: continue + uses: actions/github-script@v7 with: script: | // if matrix['include'] is an empty list return false, otherwise true @@ -102,24 +139,22 @@ jobs: analyze: name: Analyze (${{ matrix.name }}) - if: ${{ needs.languages.outputs.continue == 'true' }} + if: needs.languages.outputs.continue == 'true' defaults: run: shell: ${{ matrix.os == 'windows-latest' && 'msys2 {0}' || 'bash' }} env: GITHUB_CODEQL_BUILD: true - needs: [languages] - runs-on: ${{ matrix.os || 'ubuntu-latest' }} - timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} + needs: languages permissions: actions: read contents: read security-events: write - + runs-on: ${{ matrix.os || 'ubuntu-latest' }} strategy: fail-fast: false matrix: ${{ fromJson(needs.languages.outputs.matrix) }} - + timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} steps: - name: Maximize build space if: >- @@ -167,8 +202,7 @@ jobs: - third-party # Pre autobuild - # create a file named .codeql-prebuild-${{ matrix.language }}.sh in the root of your repository - # create a file named .codeql-build-${{ matrix.language }}.sh in the root of your repository + # create a file named .codeql-prebuild-${{ matrix.language }}-${{ runner.os }}.sh in the root of your repository - name: Prebuild id: prebuild run: | @@ -187,7 +221,7 @@ jobs: - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 with: - category: "/language:${{matrix.language}}" + category: "${{ matrix.category }}" output: sarif-results upload: failure-only @@ -204,6 +238,7 @@ jobs: - name: Upload SARIF uses: github/codeql-action/upload-sarif@v3 with: + category: "${{ matrix.category }}" sarif_file: sarif-results/${{ matrix.language }}.sarif - name: Upload loc as a Build Artifact @@ -211,4 +246,5 @@ jobs: with: name: sarif-results-${{ matrix.language }}-${{ runner.os }} path: sarif-results + if-no-files-found: error retention-days: 1 diff --git a/.github/workflows/common-lint.yml b/.github/workflows/common-lint.yml index 10692ad934b..524be6ff725 100644 --- a/.github/workflows/common-lint.yml +++ b/.github/workflows/common-lint.yml @@ -1,16 +1,22 @@ --- -# This action is centrally managed in https://github.com//.github/ +# This workflow is centrally managed in https://github.com//.github/ # Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in # the above-mentioned repo. # Common linting. name: common lint +permissions: + contents: read on: pull_request: - branches: [master] - types: [opened, synchronize, reopened] + branches: + - master + types: + - opened + - synchronize + - reopened concurrency: group: "${{ github.workflow }}-${{ github.ref }}" @@ -77,9 +83,10 @@ jobs: - name: C++ - Clang format lint if: always() && steps.cpp_files.outputs.found_files - uses: DoozyX/clang-format-lint-action@v0.18 + uses: DoozyX/clang-format-lint-action@v0.20 with: source: ${{ steps.cpp_files.outputs.found_files }} + clangFormatVersion: '20' extensions: 'c,cpp,h,hpp,m,mm' style: file inplace: false @@ -263,5 +270,4 @@ jobs: - name: YAML - log if: always() && steps.yamllint.outcome == 'failure' - run: | - cat "${{ steps.yamllint.outputs.logfile }}" >> $GITHUB_STEP_SUMMARY + run: cat "${{ steps.yamllint.outputs.logfile }}" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/issues.yml b/.github/workflows/issues.yml index aec6006c870..5bd4e8810c4 100644 --- a/.github/workflows/issues.yml +++ b/.github/workflows/issues.yml @@ -1,17 +1,22 @@ --- -# This action is centrally managed in https://github.com//.github/ +# This workflow is centrally managed in https://github.com//.github/ # Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in # the above-mentioned repo. # Label and un-label actions using `../label-actions.yml`. name: Issues +permissions: {} on: issues: - types: [labeled, unlabeled] + types: + - labeled + - unlabeled discussion: - types: [labeled, unlabeled] + types: + - labeled + - unlabeled jobs: label: diff --git a/.github/workflows/localize.yml b/.github/workflows/localize.yml index f31a3aa5782..476f2f2c83d 100644 --- a/.github/workflows/localize.yml +++ b/.github/workflows/localize.yml @@ -1,10 +1,13 @@ --- name: localize +permissions: + contents: read on: push: - branches: [master] - paths: # prevents workflow from running unless these files change + branches: + - master + paths: - '.github/workflows/localize.yml' - 'src/**' - 'locale/sunshine.po' @@ -54,7 +57,7 @@ jobs: 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 @@ -68,7 +71,9 @@ jobs: - 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' }} + if: >- + 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 9369a7fabc5..1e1f6cf42b0 100644 --- a/.github/workflows/release-notifier-moonlight.yml +++ b/.github/workflows/release-notifier-moonlight.yml @@ -1,5 +1,6 @@ --- name: Release Notifications (Moonlight) +permissions: {} on: release: @@ -8,20 +9,30 @@ on: jobs: discord: - if: >- - startsWith(github.repository, 'LizardByte/') && - !github.event.release.prerelease && - !github.event.release.draft + if: github.repository_owner == 'LizardByte' runs-on: ubuntu-latest steps: + - name: Check if latest GitHub release + id: check-release + uses: actions/github-script@v7 + with: + script: | + const latestRelease = await github.rest.repos.getLatestRelease({ + owner: context.repo.owner, + repo: context.repo.repo + }); + + core.setOutput('isLatestRelease', latestRelease.data.tag_name === context.payload.release.tag_name); + - name: discord - uses: sarisia/actions-status-discord@v1 # https://github.com/sarisia/actions-status-discord + if: steps.check-release.outputs.isLatestRelease == 'true' + uses: sarisia/actions-status-discord@v1 with: - webhook: ${{ secrets.DISCORD_RELEASE_WEBHOOK_MOONLIGHT }} + avatar_url: ${{ vars.ORG_LOGO_URL }}256 + color: 0x${{ vars.COLOR_HEX_GREEN }} + description: ${{ github.event.release.body }} nodetail: true nofail: false - username: ${{ secrets.DISCORD_USERNAME }} - avatar_url: ${{ secrets.ORG_LOGO_URL }} title: ${{ github.event.repository.name }} ${{ github.ref_name }} Released - description: ${{ github.event.release.body }} - color: 0xFF4500 + username: ${{ secrets.DISCORD_USERNAME }} + webhook: ${{ secrets.DISCORD_RELEASE_WEBHOOK_MOONLIGHT }} diff --git a/.github/workflows/release-notifier.yml b/.github/workflows/release-notifier.yml index aeb33ed2ab9..d724abf3515 100644 --- a/.github/workflows/release-notifier.yml +++ b/.github/workflows/release-notifier.yml @@ -1,11 +1,13 @@ --- -# This action is centrally managed in https://github.com//.github/ +# This workflow is centrally managed in https://github.com//.github/ # Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in # the above-mentioned repo. -# Send release notification to various platforms. +# Create a blog post for a new release and open a PR to the blog repo name: Release Notifications +permissions: + contents: read on: release: @@ -13,99 +15,123 @@ on: - released # this triggers when a release is published, but does not include pre-releases or drafts jobs: - simplified_changelog: - if: >- - startsWith(github.repository, 'LizardByte/') && - !github.event.release.prerelease && - !github.event.release.draft - outputs: - SIMPLIFIED_BODY: ${{ steps.output.outputs.SIMPLIFIED_BODY }} + update-blog: + name: Update blog + if: github.repository_owner == 'LizardByte' runs-on: ubuntu-latest steps: - - name: remove contributors section + - name: Check topics env: - RELEASE_BODY: ${{ github.event.release.body }} - id: output - run: | - echo "${RELEASE_BODY}" > ./release_body.md - modified_body=$(sed '/^---/,$d' ./release_body.md) - echo "modified_body: ${modified_body}" - - # use a heredoc to ensure the output is multiline - echo "SIMPLIFIED_BODY<> $GITHUB_OUTPUT - echo "${modified_body}" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT - - discord: - if: >- - startsWith(github.repository, 'LizardByte/') && - !github.event.release.prerelease && - !github.event.release.draft - needs: simplified_changelog - runs-on: ubuntu-latest - steps: - - name: discord - uses: sarisia/actions-status-discord@v1 + TOPIC: replicator-release-notifications + id: check-label + uses: actions/github-script@v7 with: - avatar_url: ${{ secrets.ORG_LOGO_URL }} - color: 0x00ff00 - description: ${{ needs.simplified_changelog.outputs.SIMPLIFIED_BODY }} - nodetail: true - nofail: false - title: ${{ github.event.repository.name }} ${{ github.ref_name }} Released - url: ${{ github.event.release.html_url }} - username: ${{ secrets.DISCORD_USERNAME }} - webhook: ${{ secrets.DISCORD_RELEASE_WEBHOOK }} - - facebook_page: - if: >- - startsWith(github.repository, 'LizardByte/') && - !github.event.release.prerelease && - !github.event.release.draft - runs-on: ubuntu-latest - steps: - - name: facebook-post-action - uses: LizardByte/facebook-post-action@v2024.1207.15428 + script: | + const topic = process.env.TOPIC; + console.log(`Checking if repo has topic: ${topic}`); + + const repoTopics = await github.rest.repos.getAllTopics({ + owner: context.repo.owner, + repo: context.repo.repo + }); + console.log(`Repo topics: ${repoTopics.data.names}`); + + const hasTopic = repoTopics.data.names.includes(topic); + console.log(`Has topic: ${hasTopic}`); + + core.setOutput('hasTopic', hasTopic); + + - name: Check if latest GitHub release + id: check-release + if: steps.check-label.outputs.hasTopic == 'true' + uses: actions/github-script@v7 with: - page_id: ${{ secrets.FACEBOOK_PAGE_ID }} - access_token: ${{ secrets.FACEBOOK_ACCESS_TOKEN }} - message: | - ${{ github.event.repository.name }} ${{ github.ref_name }} Released - url: ${{ github.event.release.html_url }} - - reddit: - if: >- - startsWith(github.repository, 'LizardByte/') && - !github.event.release.prerelease && - !github.event.release.draft - needs: simplified_changelog - runs-on: ubuntu-latest - steps: - - name: reddit - uses: bluwy/release-for-reddit-action@v2 + script: | + const latestRelease = await github.rest.repos.getLatestRelease({ + owner: context.repo.owner, + repo: context.repo.repo + }); + + core.setOutput('isLatestRelease', latestRelease.data.tag_name === context.payload.release.tag_name); + + - name: Checkout blog + if: >- + steps.check-label.outputs.hasTopic == 'true' && + steps.check-release.outputs.isLatestRelease == 'true' + uses: actions/checkout@v4 with: - username: ${{ secrets.REDDIT_USERNAME }} - password: ${{ secrets.REDDIT_PASSWORD }} - app-id: ${{ secrets.REDDIT_CLIENT_ID }} - app-secret: ${{ secrets.REDDIT_CLIENT_SECRET }} - subreddit: ${{ secrets.REDDIT_SUBREDDIT }} - title: ${{ github.event.repository.name }} ${{ github.ref_name }} Released - url: ${{ github.event.release.html_url }} - flair-id: ${{ secrets.REDDIT_FLAIR_ID }} # https://www.reddit.com/r/>/api/link_flair.json - comment: ${{ needs.simplified_changelog.outputs.SIMPLIFIED_BODY }} - - x: - if: >- - startsWith(github.repository, 'LizardByte/') && - !github.event.release.prerelease && - !github.event.release.draft - runs-on: ubuntu-latest - steps: - - name: x - uses: nearform-actions/github-action-notify-twitter@v1 + repository: "LizardByte/LizardByte.github.io" + + - name: Create blog post + if: >- + steps.check-label.outputs.hasTopic == 'true' && + steps.check-release.outputs.isLatestRelease == 'true' + run: | + # setup variables + tag_name="${{ github.event.release.tag_name }}" + semver="${tag_name#v}" + repo_lower="$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]')" + + # extract year, month, and day + year="${semver%%.*}" + month_day="${semver#*.}" + month_day="${month_day%%.*}" + + # ensure month_day is 4 digits + month_day=$(printf "%04d" "$month_day") + + # create the filename + file_name="_posts/releases/${repo_lower}/${year}-${month_day:0:2}-${month_day:2:2}-v${semver}.md" + mkdir -p "$(dirname "${file_name}")" + + # create jekyll blog post + echo "---" > "${file_name}" + echo "layout: release" >> "${file_name}" + echo "title: ${{ github.event.repository.name }} ${tag_name} Released" >> "${file_name}" + echo "release-tag: ${tag_name}" >> "${file_name}" + echo "gh-repo: ${{ github.repository }}" >> "${file_name}" + echo "gh-badge: [follow, fork, star]" >> "${file_name}" + echo "tags: [release, ${repo_lower}]" >> "${file_name}" + echo "comments: true" >> "${file_name}" + echo "author: LizardByte-bot" >> "${file_name}" + echo "---" >> "${file_name}" + echo "" >> "${file_name}" + + release_body=$(cat <> "${file_name}" + + - name: Create/Update Pull Request + id: create-pr + if: >- + steps.check-label.outputs.hasTopic == 'true' && + steps.check-release.outputs.isLatestRelease == 'true' + uses: peter-evans/create-pull-request@v7 with: - message: ${{ github.event.release.html_url }} - twitter-app-key: ${{ secrets.X_APP_KEY }} - twitter-app-secret: ${{ secrets.X_APP_SECRET }} - twitter-access-token: ${{ secrets.X_ACCESS_TOKEN }} - twitter-access-token-secret: ${{ secrets.X_ACCESS_TOKEN_SECRET }} + token: ${{ secrets.GH_BOT_TOKEN }} + commit-message: | + chore: Add blog post for ${{ github.event.repository.name }} release ${{ github.event.release.tag_name }} + branch: bot/add-${{ github.event.repository.name }}-${{ github.event.release.tag_name }} + delete-branch: true + title: | + chore: Add blog post for ${{ github.event.repository.name }} release ${{ github.event.release.tag_name }} + body: ${{ github.event.release.body }} + labels: + blog + + - name: Automerge PR + env: + GH_TOKEN: ${{ secrets.GH_BOT_TOKEN }} + if: >- + steps.check-label.outputs.hasTopic == 'true' && + steps.check-release.outputs.isLatestRelease == 'true' + run: | + gh pr merge \ + --auto \ + --delete-branch \ + --repo "LizardByte/LizardByte.github.io" \ + --squash \ + "${{ steps.create-pr.outputs.pull-request-number }}" diff --git a/.github/workflows/update-changelog.yml b/.github/workflows/update-changelog.yml index 3c095fcfb04..394a2432161 100644 --- a/.github/workflows/update-changelog.yml +++ b/.github/workflows/update-changelog.yml @@ -1,15 +1,20 @@ --- -# This action is centrally managed in https://github.com//.github/ +# This workflow is centrally managed in https://github.com//.github/ # Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in # the above-mentioned repo. # Update changelog on release events. name: Update changelog +permissions: + contents: read on: release: - types: [created, edited, deleted] + types: + - created + - edited + - deleted workflow_dispatch: concurrency: @@ -18,6 +23,7 @@ concurrency: jobs: update-changelog: + name: Update Changelog if: >- github.event_name == 'workflow_dispatch' || (!github.event.release.prerelease && !github.event.release.draft) diff --git a/.github/workflows/update-docs.yml b/.github/workflows/update-docs.yml index d1212f656c0..04e1e90304f 100644 --- a/.github/workflows/update-docs.yml +++ b/.github/workflows/update-docs.yml @@ -1,19 +1,23 @@ --- -# This action is centrally managed in https://github.com//.github/ +# This workflow is centrally managed in https://github.com//.github/ # Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in # the above-mentioned repo. -# Use the `rtd` repository label to identify repositories that should trigger have this workflow. +# To use, add the `rtd` repository label to identify repositories that should trigger this workflow. # If the project slug is not the repository name, add a repository variable named `READTHEDOCS_SLUG` with the value of # the ReadTheDocs project slug. # Update readthedocs on release events. name: Update docs +permissions: {} on: release: - types: [created, edited, deleted] + types: + - created + - edited + - deleted concurrency: group: "${{ github.workflow }}-${{ github.event.release.tag_name }}" @@ -73,17 +77,23 @@ jobs: - name: Update RTD project # changing the default branch in readthedocs makes "latest" point to that branch/tag # we can also update other properties like description, etc. - if: >- - steps.check.outputs.isLatestRelease == 'true' + if: steps.check.outputs.isLatestRelease == 'true' run: | json_body=$(jq -n \ --arg default_branch "${TAG}" \ --arg description "${{ github.event.repository.description }}" \ '{default_branch: $default_branch}') + # change the default branch to the latest release curl \ -X PATCH \ -H "Authorization: Token ${RTD_TOKEN}" \ - https://readthedocs.org/api/v3/projects/${RTD_SLUG}/ \ -H "Content-Type: application/json" \ + https://readthedocs.org/api/v3/projects/${RTD_SLUG}/ \ -d "$json_body" + + # trigger a build for the latest version + curl \ + -X POST \ + -H "Authorization: Token ${RTD_TOKEN}" \ + https://readthedocs.org/api/v3/projects/${RTD_SLUG}/versions/latest/builds/ diff --git a/.github/workflows/update-flathub-repo.yml b/.github/workflows/update-flathub-repo.yml index b3e326c95ea..524a8a41d93 100644 --- a/.github/workflows/update-flathub-repo.yml +++ b/.github/workflows/update-flathub-repo.yml @@ -1,15 +1,20 @@ --- -# This action is a candidate to centrally manage in https://github.com//.github/ -# If more Flathub applications are developed, consider moving this action to the organization's .github repository, -# using the `flathub-pkg` repository label to identify repositories that should trigger this workflow. +# This workflow is centrally managed in https://github.com//.github/ +# Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in +# the above-mentioned repo. + +# To use, add the `flathub-pkg` repository label to identify repositories that should trigger this workflow. # Update Flathub on release events. name: Update flathub repo +permissions: + contents: read on: release: - types: [released] + types: + - released concurrency: group: "${{ github.workflow }}-${{ github.event.release.tag_name }}" @@ -19,14 +24,13 @@ jobs: update-flathub-repo: env: FLATHUB_PKG: dev.lizardbyte.app.${{ github.event.repository.name }} - if: >- - github.repository_owner == 'LizardByte' + if: github.repository_owner == 'LizardByte' runs-on: ubuntu-latest steps: - name: Check if flathub repo + id: check-label env: TOPIC: flathub-pkg - id: check-label uses: actions/github-script@v7 with: script: | @@ -46,8 +50,7 @@ jobs: - name: Check if latest GitHub release id: check-release - if: >- - steps.check-label.outputs.hasTopic == 'true' + if: steps.check-label.outputs.hasTopic == 'true' uses: actions/github-script@v7 with: script: | @@ -104,7 +107,7 @@ jobs: if: >- steps.check-label.outputs.hasTopic == 'true' && steps.check-release.outputs.isLatestRelease == 'true' - uses: robinraju/release-downloader@v1.11 + uses: robinraju/release-downloader@v1.12 with: repository: "${{ github.repository }}" tag: "${{ github.event.release.tag_name }}" @@ -114,7 +117,7 @@ jobs: out-file-path: "flathub/${{ env.FLATHUB_PKG }}" extract: true - - name: Delete arhive + - name: Delete archive if: >- steps.check-label.outputs.hasTopic == 'true' && steps.check-release.outputs.isLatestRelease == 'true' @@ -143,9 +146,9 @@ jobs: BEGIN { replaced = 0 } // { if (!replaced) { - print "" - print "

" changelog "

" - print "
" + print " " + print "

" changelog "

" + print "
" replaced = 1 } } @@ -172,6 +175,7 @@ jobs: git checkout $main_commit - name: Create/Update Pull Request + id: create-pr if: >- steps.check-label.outputs.hasTopic == 'true' && steps.check-release.outputs.isLatestRelease == 'true' && @@ -185,3 +189,18 @@ jobs: delete-branch: true title: "chore: Update ${{ env.FLATHUB_PKG }} to ${{ github.event.release.tag_name }}" body: ${{ github.event.release.body }} + + - name: Automerge PR + env: + GH_TOKEN: ${{ secrets.GH_BOT_TOKEN }} + if: >- + steps.check-label.outputs.hasTopic == 'true' && + steps.check-release.outputs.isLatestRelease == 'true' && + fromJson(steps.download.outputs.downloaded_files)[0] + run: | + gh pr merge \ + --auto \ + --delete-branch \ + --repo "flathub/${{ env.FLATHUB_PKG }}" \ + --squash \ + "${{ steps.create-pr.outputs.pull-request-number }}" diff --git a/.github/workflows/update-homebrew-release.yml b/.github/workflows/update-homebrew-release.yml index 65c9698ee32..cc442e8dc92 100644 --- a/.github/workflows/update-homebrew-release.yml +++ b/.github/workflows/update-homebrew-release.yml @@ -1,15 +1,20 @@ --- -# This action is a candidate to centrally manage in https://github.com//.github/ -# If more Homebrew applications are developed, consider moving this action to the organization's .github repository, -# using the `homebrew-pkg` repository label to identify repositories that should trigger this workflow. +# This workflow is centrally managed in https://github.com//.github/ +# Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in +# the above-mentioned repo. + +# To use, add the `homebrew-pkg` repository label to identify repositories that should trigger this workflow. # Update Homebrew on release events. name: Update Homebrew release +permissions: + contents: read on: release: - types: [released] + types: + - released concurrency: group: "${{ github.workflow }}-${{ github.event.release.tag_name }}" @@ -17,14 +22,13 @@ concurrency: jobs: update-homebrew-release: - if: >- - github.repository_owner == 'LizardByte' + if: github.repository_owner == 'LizardByte' runs-on: ubuntu-latest steps: - name: Check if Homebrew repo + id: check-label env: TOPIC: homebrew-pkg - id: check-label uses: actions/github-script@v7 with: script: | @@ -44,9 +48,8 @@ jobs: - name: Download release asset id: download - if: >- - steps.check-label.outputs.hasTopic == 'true' - uses: robinraju/release-downloader@v1.11 + if: steps.check-label.outputs.hasTopic == 'true' + uses: robinraju/release-downloader@v1.12 with: repository: "${{ github.repository }}" tag: "${{ github.event.release.tag_name }}" diff --git a/.github/workflows/update-pacman-repo.yml b/.github/workflows/update-pacman-repo.yml index 92e14dc7cf2..a0fd8183c6e 100644 --- a/.github/workflows/update-pacman-repo.yml +++ b/.github/workflows/update-pacman-repo.yml @@ -1,15 +1,20 @@ --- -# This action is a candidate to centrally manage in https://github.com//.github/ -# If more pacman packages are developed, consider moving this action to the organization's .github repository, -# using the `pacman-pkg` repository label to identify repositories that should trigger have this workflow. +# This workflow is centrally managed in https://github.com//.github/ +# Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in +# the above-mentioned repo. + +# To use, add the `pacman-pkg` repository label to identify repositories that should trigger this workflow. # Update pacman repo on release events. name: Update pacman repo +permissions: + contents: read on: release: - types: [released] + types: + - released concurrency: group: "${{ github.workflow }}-${{ github.event.release.tag_name }}" @@ -17,14 +22,13 @@ concurrency: jobs: update-homebrew-release: - if: >- - github.repository_owner == 'LizardByte' + if: github.repository_owner == 'LizardByte' runs-on: ubuntu-latest steps: - name: Check if pacman repo + id: check-label env: TOPIC: pacman-pkg - id: check-label uses: actions/github-script@v7 with: script: | @@ -77,7 +81,7 @@ jobs: if: >- steps.check-label.outputs.hasTopic == 'true' && steps.check-release.outputs.isLatestRelease == 'true' - uses: robinraju/release-downloader@v1.11 + uses: robinraju/release-downloader@v1.12 with: repository: "${{ github.repository }}" tag: "${{ github.event.release.tag_name }}" @@ -87,7 +91,16 @@ jobs: out-file-path: "pkgbuilds/${{ steps.prep.outputs.pkg_name }}" extract: true + - name: Remove pkg.tar.gz + if: >- + steps.check-label.outputs.hasTopic == 'true' && + steps.check-release.outputs.isLatestRelease == 'true' && + fromJson(steps.download.outputs.downloaded_files)[0] + run: | + rm -f "pkgbuilds/${{ steps.prep.outputs.pkg_name }}" + - name: Create/Update Pull Request + id: create-pr if: >- steps.check-label.outputs.hasTopic == 'true' && steps.check-release.outputs.isLatestRelease == 'true' && @@ -105,3 +118,17 @@ jobs: labels: | auto-approve auto-merge + + - name: Automerge PR + env: + GH_TOKEN: ${{ secrets.GH_BOT_TOKEN }} + if: >- + steps.check-label.outputs.hasTopic == 'true' && + steps.check-release.outputs.isLatestRelease == 'true' && + fromJson(steps.download.outputs.downloaded_files)[0] + run: | + gh pr merge \ + --auto \ + --delete-branch \ + --squash \ + "${{ steps.create-pr.outputs.pull-request-number }}" diff --git a/.github/workflows/update-pages.yml b/.github/workflows/update-pages.yml index 5db0f94da68..06b1532fe3e 100644 --- a/.github/workflows/update-pages.yml +++ b/.github/workflows/update-pages.yml @@ -1,12 +1,19 @@ --- name: Build GH-Pages +permissions: + contents: read on: pull_request: - branches: [master] - types: [opened, synchronize, reopened] + branches: + - master + types: + - opened + - synchronize + - reopened push: - branches: [master] + branches: + - master workflow_dispatch: concurrency: @@ -14,49 +21,29 @@ concurrency: cancel-in-progress: true jobs: - update_pages: + prep: runs-on: ubuntu-latest - steps: - name: Checkout uses: actions/checkout@v4 - - name: Checkout gh-pages - uses: actions/checkout@v4 - with: - ref: gh-pages - path: gh-pages - persist-credentials: false # otherwise, the token used is the GITHUB_TOKEN, instead of the personal token - fetch-depth: 0 # otherwise, will fail to push refs to dest repo - - - name: Prepare gh-pages - run: | - # empty contents - rm -f -r ./gh-pages/* - - # copy template back to pages - cp -f -r ./gh-pages-template/. ./gh-pages/ - - - name: Upload Artifacts - if: ${{ github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' }} + - name: Upload artifact uses: actions/upload-artifact@v4 with: - name: gh-pages - if-no-files-found: error # 'warn' or 'ignore' are also available, defaults to `warn` - path: | - ${{ github.workspace }}/gh-pages - !**/*.git - - - name: Deploy to gh-pages - if: >- - (github.event_name == 'push' && github.ref == 'refs/heads/master') || - (github.event_name == 'workflow_dispatch') - uses: actions-js/push@v1.5 - with: - github_token: ${{ secrets.GH_BOT_TOKEN }} - author_email: ${{ secrets.GH_BOT_EMAIL }} - author_name: ${{ secrets.GH_BOT_NAME }} - directory: gh-pages - branch: gh-pages - force: false - message: sync gh-pages to ${{ github.sha }} + name: prep + path: gh-pages-template/ + if-no-files-found: error + include-hidden-files: true + retention-days: 1 + + call-jekyll-build: + needs: prep + uses: LizardByte/LizardByte.github.io/.github/workflows/jekyll-build.yml@master + secrets: + GH_BOT_EMAIL: ${{ secrets.GH_BOT_EMAIL }} + GH_BOT_NAME: ${{ secrets.GH_BOT_NAME }} + GH_BOT_TOKEN: ${{ secrets.GH_BOT_TOKEN }} + with: + clean_gh_pages: true + site_artifact: 'prep' + target_branch: 'gh-pages' diff --git a/.github/workflows/update-winget-release.yml b/.github/workflows/update-winget-release.yml index c004dcdd605..860f086fa60 100644 --- a/.github/workflows/update-winget-release.yml +++ b/.github/workflows/update-winget-release.yml @@ -1,15 +1,20 @@ --- -# This action is a candidate to centrally manage in https://github.com//.github/ -# If more Winget applications are developed, consider moving this action to the organization's .github repository, -# using the `winget-pkg` repository label to identify repositories that should trigger this workflow. +# This workflow is centrally managed in https://github.com//.github/ +# Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in +# the above-mentioned repo. + +# To use, add the `winget-pkg` repository label to identify repositories that should trigger this workflow. # Update Winget on release events. name: Update Winget release +permissions: + contents: read on: release: - types: [released] + types: + - released concurrency: group: "${{ github.workflow }}-${{ github.event.release.tag_name }}" @@ -17,14 +22,13 @@ concurrency: jobs: update-winget-release: - if: >- - github.repository_owner == 'LizardByte' + if: github.repository_owner == 'LizardByte' runs-on: ubuntu-latest steps: - name: Check if Winget repo + id: check-label env: TOPIC: winget-pkg - id: check-label uses: actions/github-script@v7 with: script: | @@ -44,9 +48,8 @@ jobs: - name: Download release asset id: download - if: >- - steps.check-label.outputs.hasTopic == 'true' - uses: robinraju/release-downloader@v1.11 + if: steps.check-label.outputs.hasTopic == 'true' + uses: robinraju/release-downloader@v1.12 with: repository: "${{ github.repository }}" tag: "${{ github.event.release.tag_name }}" diff --git a/.gitmodules b/.gitmodules index be1c418a365..c06ff0d4d2e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -22,6 +22,10 @@ path = third-party/inputtino url = https://github.com/games-on-whales/inputtino.git branch = stable +[submodule "third-party/libdisplaydevice"] + path = third-party/libdisplaydevice + url = https://github.com/LizardByte/libdisplaydevice.git + branch = master [submodule "third-party/moonlight-common-c"] path = third-party/moonlight-common-c url = https://github.com/moonlight-stream/moonlight-common-c.git @@ -40,7 +44,7 @@ branch = sdk [submodule "third-party/Simple-Web-Server"] path = third-party/Simple-Web-Server - url = https://gitlab.com/eidheim/Simple-Web-Server.git + url = https://github.com/LizardByte-infrastructure/Simple-Web-Server.git branch = master [submodule "third-party/TPCircularBuffer"] path = third-party/TPCircularBuffer @@ -56,9 +60,9 @@ branch = master [submodule "third-party/wayland-protocols"] path = third-party/wayland-protocols - url = https://gitlab.freedesktop.org/wayland/wayland-protocols.git + url = https://github.com/LizardByte-infrastructure/wayland-protocols.git branch = main [submodule "third-party/wlr-protocols"] path = third-party/wlr-protocols - url = https://gitlab.freedesktop.org/wlroots/wlr-protocols.git + url = https://github.com/LizardByte-infrastructure/wlr-protocols.git branch = master diff --git a/README.md b/README.md index be73eb71a8a..61369b3c2cc 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,25 @@ -# Overview - -[![GitHub stars](https://img.shields.io/github/stars/lizardbyte/sunshine.svg?logo=github&style=for-the-badge)](https://github.com/LizardByte/Sunshine) -[![GitHub Releases](https://img.shields.io/github/downloads/lizardbyte/sunshine/total.svg?style=for-the-badge&logo=github)](https://github.com/LizardByte/Sunshine/releases/latest) -[![Docker](https://img.shields.io/docker/pulls/lizardbyte/sunshine.svg?style=for-the-badge&logo=docker)](https://hub.docker.com/r/lizardbyte/sunshine) -[![Flathub installs](https://img.shields.io/flathub/downloads/dev.lizardbyte.app.Sunshine?style=for-the-badge&logo=flathub)](https://flathub.org/apps/dev.lizardbyte.app.Sunshine) -[![Flathub Version](https://img.shields.io/flathub/v/dev.lizardbyte.app.Sunshine?style=for-the-badge&logo=flathub)](https://flathub.org/apps/dev.lizardbyte.app.Sunshine) -[![GHCR](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fipitio.github.io%2Fbackage%2FLizardByte%2FSunshine%2Fsunshine.json&query=%24.downloads&label=ghcr%20pulls&style=for-the-badge&logo=github)](https://github.com/LizardByte/Sunshine/pkgs/container/sunshine) -[![Winget Version](https://img.shields.io/winget/v/LizardByte.Sunshine?style=for-the-badge&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAHuSURBVFhH7ZfNTtRQGIYZiMDwN/IrCAqIhMSNKxcmymVwG+5dcDVsWHgDrtxwCYQVl+BChzDEwSnPY+eQ0sxoOz1mQuBNnpyvTdvz9jun5/SrjfxnJUkyQbMEz2ELduF1l0YUA3QyTrMAa2AnPtyOXsELeAYNyKtV2EC3k3lYgTOwg09ghy/BTp7CKBRV844BOpmmMV2+ySb4BmInG7AKY7AHH+EYqqhZo9PPBG/BVDlOizAD/XQFmnoPXzxRQX8M/CCYS48L6RIc4ygGHK9WGg9HZSZMUNRPVwNJGg5Hg2Qgqh4N3FsDsb6EmgYm07iwwvUxstdxJTwgmILf4CfZ6bb5OHANX8GN5x20IVxnG8ge94pt2xpwU3GnCwayF4Q2G2vgFLzHndFzQdk4q77nNfCdwL28qNyMtmEf3A1/QV5FjDiPWo5jrwf8TWZChTlgJvL4F9QL50/A43qVidTvLcuoM2wDQ1+IkgefgUpLcYwMVBqCKNJA2b0gKNocOIITOIef8C/F/CdMbh/GklynsSawKLHS8d9/B1x2LUqsfFyy3TMsWj5A1cLkotDbYO4JjWWZlZEGv8EbOIR1CAVN2eG8W5oNKgxaeC6DmTJjZs7ixUxpznLPLT+v4sXpoMLcLI3mzFSonDXIEI/M3QCIO4YuimBJ/gAAAABJRU5ErkJggg==)](https://github.com/microsoft/winget-pkgs/tree/master/manifests/l/LizardByte/Sunshine) - -[![GitHub Workflow Status (CI)](https://img.shields.io/github/actions/workflow/status/lizardbyte/sunshine/CI.yml.svg?branch=master&label=CI%20build&logo=github&style=for-the-badge)](https://github.com/LizardByte/Sunshine/actions/workflows/CI.yml?query=branch%3Amaster) -[![GitHub Workflow Status (localize)](https://img.shields.io/github/actions/workflow/status/lizardbyte/sunshine/localize.yml.svg?branch=master&label=localize%20build&logo=github&style=for-the-badge)](https://github.com/LizardByte/Sunshine/actions/workflows/localize.yml?query=branch%3Amaster) -[![Read the Docs](https://img.shields.io/readthedocs/sunshinestream.svg?label=Docs&style=for-the-badge&logo=readthedocs)](http://sunshinestream.readthedocs.io) -[![Codecov](https://img.shields.io/codecov/c/gh/LizardByte/Sunshine?token=SMGXQ5NVMJ&style=for-the-badge&logo=codecov&label=codecov)](https://codecov.io/gh/LizardByte/Sunshine) +
+ +

Sunshine

+

Self-hosted game stream host for Moonlight.

+
-LizardByte has the full documentation hosted on [Read the Docs](https://sunshinestream.readthedocs.io). +
+ GitHub stars + GitHub Releases + Docker + GHCR + Flathub installs + Flathub Version + Winget Version + Gurubase + GitHub Workflow Status (CI) + GitHub Workflow Status (localize) + Read the Docs + Codecov +
-## About +## ℹ️ 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 @@ -23,7 +27,12 @@ encoding. Software encoding is also available. You can connect to Sunshine from 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. -## System Requirements +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/) + +## 🖥️ System Requirements @warning{These tables are a work in progress. Do not purchase hardware based on this information.} @@ -38,7 +47,11 @@ the local server or any mobile device. AMD: VCE 1.0 or higher, see: obs-amd hardware support - Intel: VAAPI-compatible, see: VAAPI hardware support + + Intel:
+   Linux: VAAPI-compatible, see: VAAPI hardware support
+   Windows: Skylake or newer with QuickSync encoding support + Nvidia: NVENC enabled cards, see: nvenc support matrix @@ -65,7 +78,7 @@ the local server or any mobile device. Linux/Debian: 12+ (bookworm) - Linux/Fedora: 39+ + Linux/Fedora: 40+ Linux/Ubuntu: 22.04+ (jammy) @@ -90,7 +103,11 @@ the local server or any mobile device. AMD: Video Coding Engine 3.1 or higher - Intel: HD Graphics 510 or higher + + Intel:
+   Linux: HD Graphics 510 or higher
+   Windows: Skylake or newer with QuickSync encoding support + Nvidia: GeForce GTX 1080 or higher @@ -125,7 +142,7 @@ the local server or any mobile device. Intel: HD Graphics 730 or higher - Nvidia: Nvidia: Pascal-based GPU (GTX 10-series) or higher + Nvidia: Pascal-based GPU (GTX 10-series) or higher CPU @@ -143,9 +160,37 @@ the local server or any mobile device. -## Support +## ❓ Support + +Our support methods are listed in our [LizardByte Docs](https://docs.lizardbyte.dev/latest/about/support.html). + +## 💲 Sponsors and Supporters + +

+ + + +

+ +## 👥 Contributors + +Thank you to all the contributors who have helped make Sunshine better! + +### GitHub + +

+ + + +

+ +### CrowdIn -Our support methods are listed in our [LizardByte Docs](https://lizardbyte.readthedocs.io/en/latest/about/support.html). +

+ + + +

diff --git a/cmake/compile_definitions/common.cmake b/cmake/compile_definitions/common.cmake index 02ec72247c4..723a1a7a540 100644 --- a/cmake/compile_definitions/common.cmake +++ b/cmake/compile_definitions/common.cmake @@ -68,6 +68,8 @@ set(SUNSHINE_TARGET_FILES "${CMAKE_SOURCE_DIR}/src/uuid.h" "${CMAKE_SOURCE_DIR}/src/config.h" "${CMAKE_SOURCE_DIR}/src/config.cpp" + "${CMAKE_SOURCE_DIR}/src/display_device.h" + "${CMAKE_SOURCE_DIR}/src/display_device.cpp" "${CMAKE_SOURCE_DIR}/src/entry_handler.cpp" "${CMAKE_SOURCE_DIR}/src/entry_handler.h" "${CMAKE_SOURCE_DIR}/src/file_handler.cpp" @@ -146,6 +148,8 @@ list(APPEND SUNSHINE_EXTERNAL_LIBRARIES ${MINIUPNP_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT} enet + libdisplaydevice::display_device + nlohmann_json::nlohmann_json opus ${FFMPEG_LIBRARIES} ${Boost_LIBRARIES} diff --git a/cmake/compile_definitions/linux.cmake b/cmake/compile_definitions/linux.cmake index 5bf24ba6f33..152466d806c 100644 --- a/cmake/compile_definitions/linux.cmake +++ b/cmake/compile_definitions/linux.cmake @@ -137,7 +137,8 @@ if(WAYLAND_FOUND) endif() GEN_WAYLAND("${WAYLAND_PROTOCOLS_DIR}" "unstable/xdg-output" xdg-output-unstable-v1) - GEN_WAYLAND("${CMAKE_SOURCE_DIR}/third-party/wlr-protocols" "unstable" wlr-export-dmabuf-unstable-v1) + GEN_WAYLAND("${WAYLAND_PROTOCOLS_DIR}" "unstable/linux-dmabuf" linux-dmabuf-unstable-v1) + GEN_WAYLAND("${CMAKE_SOURCE_DIR}/third-party/wlr-protocols" "unstable" wlr-screencopy-unstable-v1) include_directories( SYSTEM @@ -145,7 +146,7 @@ if(WAYLAND_FOUND) ${CMAKE_BINARY_DIR}/generated-src ) - list(APPEND PLATFORM_LIBRARIES ${WAYLAND_LIBRARIES}) + list(APPEND PLATFORM_LIBRARIES ${WAYLAND_LIBRARIES} gbm) list(APPEND PLATFORM_TARGET_FILES "${CMAKE_SOURCE_DIR}/src/platform/linux/wlgrab.cpp" "${CMAKE_SOURCE_DIR}/src/platform/linux/wayland.h" @@ -198,29 +199,33 @@ if(${SUNSHINE_ENABLE_TRAY}) list(APPEND PLATFORM_TARGET_FILES "${CMAKE_SOURCE_DIR}/third-party/tray/src/tray_linux.c") list(APPEND SUNSHINE_EXTERNAL_LIBRARIES ${APPINDICATOR_LIBRARIES} ${LIBNOTIFY_LIBRARIES}) endif() + + # flatpak icons must be prefixed with the app id or they will not be included in the flatpak + if(${SUNSHINE_BUILD_FLATPAK}) + set(SUNSHINE_TRAY_PREFIX "${PROJECT_FQDN}") + else() + set(SUNSHINE_TRAY_PREFIX "sunshine") + endif() + list(APPEND SUNSHINE_DEFINITIONS SUNSHINE_TRAY_PREFIX="${SUNSHINE_TRAY_PREFIX}") else() set(SUNSHINE_TRAY 0) message(STATUS "Tray icon disabled") endif() -if(${SUNSHINE_USE_LEGACY_INPUT}) # TODO: Remove this legacy option after the next stable release - list(APPEND PLATFORM_TARGET_FILES "${CMAKE_SOURCE_DIR}/src/platform/linux/input/legacy_input.cpp") -else() - # 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}") - - add_subdirectory("${CMAKE_SOURCE_DIR}/third-party/inputtino") - list(APPEND SUNSHINE_EXTERNAL_LIBRARIES inputtino::libinputtino) - file(GLOB_RECURSE INPUTTINO_SOURCES - ${CMAKE_SOURCE_DIR}/src/platform/linux/input/inputtino*.h - ${CMAKE_SOURCE_DIR}/src/platform/linux/input/inputtino*.cpp) - list(APPEND PLATFORM_TARGET_FILES ${INPUTTINO_SOURCES}) - - # build libevdev before the libinputtino target - if(EXTERNAL_PROJECT_LIBEVDEV_USED) - add_dependencies(libinputtino libevdev) - 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}") + +add_subdirectory("${CMAKE_SOURCE_DIR}/third-party/inputtino") +list(APPEND SUNSHINE_EXTERNAL_LIBRARIES inputtino::libinputtino) +file(GLOB_RECURSE INPUTTINO_SOURCES + ${CMAKE_SOURCE_DIR}/src/platform/linux/input/inputtino*.h + ${CMAKE_SOURCE_DIR}/src/platform/linux/input/inputtino*.cpp) +list(APPEND PLATFORM_TARGET_FILES ${INPUTTINO_SOURCES}) + +# build libevdev before the libinputtino target +if(EXTERNAL_PROJECT_LIBEVDEV_USED) + add_dependencies(libinputtino libevdev) endif() # AppImage and Flatpak diff --git a/cmake/compile_definitions/windows.cmake b/cmake/compile_definitions/windows.cmake index 7643d1d9efc..a3009be0aa4 100644 --- a/cmake/compile_definitions/windows.cmake +++ b/cmake/compile_definitions/windows.cmake @@ -38,7 +38,7 @@ if(NOT DEFINED SUNSHINE_ICON_PATH) set(SUNSHINE_ICON_PATH "${CMAKE_SOURCE_DIR}/sunshine.ico") endif() -configure_file("${CMAKE_SOURCE_DIR}/src/platform/windows/windows.rs.in" windows.rc @ONLY) +configure_file("${CMAKE_SOURCE_DIR}/src/platform/windows/windows.rc.in" windows.rc @ONLY) set(PLATFORM_TARGET_FILES "${CMAKE_CURRENT_BINARY_DIR}/windows.rc" @@ -64,23 +64,26 @@ set(OPENSSL_LIBRARIES libcrypto.a) list(PREPEND PLATFORM_LIBRARIES + ${CURL_STATIC_LIBRARIES} + avrt + d3d11 + D3DCompiler + dwmapi + dxgi + iphlpapi + ksuser + libssp.a libstdc++.a libwinpthread.a - libssp.a + minhook::minhook ntdll - ksuser - wsock32 - ws2_32 - d3d11 dxgi D3DCompiler setupapi - dwmapi - userenv - synchronization.lib - avrt - iphlpapi shlwapi - PkgConfig::NLOHMANN_JSON - ${CURL_STATIC_LIBRARIES}) + synchronization.lib + userenv + ws2_32 + wsock32 +) if(SUNSHINE_ENABLE_TRAY) list(APPEND PLATFORM_TARGET_FILES diff --git a/cmake/dependencies/Boost_Sunshine.cmake b/cmake/dependencies/Boost_Sunshine.cmake index 72ab54434c8..eb2ac4095fd 100644 --- a/cmake/dependencies/Boost_Sunshine.cmake +++ b/cmake/dependencies/Boost_Sunshine.cmake @@ -3,26 +3,43 @@ # include_guard(GLOBAL) -set(BOOST_VERSION 1.86) +set(BOOST_VERSION "1.87.0") set(BOOST_COMPONENTS filesystem locale log program_options - system) # system is not used by Sunshine, but by Simple-Web-Server, added here for convenience + system +) +# system is not used by Sunshine, but by Simple-Web-Server, added here for convenience + +# algorithm, preprocessor, scope, and uuid are not used by Sunshine, but by libdisplaydevice, added here for convenience +if(WIN32) + list(APPEND BOOST_COMPONENTS + algorithm + preprocessor + scope + uuid + ) +endif() if(BOOST_USE_STATIC) set(Boost_USE_STATIC_LIBS ON) # cmake-lint: disable=C0103 endif() -find_package(Boost CONFIG ${BOOST_VERSION} COMPONENTS ${BOOST_COMPONENTS}) +if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.30") + cmake_policy(SET CMP0167 NEW) # Get BoostConfig.cmake from upstream +endif() +find_package(Boost CONFIG ${BOOST_VERSION} EXACT COMPONENTS ${BOOST_COMPONENTS}) if(NOT Boost_FOUND) - message(STATUS "Boost v${BOOST_VERSION}.x package not found in the system. Falling back to FetchContent.") + message(STATUS "Boost v${BOOST_VERSION} package not found in the system. Falling back to FetchContent.") include(FetchContent) - # Avoid warning about DOWNLOAD_EXTRACT_TIMESTAMP in CMake 3.24: if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.24.0") - cmake_policy(SET CMP0135 NEW) + cmake_policy(SET CMP0135 NEW) # Avoid warning about DOWNLOAD_EXTRACT_TIMESTAMP in CMake 3.24 + endif() + if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.31.0") + cmake_policy(SET CMP0174 NEW) # Handle empty variables endif() # more components required for compiling boost targets @@ -36,12 +53,9 @@ if(NOT Boost_FOUND) set(BOOST_ENABLE_CMAKE ON) # Limit boost to the required libraries only - set(BOOST_INCLUDE_LIBRARIES - ${BOOST_COMPONENTS}) - set(BOOST_URL - "https://github.com/boostorg/boost/releases/download/boost-1.86.0/boost-1.86.0-cmake.tar.xz") - set(BOOST_HASH - "MD5=D02759931CEDC02ADED80402906C5EB6") + 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") if(CMAKE_VERSION VERSION_LESS "3.24.0") FetchContent_Declare( @@ -72,7 +86,7 @@ if(NOT Boost_FOUND) set(Boost_FOUND TRUE) # cmake-lint: disable=C0103 set(Boost_INCLUDE_DIRS # cmake-lint: disable=C0103 - "$;$") + "$") if(WIN32) # Windows build is failing to create .h file in this directory diff --git a/cmake/dependencies/common.cmake b/cmake/dependencies/common.cmake index 810f8a878fe..c92b4777b86 100644 --- a/cmake/dependencies/common.cmake +++ b/cmake/dependencies/common.cmake @@ -12,7 +12,11 @@ add_subdirectory("${CMAKE_SOURCE_DIR}/third-party/moonlight-common-c/enet") # web server add_subdirectory("${CMAKE_SOURCE_DIR}/third-party/Simple-Web-Server") +# libdisplaydevice +add_subdirectory("${CMAKE_SOURCE_DIR}/third-party/libdisplaydevice") + # common dependencies +include("${CMAKE_MODULE_PATH}/dependencies/nlohmann_json.cmake") find_package(OpenSSL REQUIRED) find_package(PkgConfig REQUIRED) find_package(Threads REQUIRED) diff --git a/cmake/dependencies/nlohmann_json.cmake b/cmake/dependencies/nlohmann_json.cmake new file mode 100644 index 00000000000..11000c8d048 --- /dev/null +++ b/cmake/dependencies/nlohmann_json.cmake @@ -0,0 +1,25 @@ +# +# Loads the nlohmann_json library giving the priority to the system package first, with a fallback to FetchContent. +# +include_guard(GLOBAL) + +find_package(nlohmann_json 3.11 QUIET GLOBAL) +if(NOT nlohmann_json_FOUND) + message(STATUS "nlohmann_json v3.11.x package not found in the system. Falling back to FetchContent.") + include(FetchContent) + + if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.24.0") + cmake_policy(SET CMP0135 NEW) # Avoid warning about DOWNLOAD_EXTRACT_TIMESTAMP in CMake 3.24 + endif() + if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.31.0") + cmake_policy(SET CMP0174 NEW) # Handle empty variables + endif() + + FetchContent_Declare( + json + URL https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz + URL_HASH MD5=c23a33f04786d85c29fda8d16b5f0efd + DOWNLOAD_EXTRACT_TIMESTAMP + ) + FetchContent_MakeAvailable(json) +endif() diff --git a/cmake/dependencies/windows.cmake b/cmake/dependencies/windows.cmake index 0563e567817..3faad7dfd41 100644 --- a/cmake/dependencies/windows.cmake +++ b/cmake/dependencies/windows.cmake @@ -1,4 +1,9 @@ # windows specific dependencies -# nlohmann_json -pkg_check_modules(NLOHMANN_JSON nlohmann_json REQUIRED IMPORTED_TARGET) +# Make sure MinHook is installed +find_library(MINHOOK_LIBRARY libMinHook.a REQUIRED) +find_path(MINHOOK_INCLUDE_DIR MinHook.h PATH_SUFFIXES include REQUIRED) + +add_library(minhook::minhook STATIC IMPORTED) +set_property(TARGET minhook::minhook PROPERTY IMPORTED_LOCATION ${MINHOOK_LIBRARY}) +target_include_directories(minhook::minhook INTERFACE ${MINHOOK_INCLUDE_DIR}) diff --git a/cmake/packaging/linux.cmake b/cmake/packaging/linux.cmake index 14b89ead8d1..6f4ebbe3c6a 100644 --- a/cmake/packaging/linux.cmake +++ b/cmake/packaging/linux.cmake @@ -100,15 +100,31 @@ endif() # tray icon if(${SUNSHINE_TRAY} STREQUAL 1) - install(FILES "${CMAKE_SOURCE_DIR}/sunshine.svg" - DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/scalable/status" - RENAME "sunshine-tray.svg") - install(FILES "${SUNSHINE_SOURCE_ASSETS_DIR}/common/assets/web/public/images/sunshine-playing.svg" - DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/scalable/status") - install(FILES "${SUNSHINE_SOURCE_ASSETS_DIR}/common/assets/web/public/images/sunshine-pausing.svg" - DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/scalable/status") - install(FILES "${SUNSHINE_SOURCE_ASSETS_DIR}/common/assets/web/public/images/sunshine-locked.svg" - DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/scalable/status") + if(NOT ${SUNSHINE_BUILD_FLATPAK}) + install(FILES "${CMAKE_SOURCE_DIR}/sunshine.svg" + DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/scalable/status" + RENAME "sunshine-tray.svg") + install(FILES "${SUNSHINE_SOURCE_ASSETS_DIR}/common/assets/web/public/images/sunshine-playing.svg" + DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/scalable/status") + install(FILES "${SUNSHINE_SOURCE_ASSETS_DIR}/common/assets/web/public/images/sunshine-pausing.svg" + DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/scalable/status") + install(FILES "${SUNSHINE_SOURCE_ASSETS_DIR}/common/assets/web/public/images/sunshine-locked.svg" + DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/scalable/status") + else() + # flatpak icons must be prefixed with the app id or they will not be included in the flatpak + install(FILES "${CMAKE_SOURCE_DIR}/sunshine.svg" + DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/scalable/status" + RENAME "${PROJECT_FQDN}-tray.svg") + install(FILES "${SUNSHINE_SOURCE_ASSETS_DIR}/common/assets/web/public/images/sunshine-playing.svg" + DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/scalable/status" + RENAME "${PROJECT_FQDN}-playing.svg") + install(FILES "${SUNSHINE_SOURCE_ASSETS_DIR}/common/assets/web/public/images/sunshine-pausing.svg" + DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/scalable/status" + RENAME "${PROJECT_FQDN}-pausing.svg") + install(FILES "${SUNSHINE_SOURCE_ASSETS_DIR}/common/assets/web/public/images/sunshine-locked.svg" + DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/scalable/status" + RENAME "${PROJECT_FQDN}-locked.svg") + endif() set(CPACK_DEBIAN_PACKAGE_DEPENDS "\ ${CPACK_DEBIAN_PACKAGE_DEPENDS}, \ @@ -128,15 +144,8 @@ else() install(FILES "${CMAKE_CURRENT_BINARY_DIR}/sunshine.desktop" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/applications" RENAME "${PROJECT_FQDN}.desktop") - install(FILES "${CMAKE_CURRENT_BINARY_DIR}/sunshine_kms.desktop" - DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/applications" - RENAME "${PROJECT_FQDN}_kms.desktop") endif() -if(${SUNSHINE_BUILD_FLATPAK}) - install(FILES "${CMAKE_CURRENT_BINARY_DIR}/sunshine_terminal.desktop" - DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/applications" - RENAME "${PROJECT_FQDN}_terminal.desktop") -elseif(NOT ${SUNSHINE_BUILD_APPIMAGE}) +if(NOT ${SUNSHINE_BUILD_APPIMAGE} AND NOT ${SUNSHINE_BUILD_FLATPAK}) install(FILES "${CMAKE_CURRENT_BINARY_DIR}/sunshine_terminal.desktop" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/applications") endif() diff --git a/cmake/packaging/windows.cmake b/cmake/packaging/windows.cmake index f8df493d5b5..fb949aea848 100644 --- a/cmake/packaging/windows.cmake +++ b/cmake/packaging/windows.cmake @@ -11,7 +11,6 @@ install(TARGETS dxgi-info RUNTIME DESTINATION "tools" COMPONENT dxgi) install(TARGETS audio-info RUNTIME DESTINATION "tools" COMPONENT audio) # Mandatory tools -install(TARGETS ddprobe RUNTIME DESTINATION "tools" COMPONENT application) install(TARGETS sunshinesvc RUNTIME DESTINATION "tools" COMPONENT application) # Mandatory scripts @@ -21,6 +20,9 @@ install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/windows/misc/service/" install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/windows/misc/migration/" DESTINATION "scripts" COMPONENT assets) +install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/windows/misc/path/" + DESTINATION "scripts" + COMPONENT assets) # Configurable options for the service install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/windows/misc/autostart/" @@ -63,8 +65,9 @@ set(CPACK_PACKAGE_INSTALL_DIRECTORY "${CPACK_PACKAGE_NAME}") SET(CPACK_NSIS_EXTRA_INSTALL_COMMANDS "${CPACK_NSIS_EXTRA_INSTALL_COMMANDS} IfSilent +2 0 - ExecShell 'open' 'https://sunshinestream.readthedocs.io/' + ExecShell 'open' 'https://docs.lizardbyte.dev/projects/sunshine' nsExec::ExecToLog 'icacls \\\"$INSTDIR\\\" /reset' + nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\update-path.bat\\\" add' nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\migrate-config.bat\\\"' nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\add-firewall-rule.bat\\\"' nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\install-gamepad.bat\\\"' @@ -79,9 +82,9 @@ set(CPACK_NSIS_EXTRA_UNINSTALL_COMMANDS "${CPACK_NSIS_EXTRA_UNINSTALL_COMMANDS} nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\delete-firewall-rule.bat\\\"' nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\uninstall-service.bat\\\"' - nsExec::ExecToLog '\\\"$INSTDIR\\\\sunshine.exe\\\" --restore-nvprefs-undo' + nsExec::ExecToLog '\\\"$INSTDIR\\\\${CMAKE_PROJECT_NAME}.exe\\\" --restore-nvprefs-undo' MessageBox MB_YESNO|MB_ICONQUESTION \ - 'Do you want to remove Virtual Gamepad)?' \ + 'Do you want to remove Virtual Gamepad?' \ /SD IDNO IDNO NoGamepad nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\uninstall-gamepad.bat\\\"'; skipped if no NoGamepad: @@ -89,16 +92,18 @@ set(CPACK_NSIS_EXTRA_UNINSTALL_COMMANDS 'Do you want to remove $INSTDIR (this includes the configuration, cover images, and settings)?' \ /SD IDNO IDNO NoDelete RMDir /r \\\"$INSTDIR\\\"; skipped if no + nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\update-path.bat\\\" remove' NoDelete: ") # Adding an option for the start menu -set(CPACK_NSIS_MODIFY_PATH "OFF") +set(CPACK_NSIS_MODIFY_PATH OFF) set(CPACK_NSIS_EXECUTABLES_DIRECTORY ".") # This will be shown on the installed apps Windows settings set(CPACK_NSIS_INSTALLED_ICON_NAME "${CMAKE_PROJECT_NAME}.exe") set(CPACK_NSIS_CREATE_ICONS_EXTRA "${CPACK_NSIS_CREATE_ICONS_EXTRA} + SetOutPath '\$INSTDIR' CreateShortCut '\$SMPROGRAMS\\\\$STARTMENU_FOLDER\\\\${CMAKE_PROJECT_NAME}.lnk' \ '\$INSTDIR\\\\${CMAKE_PROJECT_NAME}.exe' '--shortcut' ") @@ -110,12 +115,12 @@ set(CPACK_NSIS_DELETE_ICONS_EXTRA # Checking for previous installed versions set(CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL "ON") -set(CPACK_NSIS_HELP_LINK "https://sunshinestream.readthedocs.io/en/latest/about/installation.html") +set(CPACK_NSIS_HELP_LINK "https://docs.lizardbyte.dev/projects/sunshine/latest/md_docs_2getting__started.html") set(CPACK_NSIS_URL_INFO_ABOUT "${CMAKE_PROJECT_HOMEPAGE_URL}") set(CPACK_NSIS_CONTACT "${CMAKE_PROJECT_HOMEPAGE_URL}/support") set(CPACK_NSIS_MENU_LINKS - "https://sunshinestream.readthedocs.io" "Sunshine documentation" + "https://docs.lizardbyte.dev/projects/sunshine" "Sunshine documentation" "https://app.lizardbyte.dev" "LizardByte Web Site" "https://app.lizardbyte.dev/support" "LizardByte Support") set(CPACK_NSIS_MANIFEST_DPI_AWARE true) diff --git a/cmake/prep/init.cmake b/cmake/prep/init.cmake index ae15d9f2673..4c03ecfb946 100644 --- a/cmake/prep/init.cmake +++ b/cmake/prep/init.cmake @@ -8,10 +8,10 @@ elseif (UNIX) endif() if(SUNSHINE_BUILD_FLATPAK) - set(SUNSHINE_SERVICE_START_COMMAND "ExecStart=${PROJECT_FQDN}") + set(SUNSHINE_SERVICE_START_COMMAND "ExecStart=flatpak run --command=sunshine ${PROJECT_FQDN}") set(SUNSHINE_SERVICE_STOP_COMMAND "ExecStop=flatpak kill ${PROJECT_FQDN}") else() set(SUNSHINE_SERVICE_START_COMMAND "ExecStart=${SUNSHINE_EXECUTABLE_PATH}") set(SUNSHINE_SERVICE_STOP_COMMAND "") endif() -endif () +endif() diff --git a/cmake/prep/options.cmake b/cmake/prep/options.cmake index ba1dc193238..0043b137704 100644 --- a/cmake/prep/options.cmake +++ b/cmake/prep/options.cmake @@ -12,10 +12,6 @@ option(BUILD_TESTS "Build tests" ON) option(NPM_OFFLINE "Use offline npm packages. You must ensure packages are in your npm cache." OFF) option(TESTS_ENABLE_PYTHON_TESTS "Enable Python tests" ON) -# DirectX11 is not available in GitHub runners, so even software encoding fails -set(TESTS_SOFTWARE_ENCODER_UNAVAILABLE "fail" - CACHE STRING "How to handle unavailable software encoders in tests. 'fail/skip'") - option(BUILD_WERROR "Enable -Werror flag." OFF) # if this option is set, the build will exit after configuring special package configuration files @@ -69,6 +65,4 @@ elseif(UNIX) # Linux "Enable building wayland specific code." ON) option(SUNSHINE_ENABLE_X11 "Enable X11 grab if available." ON) - option(SUNSHINE_USE_LEGACY_INPUT # TODO: Remove this legacy option after the next stable release - "Use the legacy virtual input implementation." OFF) endif() diff --git a/cmake/prep/special_package_configuration.cmake b/cmake/prep/special_package_configuration.cmake index df1bbb36a6b..298c0226faf 100644 --- a/cmake/prep/special_package_configuration.cmake +++ b/cmake/prep/special_package_configuration.cmake @@ -10,14 +10,12 @@ if(APPLE) endif() elseif(UNIX) # configure the .desktop file - set(SUNSHINE_DESKTOP_ICON "sunshine.svg") + set(SUNSHINE_DESKTOP_ICON "sunshine") if(${SUNSHINE_BUILD_APPIMAGE}) configure_file(packaging/linux/AppImage/sunshine.desktop sunshine.desktop @ONLY) elseif(${SUNSHINE_BUILD_FLATPAK}) - set(SUNSHINE_DESKTOP_ICON "${PROJECT_FQDN}.svg") + set(SUNSHINE_DESKTOP_ICON "${PROJECT_FQDN}") configure_file(packaging/linux/flatpak/sunshine.desktop sunshine.desktop @ONLY) - configure_file(packaging/linux/flatpak/sunshine_kms.desktop sunshine_kms.desktop @ONLY) - configure_file(packaging/linux/sunshine_terminal.desktop sunshine_terminal.desktop @ONLY) configure_file(packaging/linux/flatpak/${PROJECT_FQDN}.metainfo.xml ${PROJECT_FQDN}.metainfo.xml @ONLY) else() diff --git a/docs/Doxyfile b/docs/Doxyfile index 5695a2d6240..8549dbba13b 100644 --- a/docs/Doxyfile +++ b/docs/Doxyfile @@ -31,8 +31,7 @@ PROJECT_NAME = Sunshine # project specific settings DOT_GRAPH_MAX_NODES = 60 -IMAGE_PATH = ../docs/images -INCLUDE_PATH = ../third-party/build-deps/dist/Linux-x86_64/include/ +# IMAGE_PATH = ../docs/images PREDEFINED += SUNSHINE_BUILD_WAYLAND PREDEFINED += SUNSHINE_TRAY=1 @@ -50,6 +49,7 @@ INPUT = ../README.md \ legal.md \ configuration.md \ app_examples.md \ + awesome_sunshine.md \ guides.md \ performance_tuning.md \ api.md \ @@ -63,4 +63,8 @@ INPUT = ../README.md \ HTML_EXTRA_STYLESHEET += doc-styles.css # extra js +HTML_EXTRA_FILES += api.js HTML_EXTRA_FILES += configuration.js + +# custom aliases +ALIASES += api_examples{3|}="@htmlonly@endhtmlonly" diff --git a/docs/api.js b/docs/api.js new file mode 100644 index 00000000000..5f887e94e93 --- /dev/null +++ b/docs/api.js @@ -0,0 +1,130 @@ +function generateExamples(endpoint, method, body = null) { + let curlBodyString = ''; + let psBodyString = ''; + + if (body) { + const curlJsonString = JSON.stringify(body).replace(/"/g, '\\"'); + curlBodyString = ` -d "${curlJsonString}"`; + psBodyString = `-Body (ConvertTo-Json ${JSON.stringify(body)})`; + } + + return { + cURL: `curl -u user:pass -X ${method.trim()} -k https://localhost:47990${endpoint.trim()}${curlBodyString}`, + Python: `import json +import requests +from requests.auth import HTTPBasicAuth + +requests.${method.trim().toLowerCase()}( + auth=HTTPBasicAuth('user', 'pass'), + url='https://localhost:47990${endpoint.trim()}', + verify=False,${body ? `\n json=${JSON.stringify(body)},` : ''} +).json()`, + JavaScript: `fetch('https://localhost:47990${endpoint.trim()}', { + method: '${method.trim()}', + headers: { + 'Authorization': 'Basic ' + btoa('user:pass'), + 'Content-Type': 'application/json', + }${body ? `,\n body: JSON.stringify(${JSON.stringify(body)}),` : ''} +}) +.then(response => response.json()) +.then(data => console.log(data));`, + PowerShell: `Invoke-RestMethod \` + -SkipCertificateCheck \` + -Uri 'https://localhost:47990${endpoint.trim()}' \` + -Method ${method.trim()} \` + -Headers @{Authorization = 'Basic ' + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes('user:pass'))} + ${psBodyString}` + }; +} + +function hashString(str) { + let hash = 0; + for (let i = 0; i < str.length; i++) { + const char = str.charCodeAt(i); + hash = (hash << 5) - hash + char; + hash |= 0; // Convert to 32bit integer + } + return hash; +} + +function createTabs(examples) { + const languages = Object.keys(examples); + let tabs = '
'; + let content = '
'; + + languages.forEach((lang, index) => { + const hash = hashString(examples[lang]); + tabs += ``; + content += `
+
+
+ ${examples[lang].split('\n').map(line => `
${line}
`).join('')} +
+ + + + + + +
+
`; + }); + + tabs += '
'; + content += '
'; + + setTimeout(() => { + languages.forEach((lang, index) => { + const hash = hashString(examples[lang]); + const copyButton = document.getElementById(`copy-button-${lang}-${hash}`); + copyButton.addEventListener('click', copyContent); + }); + }, 0); + + return tabs + content; +} + +function copyContent() { + const content = this.previousElementSibling.cloneNode(true); + if (content instanceof Element) { + // filter out line number from file listings + content.querySelectorAll(".lineno, .ttc").forEach((node) => { + node.remove(); + }); + let textContent = Array.from(content.querySelectorAll('.line')) + .map(line => line.innerText) + .join('\n') + .trim(); // Join lines with newline characters and trim leading/trailing whitespace + navigator.clipboard.writeText(textContent); + this.classList.add("success"); + this.innerHTML = ``; + window.setTimeout(() => { + this.classList.remove("success"); + this.innerHTML = ``; + }, 980); + } else { + console.error('Failed to copy: content is not a DOM element'); + } +} + +function openTab(evt, lang) { + const tabcontent = document.getElementsByClassName("tabcontent"); + for (const content of tabcontent) { + content.style.display = "none"; + } + + const tablinks = document.getElementsByClassName("tab-button"); + for (const link of tablinks) { + link.className = link.className.replace(" active", ""); + } + + const selectedTabs = document.querySelectorAll(`#${lang}`); + for (const tab of selectedTabs) { + tab.style.display = "block"; + } + + const selectedButtons = document.querySelectorAll(`.tab-button[onclick*="${lang}"]`); + for (const button of selectedButtons) { + button.className += " active"; + } +} diff --git a/docs/api.md b/docs/api.md index 2c6e640989d..5313d5988f6 100644 --- a/docs/api.md +++ b/docs/api.md @@ -5,20 +5,30 @@ Sunshine has a RESTful API which can be used to interact with the service. Unless otherwise specified, authentication is required for all API calls. You can authenticate using basic authentication with the admin username and password. +@htmlonly + +@endhtmlonly + ## GET /api/apps @copydoc confighttp::getApps() -## GET /api/logs -@copydoc confighttp::getLogs() - ## POST /api/apps @copydoc confighttp::saveApp() -## DELETE /api/apps{index} +## POST /api/apps/close +@copydoc confighttp::closeApp() + +## DELETE /api/apps/{index} @copydoc confighttp::deleteApp() -## POST /api/covers/upload -@copydoc confighttp::uploadCover() +## GET /api/clients/list +@copydoc confighttp::getClients() + +## POST /api/clients/unpair +@copydoc confighttp::unpair() + +## POST /api/clients/unpair-all +@copydoc confighttp::unpairAll() ## GET /api/config @copydoc confighttp::getConfig() @@ -29,8 +39,11 @@ basic authentication with the admin username and password. ## POST /api/config @copydoc confighttp::saveConfig() -## POST /api/restart -@copydoc confighttp::restart() +## POST /api/covers/upload +@copydoc confighttp::uploadCover() + +## GET /api/logs +@copydoc confighttp::getLogs() ## POST /api/password @copydoc confighttp::savePassword() @@ -38,17 +51,11 @@ basic authentication with the admin username and password. ## POST /api/pin @copydoc confighttp::savePin() -## POST /api/clients/unpair-all -@copydoc confighttp::unpairAll() - -## POST /api/clients/unpair -@copydoc confighttp::unpair() - -## GET /api/clients/list -@copydoc confighttp::listClients() +## POST /api/reset-display-device-persistence +@copydoc confighttp::resetDisplayDevicePersistence() -## GET /api/apps/close -@copydoc confighttp::closeApp() +## POST /api/restart +@copydoc confighttp::restart()
diff --git a/docs/app_examples.md b/docs/app_examples.md index 015fb7913e1..782681fdc56 100644 --- a/docs/app_examples.md +++ b/docs/app_examples.md @@ -23,25 +23,28 @@ process is killed.} @tabs{ @tab{Linux | - \| Field \| Value \| - \|-------------------\|-----------------------------------------------------\| - \| Application Name \| @code{}Steam Big Picture@endcode \| - \| Detached Commands \| @code{}setsid steam steam://open/bigpicture@endcode \| - \| Image \| @code{}steam.png@endcode \| + \| 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{macOS | - \| Field \| Value \| - \|-------------------\|---------------------------------------------------\| - \| Application Name \| @code{}Steam Big Picture@endcode \| - \| Detached Commands \| @code{}open steam steam://open/bigpicture@endcode \| - \| Image \| @code{}steam.png@endcode \| + \| Field \| Value \| + \|------------------------------\|------------------------------------------------\| + \| Application Name \| @code{}Steam Big Picture@endcode \| + \| Command Preporations -> Undo \| @code{}open steam://close/bigpicture@endcode \| + \| Detached Commands \| @code{}open steam://open/bigpicture@endcode \| + \| Image \| @code{}steam.png@endcode \| } @tab{Windows | - \| Field \| Value \| - \|-------------------\|----------------------------------------\| - \| Application Name \| @code{}Steam Big Picture@endcode \| - \| Detached Commands \| @code{}steam://open/bigpicture@endcode \| - \| Image \| @code{}steam.png@endcode \| + \| Field \| Value \| + \|------------------------------\|-------------------------------------------\| + \| Application Name \| @code{}Steam Big Picture@endcode \| + \| Command Preporations -> Undo \| @code{}steam://close/bigpicture@endcode \| + \| Detached Commands \| @code{}steam://open/bigpicture@endcode \| + \| Image \| @code{}steam.png@endcode \| } } @@ -167,7 +170,7 @@ process is killed.} | 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. +and iOS devices, since they use non-standard resolutions. You can update the ``Do`` command to this: ```bash @@ -210,7 +213,7 @@ xrandr --output ${display_output} --primary --mode ${mode_alias} --pos 0x0 --rot ``` } -###### Wayland +###### Wayland (wlroots, e.g. hyprland) | Prep Step | Command | |-----------|------------------------------------------------------------------------------------------------------------------------------------------| @@ -219,17 +222,30 @@ xrandr --output ${display_output} --primary --mode ${mode_alias} --pos 0x0 --rot @hint{`wlr-xrandr` only works with wlroots-based compositors.} -###### Gnome (Wayland, X11) +###### Gnome (X11) | Prep Step | Command | |-----------|---------------------------------------------------------------------------------------------------------------------------------------| | 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 | -The commands above are valid for an X11 session but won't work for -Wayland. In that case `xrandr` must be replaced by [gnome-randr.py](https://gitlab.com/Oschowa/gnome-randr). -This script is intended as a drop-in replacement with the same syntax. (It can be saved in -`/usr/local/bin` and needs to be made executable.) +###### Gnome (Wayland) + +| Prep Step | Command | +|-----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Do | @code{}sh -c "displayconfig-mutter set --connector HDMI-1 --resolution ${SUNSHINE_CLIENT_WIDTH}x${SUNSHINE_CLIENT_HEIGHT} --refresh-rate ${SUNSHINE_CLIENT_FPS} --hdr ${SUNSHINE_CLIENT_HDR}"@endcode | +| Undo | @code{}displayconfig-mutter set --connector HDMI-1 --resolution 3840x2160 --refresh-rate 120 --hdr false@endcode | + +Installation instructions for displayconfig-mutter can be [found here](https://github.com/eaglesemanation/displayconfig-mutter). Alternatives include +[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. +} ###### KDE Plasma (Wayland, X11) @@ -257,22 +273,10 @@ hard-coding their corresponding number (e.g. ``kscreen-doctor output.HDMI-A1.mod ###### NVIDIA -| Prep Step | Command | -|-----------|-------------------------------------------------------------------------------------------------------------| -| Do | @code{}sh -c "${HOME}/scripts/set-custom-res.sh ${SUNSHINE_CLIENT_WIDTH} ${SUNSHINE_CLIENT_HEIGHT}"@endcode | -| Undo | @code{}sh -c "${HOME}/scripts/set-custom-res.sh 3840 2160"@endcode | - -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} -output=${3:-HDMI-1} -nvidia-settings -a CurrentMetaMode="${output}: nvidia-auto-select { ViewPortIn=${width}x${height}, ViewPortOut=${width}x${height}+0+0 }" -``` +| Prep Step | Command | +|-----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Do | @code{}sh -c "nvidia-settings -a CurrentMetaMode=\"HDMI-1: nvidia-auto-select { ViewPortIn=${SUNSHINE_CLIENT_WIDTH}x${SUNSHINE_CLIENT_HEIGHT}, ViewPortOut=${SUNSHINE_CLIENT_WIDTH}x${SUNSHINE_CLIENT_HEIGHT}+0+0 }\""@endcode | +| Undo | @code{}nvidia-settings -a CurrentMetaMode=\"HDMI-1: nvidia-auto-select { ViewPortIn=3840x2160, ViewPortOut=3840x2160+0+0 }"@endcode | ##### macOS @@ -281,21 +285,23 @@ nvidia-settings -a CurrentMetaMode="${output}: nvidia-auto-select { ViewPortIn=$ This tool can be installed following instructions in their [GitHub repository](https://github.com/jakehilborn/displayplacer)}. -| Prep Step | Command | -|-----------|----------------------------------------------------------------------------------------------------| -| Do | @code{}displayplacer "id: res:1920x1080 hz:60 scaling:on origin:(0,0) degree:0"@endcode | -| Undo | @code{}displayplacer "id: res:3840x2160 hz:120 scaling:on origin:(0,0) degree:0"@endcode | +| Prep Step | Command | +|-----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Do | @code{}sh -c "displayplacer \"id: res:${SUNSHINE_CLIENT_WIDTH}x${SUNSHINE_CLIENT_HEIGHT} hz:${SUNSHINE_CLIENT_FPS} scaling:on origin:(0,0) degree:0\""@endcode | +| Undo | @code{}displayplacer "id: res:3840x2160 hz:120 scaling:on origin:(0,0) degree:0"@endcode | ##### Windows +Sunshine has built-in support for changing the resolution and refresh rate on Windows. If you prefer to use a +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).}. +This tool can be downloaded from their [SourceForge repository](https://sourceforge.net/projects/qres).} -| Prep Step | Command | -|-----------|-------------------------------------------------------------------------------------------------------------------------| -| Do | @code{}cmd /C FullPath\qres.exe /x:%SUNSHINE_CLIENT_WIDTH% /y:%SUNSHINE_CLIENT_HEIGHT% /r:%SUNSHINE_CLIENT_FPS%@endcode | -| Undo | @code{}cmd /C FullPath\qres.exe /x:3840 /y:2160 /r:120@endcode | +| Prep Step | Command | +|-----------|---------------------------------------------------------------------------------------------------------------------------| +| Do | @code{}cmd /C "FullPath\qres.exe /x:%SUNSHINE_CLIENT_WIDTH% /y:%SUNSHINE_CLIENT_HEIGHT% /r:%SUNSHINE_CLIENT_FPS%"@endcode | +| Undo | @code{}FullPath\qres.exe /x:3840 /y:2160 /r:120@endcode | ### Additional Considerations @@ -311,22 +317,19 @@ administrative privileges. Simply enable the elevated option in the WEB UI, or a This is an option for both prep-cmd and regular commands and will launch the process with the current user without a UAC prompt. -@note{It is important to write the values "true" and "false" as string values, not as the typical true/false -values in most JSON.} - **Example** ```json { "name": "Game With AntiCheat that Requires Admin", "output": "", "cmd": "ping 127.0.0.1", - "exclude-global-prep-cmd": "false", - "elevated": "true", + "exclude-global-prep-cmd": false, + "elevated": true, "prep-cmd": [ { "do": "powershell.exe -command \"Start-Streaming\"", "undo": "powershell.exe -command \"Stop-Streaming\"", - "elevated": "false" + "elevated": false } ], "image-path": "" @@ -335,9 +338,9 @@ values in most JSON.}
-| Previous | Next | -|:----------------------------------|--------------------:| -| [Configuration](configuration.md) | [Guides](guides.md) | +| Previous | Next | +|:----------------------------------|----------------------------------------:| +| [Configuration](configuration.md) | [Awesome-Sunshine](awesome_sunshine.md) |
diff --git a/docs/awesome_sunshine.md b/docs/awesome_sunshine.md new file mode 100644 index 00000000000..554c8113e7b --- /dev/null +++ b/docs/awesome_sunshine.md @@ -0,0 +1,23 @@ +# Awesome-Sunshine + +@htmlonly + + + +@endhtmlonly + +
+ +| Previous | Next | +|:--------------------------------|--------------------:| +| [App Examples](app_examples.md) | [Guides](guides.md) | + +
+ +
+ + [TOC] +
diff --git a/docs/building.md b/docs/building.md index 851b21b1f50..b7f5d402601 100644 --- a/docs/building.md +++ b/docs/building.md @@ -83,15 +83,15 @@ pacman -Syu ##### Install dependencies ```bash dependencies=( - "doxygen" # Optional, for docs "git" "mingw-w64-ucrt-x86_64-boost" # Optional "mingw-w64-ucrt-x86_64-cmake" "mingw-w64-ucrt-x86_64-cppwinrt" "mingw-w64-ucrt-x86_64-curl-winssl" + "mingw-w64-ucrt-x86_64-doxygen" # Optional, for docs... better to install official Doxygen "mingw-w64-ucrt-x86_64-graphviz" # Optional, for docs + "mingw-w64-ucrt-x86_64-MinHook" "mingw-w64-ucrt-x86_64-miniupnpc" - "mingw-w64-ucrt-x86_64-nlohmann-json" "mingw-w64-ucrt-x86_64-nodejs" "mingw-w64-ucrt-x86_64-nsis" "mingw-w64-ucrt-x86_64-onevpl" diff --git a/docs/configuration.md b/docs/configuration.md index d4e3354045b..ef2486436d1 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -57,7 +57,11 @@ editing the `conf` file in a text editor. Use the examples as reference. @endcode - Choices + Choices + bg + Bulgarian + + de German @@ -89,10 +93,22 @@ editing the `conf` file in a text editor. Use the examples as reference. ja Japanese + + ko + Korean + + + pl + Polish + pt Portuguese + + pt_BR + Portuguese (Brazilian) + ru Russian @@ -105,6 +121,10 @@ editing the `conf` file in a text editor. Use the examples as reference. tr Turkish + + uk + Ukranian + zh Chinese (Simplified) @@ -205,7 +225,7 @@ editing the `conf` file in a text editor. Use the examples as reference. Example @code{} - global_prep_cmd = [{"do":"nircmd.exe setdisplay 1280 720 32 144","undo":"nircmd.exe setdisplay 2560 1440 32 144"}] + global_prep_cmd = [{"do":"nircmd.exe setdisplay 1280 720 32 144","elevated":true,"undo":"nircmd.exe setdisplay 2560 1440 32 144"}] @endcode @@ -750,6 +770,29 @@ editing the `conf` file in a text editor. Use the examples as reference. +### stream_audio + + + + + + + + + + + + + + +
Description + Whether to stream audio or not. Disabling this can be useful for streaming headless displays as second monitors. +
Default@code{} + enabled + @endcode
Example@code{} + stream_audio = disabled + @endcode
+ ### install_steam_audio_drivers @@ -865,10 +908,56 @@ editing the `conf` file in a text editor. Use the examples as reference.
**Windows:**
- Enter the following command in command prompt or PowerShell. + During Sunshine startup, you should see the list of detected displays: @code{} - %ProgramFiles%\Sunshine\tools\dxgi-info.exe + Info: Currently available display devices: + [ + { + "device_id": "{64243705-4020-5895-b923-adc862c3457e}", + "display_name": "", + "friendly_name": "IDD HDR", + "info": null + }, + { + "device_id": "{77f67f3e-754f-5d31-af64-ee037e18100a}", + "display_name": "", + "friendly_name": "SunshineHDR", + "info": null + }, + { + "device_id": "{daeac860-f4db-5208-b1f5-cf59444fb768}", + "display_name": "\\\\.\\DISPLAY1", + "friendly_name": "ROG PG279Q", + "info": { + "hdr_state": null, + "origin_point": { + "x": 0, + "y": 0 + }, + "primary": true, + "refresh_rate": { + "type": "rational", + "value": { + "denominator": 1000, + "numerator": 119998 + } + }, + "resolution": { + "height": 1440, + "width": 2560 + }, + "resolution_scale": { + "type": "rational", + "value": { + "denominator": 100, + "numerator": 100 + } + } + } + } + ] @endcode + You need to use the `device_id` value. } @@ -891,7 +980,385 @@ editing the `conf` file in a text editor. Use the examples as reference. + +
Example (Windows) @code{} - output_name = \\.\DISPLAY1 + output_name = {daeac860-f4db-5208-b1f5-cf59444fb768} + @endcode
+ +### dd_configuration_option + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Description + Perform mandatory verification and additional configuration for the display device. + @note{Applies to Windows only.} +
Default@code{} + disabled + @endcode
Example@code{} + dd_configuration_option = ensure_only_display + @endcode
ChoicesdisabledPerform no additional configuration (disables all `dd_` configuration options).
verify_onlyVerify that display is active only (this is a mandatory step without any extra steps to verify display state).
ensure_activeActivate the display if it's currently inactive.
ensure_primaryActivate the display if it's currently inactive and make it primary.
ensure_only_displayActivate the display if it's currently inactive and disable all others.
+ +### dd_resolution_option + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Description + Perform additional resolution configuration for the display device. + @note{"Optimize game settings" must be enabled in Moonlight for this option to work.} + @note{Applies to Windows only.} +
Default@code{}auto@endcode
Example@code{} + dd_resolution_option = manual + @endcode
ChoicesdisabledPerform no additional configuration.
autoChange resolution to the requested resolution from the client.
manualChange resolution to the user specified one (set via [dd_manual_resolution](#dd_manual_resolution)).
+ +### dd_manual_resolution + + + + + + + + + + + + + + +
Description + Specify manual resolution to be used. + @note{[dd_resolution_option](#dd_resolution_option) must be set to `manual`} + @note{Applies to Windows only.} +
Defaultn/a
Example@code{} + dd_manual_resolution = 1920x1080 + @endcode
+ +### dd_refresh_rate_option + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Description + Perform additional refresh rate configuration for the display device. + @note{Applies to Windows only.} +
Default@code{}auto@endcode
Example@code{} + dd_refresh_rate_option = manual + @endcode
ChoicesdisabledPerform no additional configuration.
autoChange refresh rate to the requested FPS value from the client.
manualChange refresh rate to the user specified one (set via [dd_manual_refresh_rate](#dd_manual_refresh_rate)).
+ +### dd_manual_refresh_rate + + + + + + + + + + + + + + +
Description + Specify manual refresh rate to be used. + @note{[dd_refresh_rate_option](#dd_refresh_rate_option) must be set to `manual`} + @note{Applies to Windows only.} +
Defaultn/a
Example@code{} + dd_manual_resolution = 120 + dd_manual_resolution = 59.95 + @endcode
+ +### dd_hdr_option + + + + + + + + + + + + + + + + + + + + + + + +
Description + Perform additional HDR configuration for the display device. + @note{Applies to Windows only.} +
Default@code{}auto@endcode
Example@code{} + dd_hdr_option = disabled + @endcode
ChoicesdisabledPerform no additional configuration.
autoChange HDR to the requested state from the client if the display supports it.
+ +### dd_wa_hdr_toggle_delay + + + + + + + + + + + + + + +
Description + When using virtual display device (VDD) for streaming, it might incorrectly display HDR color. Sunshine can try to mitigate this issue, by turning HDR off and then on again.
+ If the value is set to 0, the workaround is disabled (default). If the value is between 0 and 3000 milliseconds, Sunshine will turn off HDR, wait for the specified amount of time and then turn HDR on again. The recommended delay time is around 500 milliseconds in most cases.
+ DO NOT use this workaround unless you actually have issues with HDR as it directly impacts stream start time! + @note{This option works independently of [dd_hdr_option](#dd_hdr_option)} + @note{Applies to Windows only.} +
Default@code{} + 0 + @endcode
Example@code{} + dd_wa_hdr_toggle_delay = 500 + @endcode
+ +### dd_config_revert_delay + + + + + + + + + + + + + + +
Description + Additional delay in milliseconds to wait before reverting configuration when the app has been closed or the last session terminated. + Main purpose is to provide a smoother transition when quickly switching between apps. + @note{Applies to Windows only.} +
Default@code{}3000@endcode
Example@code{} + dd_config_revert_delay = 1500 + @endcode
+ + +### dd_config_revert_on_disconnect + + + + + + + + + + + + + + +
Description + When enabled, display configuration is reverted upon disconnect of all clients instead of app close or last session termination. + This can be useful for returning to physical usage of the host machine without closing the active app. + @warning{Some applications may not function properly when display configuration is changed while active.} + @note{Applies to Windows only.} +
Default@code{}disabled@endcode
Example@code{} + dd_config_revert_on_disconnect = enabled + @endcode
+ +### dd_mode_remapping + + + + + + + + + + + + + + +
Description + Remap the requested resolution and FPS to another display mode.
+ Depending on the [dd_resolution_option](#dd_resolution_option) and + [dd_refresh_rate_option](#dd_refresh_rate_option) values, the following mapping + groups are available: +
    +
  • `mixed` - both options are set to `auto`.
  • +
  • + `resolution_only` - only [dd_resolution_option](#dd_resolution_option) is set to `auto`. +
  • +
  • + `refresh_rate_only` - only [dd_refresh_rate_option](#dd_refresh_rate_option) is set to `auto`. +
  • +
+ For each of those groups, a list of fields can be configured to perform remapping: +
    +
  • + `requested_resolution` - resolution that needs to be matched in order to use this remapping entry. +
  • +
  • `requested_fps` - FPS that needs to be matched in order to use this remapping entry.
  • +
  • `final_resolution` - resolution value to be used if the entry was matched.
  • +
  • `final_refresh_rate` - refresh rate value to be used if the entry was matched.
  • +
+ If `requested_*` field is left empty, it will match everything.
+ If `final_*` field is left empty, the original value will not be remapped and either a requested, manual + or current value is used. However, at least one `final_*` must be set, otherwise the entry is considered + invalid.
+ @note{"Optimize game settings" must be enabled on client side for ANY entry with `resolution` + field to be considered.} + @note{First entry to be matched in the list is the one that will be used.} + @tip{`requested_resolution` and `final_resolution` can be omitted for `refresh_rate_only` group.} + @tip{`requested_fps` and `final_refresh_rate` can be omitted for `resolution_only` group.} + @note{Applies to Windows only.} +
Default@code{} + dd_mode_remapping = { + "mixed": [], + "resolution_only": [], + "refresh_rate_only": [] + } + @endcode +
Example@code{} + dd_mode_remapping = { + "mixed": [ + { + "requested_fps": "60", + "final_refresh_rate": "119.95", + "requested_resolution": "1920x1080", + "final_resolution": "2560x1440" + }, + { + "requested_fps": "60", + "final_refresh_rate": "120", + "requested_resolution": "", + "final_resolution": "" + } + ], + "resolution_only": [ + { + "requested_resolution": "1920x1080", + "final_resolution": "2560x1440" + } + ], + "refresh_rate_only": [ + { + "requested_fps": "60", + "final_refresh_rate": "119.95" + } + ] + }@endcode +
+ +### max_bitrate + + + + + + + + + + + + +
Description + The maximum bitrate (in Kbps) that Sunshine will encode the stream at. If set to 0, it will always use the bitrate requested by Moonlight. +
Default@code{} + 0 + @endcode
Example@code{} + max_bitrate = 5000 @endcode
@@ -1503,7 +1970,8 @@ editing the `conf` file in a text editor. Use the examples as reference. wlr - Capture for wlroots based Wayland compositors via DMA-BUF. + Capture for wlroots based Wayland compositors via wlr-screencopy-unstable-v1. It is possible to capture + virtual displays in e.g. Hyprland using this method. @note{Applies to Linux only.} diff --git a/docs/contributing.md b/docs/contributing.md index dfc98c3cbdd..a9f20cfa064 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -1,6 +1,12 @@ # Contributing Read our contribution guide in our organization level -[docs](https://lizardbyte.readthedocs.io/en/latest/developers/contributing.html). +[docs](https://docs.lizardbyte.dev/latest/developers/contributing.html). + +## Recommended Tools + +| Tool | Description | +|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|
CLion | Recommended IDE for C++ development. Free licenses available for open source developers through the [JetBrains Open Source Program](https://www.jetbrains.com/community/opensource/). | ## Project Patterns diff --git a/docs/getting_started.md b/docs/getting_started.md index e100d5cc555..adc7a171200 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -30,7 +30,9 @@ See [Docker](../DOCKER_README.md) for more information. CUDA is used for NVFBC capture. -@tip{See [CUDA GPUS](https://developer.nvidia.com/cuda-gpus) to cross-reference Compute Capability to your GPU.} +@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.} @@ -55,7 +57,7 @@ CUDA is used for NVFBC capture. - + @@ -68,7 +70,12 @@ CUDA is used for NVFBC capture. - + + + + + +
CUDA Compatibility
12.0.0 525.60.1350;52;60;61;62;70;72;75;80;86;87;89;9050;52;60;61;62;70;72;75;80;86;87;89;90 sunshine-debian-bookworm-{arch}.deb
sunshine_{arch}.flatpak
Sunshine (copr)Sunshine (copr - Fedora 40/41)
12.8.1570.124.06Sunshine (copr - Fedora 42)
@@ -271,42 +278,6 @@ brew uninstall sunshine @tip{For beta you can replace `sunshine` with `sunshine-beta` in the above commands.} -#### Portfile -This package requires that you have [MacPorts](https://www.macports.org/install.php) installed. - -##### Install -1. Update the Macports sources. - ```bash - sudo nano /opt/local/etc/macports/sources.conf - ``` - - Add this line, replacing your username, below the line that starts with `rsync`. - ```bash - file:///Users//ports - ``` - - `Ctrl+x`, then `Y` to exit and save changes. - -2. Download and install by running the following commands. - ```bash - mkdir -p ~/ports/multimedia/sunshine - cd ~/ports/multimedia/sunshine - curl -OL https://github.com/LizardByte/Sunshine/releases/latest/download/Portfile - cd ~/ports - portindex - sudo port install sunshine - ``` - -##### Install service (optional) -```bash -sudo port load sunshine -``` - -##### Uninstall -```bash -sudo port uninstall sunshine -``` - ### Windows #### Installer (recommended) @@ -371,8 +342,6 @@ recommended for most users. No support will be provided!} scripts/uninstall-service.bat ``` -To uninstall, delete the extracted directory which contains the `sunshine.exe` file. - ## Initial Setup After installation, some initial setup is required. @@ -456,7 +425,7 @@ ssh @ 'startx &; export DISPLAY=:0; sunshine' @tip{You could also utilize the `~/.bash_profile` or `~/.bashrc` files to set up the `DISPLAY` variable.} -@seealso{ See [Remote SSH Headless Setup](md_docs_2guides.html#remote-ssh-headless-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 diff --git a/docs/guides.md b/docs/guides.md index 182399e9da3..8f3d056dc2f 100644 --- a/docs/guides.md +++ b/docs/guides.md @@ -1,1105 +1,15 @@ # Guides -@admonition{Community | This collection of guides is written by the community! -Feel free to contribute your own tips and trips by making a PR.} - - -## Linux - -### Discord call cancellation - -| Author | [RickAndTired](https://github.com/RickAndTired) | -|------------|-------------------------------------------------| -| Difficulty | Easy | - -* Set your normal *Sound Output* volume to 100% - - ![](images/discord_calls_01.png) - -* Start Sunshine - -* Set *Sound Output* to *sink-sunshine-stereo* (if it isn't automatic) - - ![](images/discord_calls_02.png) - -* In Discord, right click *Deafen* and select your normal *Output Device*. - This is also where you will need to adjust output volume for Discord calls - - ![](images/discord_calls_03.png) - -* Open *qpwgraph* - - ![](images/discord_calls_04.png) - -* Connect `sunshine [sunshine-record]` to your normal *Output Device* - * Drag `monitor_FL` to `playback_FL` - * Drag `monitor_FR` to `playback_FR` - - ![](images/discord_calls_05.png) - -### Remote SSH Headless Setup - -| Author | [Eric Dong](https://github.com/e-dong) | -|------------|----------------------------------------| -| Difficulty | Intermediate | - -This is a guide to setup remote SSH into host to startup X server and Sunshine without physical login and dummy plug. -The virtual display is accelerated by the NVidia GPU using the TwinView configuration. - -@attention{This guide is specific for Xorg and NVidia GPUs. I start the X server using the `startx` command. -I also only tested this on an Artix runit init system on LAN. -I didn't have to do anything special with pulseaudio (pipewire untested). - -Pipewire does not seem to work when Sunshine is started over an SSH session. -A workaround to this problem is to kill the Sunshine instance started via SSH, and start a new one -with the permissions of the desktop session. See [Autostart on boot without auto-login](#autostart-on-boot-without-auto-login) - -Keep your monitors plugged in until the [Checkpoint](#checkpoint) step.} - -@tip{Prior to editing any system configurations, you should make a copy of the original file. -This will allow you to use it for reference or revert your changes easily.} - -#### The Big Picture -Once you are done, you will need to perform these 3 steps: - -1. Turn on the host machine -2. Start Sunshine on remote host with a script that: - - * Edits permissions of `/dev/uinput` (added sudo config to execute script with no password prompt) - * Starts X server with `startx` on virtual display - * Starts Sunshine - -3. Startup Moonlight on the client of interest and connect to host - -@hint{As an alternative to SSH... - -**Step 2** can be replaced with autologin and starting Sunshine as a service or putting -`sunshine &` in your `.xinitrc` file if you start your X server with `startx`. -In this case, the workaround for `/dev/uinput` permissions is not needed because the udev rule would be triggered -for "physical" login. See [Linux Setup](md_docs_2getting__started.html#linux). I personally think autologin compromises -the security of the PC, so I went with the remote SSH route. I use the PC more than for gaming, so I don't need a -virtual display everytime I turn on the PC (E.g running updates, config changes, file/media server).} - -First we will [setup the host](#host-setup) and then the [SSH Client](#ssh-client-setup) -(Which may not be the same as the machine running the moonlight client). - -#### Host Setup -We will be setting up: - -1. [Static IP Setup](#static-ip-setup) -2. [SSH Server Setup](#ssh-server-setup) -3. [Virtual Display Setup](#virtual-display-setup) -4. [Uinput Permissions Workaround](#uinput-permissions-workaround) -5. [Stream Launcher Script](#stream-launcher-script) - -#### Static IP Setup -Setup static IP Address for host. For LAN connections you can use DHCP reservation within your assigned range. -e.g. 192.168.x.x. This will allow you to ssh to the host consistently, so the assigned IP address does -not change. It is preferred to set this through your router config. - -#### SSH Server Setup -@note{Most distros have OpenSSH already installed. If it is not present, install OpenSSH using your package manager.} - -@tabs{ - @tab{Debian based | ```bash - sudo apt update - sudo apt install openssh-server - ```} - @tab{Arch based | ```bash - sudo pacman -S openssh - # Install openssh- if you are not using SystemD - # e.g. sudo pacman -S openssh-runit - ```} - @tab{Alpine based | ```bash - sudo apk update - sudo apk add openssh - ```} - @tab{Fedora based (dnf) | ```bash - sudo dnf install openssh-server - ```} - @tab{Fedora based (yum) | ```bash - sudo yum install openssh-server - ```} -} - -Next make sure the OpenSSH daemon is enabled to run when the system starts. - -@tabs{ - @tab{SystemD | ```bash - sudo systemctl enable sshd.service - sudo systemctl start sshd.service # Starts the service now - sudo systemctl status sshd.service # See if the service is running - ```} - @tab{Runit | ```bash - sudo ln -s /etc/runit/sv/sshd /run/runit/service # Enables the OpenSSH daemon to run when system starts - sudo sv start sshd # Starts the service now - sudo sv status sshd # See if the service is running - ```} - @tab{OpenRC | ```bash - rc-update add sshd # Enables service - rc-status # List services to verify sshd is enabled - rc-service sshd start # Starts the service now - ```} -} - -**Disabling PAM in sshd** - -I noticed when the ssh session is disconnected for any reason, `pulseaudio` would disconnect. -This is due to PAM handling sessions. When running `dmesg`, I noticed `elogind` would say removed user session. -In this [Gentoo Forums post](https://forums.gentoo.org/viewtopic-t-1090186-start-0.html), -someone had a similar issue. Starting the X server in the background and exiting out of the console would cause your -session to be removed. - -@caution{According to this [article](https://devicetests.com/ssh-usepam-security-session-status) -disabling PAM increases security, but reduces certain functionality in terms of session handling. -*Do so at your own risk!*} - -Edit the ``sshd_config`` file with the following to disable PAM. - -```txt -usePAM no -``` - -After making changes to the `sshd_config`, restart the sshd service for changes to take effect. - -@tip{Run the command to check the ssh configuration prior to restarting the sshd service. -```bash -sudo sshd -t -f /etc/ssh/sshd_config -``` - -An incorrect configuration will prevent the sshd service from starting, which might mean -losing SSH access to the server.} - -@tabs{ - @tab{SystemD | ```bash - sudo systemctl restart sshd.service - ```} - @tab{Runit | ```bash - sudo sv restart sshd - ```} - @tab{OpenRC | ```bash - sudo rc-service sshd restart - ```} -} - -#### Virtual Display Setup -As an alternative to a dummy dongle, you can use this config to create a virtual display. - -@important{This is only available for NVidia GPUs using Xorg.} - -@hint{Use ``xrandr`` to see name of your active display output. Usually it starts with ``DP`` or ``HDMI``. For me, it is ``DP-0``. -Put this name for the ``ConnectedMonitor`` option under the ``Device`` section. - -```bash -xrandr | grep " connected" | awk '{ print $1 }' -``` -} - -```xorg -Section "ServerLayout" - Identifier "TwinLayout" - Screen 0 "metaScreen" 0 0 -EndSection - -Section "Monitor" - Identifier "Monitor0" - Option "Enable" "true" -EndSection - -Section "Device" - Identifier "Card0" - Driver "nvidia" - VendorName "NVIDIA Corporation" - Option "MetaModes" "1920x1080" - Option "ConnectedMonitor" "DP-0" - Option "ModeValidation" "NoDFPNativeResolutionCheck,NoVirtualSizeCheck,NoMaxPClkCheck,NoHorizSyncCheck,NoVertRefreshCheck,NoWidthAlignmentCheck" -EndSection - -Section "Screen" - Identifier "metaScreen" - Device "Card0" - Monitor "Monitor0" - DefaultDepth 24 - Option "TwinView" "True" - SubSection "Display" - Modes "1920x1080" - EndSubSection -EndSection -``` - -@note{The `ConnectedMonitor` tricks the GPU into thinking a monitor is connected, -even if there is none actually connected! This allows a virtual display to be created that is accelerated with -your GPU! The `ModeValidation` option disables valid resolution checks, so you can choose any -resolution on the host! - -**References** - -* [issue comment on virtual-display-linux](https://github.com/dianariyanto/virtual-display-linux/issues/9#issuecomment-786389065) -* [Nvidia Documentation on Configuring TwinView](https://download.nvidia.com/XFree86/Linux-x86/270.29/README/configtwinview.html) -* [Arch Wiki Nvidia#TwinView](https://wiki.archlinux.org/title/NVIDIA#TwinView) -* [Unix Stack Exchange - How to add virtual display monitor with Nvidia proprietary driver](https://unix.stackexchange.com/questions/559918/how-to-add-virtual-monitor-with-nvidia-proprietary-driver) -} - -#### Uinput Permissions Workaround - -##### Steps -We can use `chown` to change the permissions from a script. Since this requires `sudo`, -we will need to update the sudo configuration to execute this without being prompted for a password. - -1. Create a `sunshine-setup.sh` script to update permissions on `/dev/uinput`. Since we aren't logged into the host, - the udev rule doesn't apply. -2. Update user sudo configuration `/etc/sudoers.d/` to allow the `sunshine-setup.sh` - script to be executed with `sudo`. - -@note{After I setup the :ref:`udev rule ` to get access to `/dev/uinput`, I noticed when I sshed -into the host without physical login, the ACL permissions on `/dev/uinput` were not changed. So I asked -[reddit](https://www.reddit.com/r/linux_gaming/comments/14htuzv/does_sshing_into_host_trigger_udev_rule_on_the). -I discovered that SSH sessions are not the same as a physical login. -I suppose it's not possible for SSH to trigger a udev rule or create a physical login session.} - -##### Setup Script -This script will take care of any preconditions prior to starting up Sunshine. - -Run the following to create a script named something like `sunshine-setup.sh`: - -```bash -echo "chown $(id -un):$(id -gn) /dev/uinput" > sunshine-setup.sh && \ - chmod +x sunshine-setup.sh -``` - -(**Optional**) To Ensure ethernet is being used for streaming, you can block Wi-Fi with `rfkill`. - -Run this command to append the rfkill block command to the script: - -```bash -echo "rfkill block $(rfkill list | grep "Wireless LAN" \ - | sed 's/^\([[:digit:]]\).*/\1/')" >> sunshine-setup.sh -``` - -##### Sudo Configuration -We will manually change the permissions of `/dev/uinput` using `chown`. -You need to use `sudo` to make this change, so add/update the entry in `/etc/sudoers.d/${USER}`. - -@danger{Do so at your own risk! It is more secure to give sudo and no password prompt to a single script, -than a generic executable like chown.} - -@warning{Be very careful of messing this config up. If you make a typo, *YOU LOSE THE ABILITY TO USE SUDO*. -Fortunately, your system is not borked, you will need to login as root to fix the config. -You may want to setup a backup user / SSH into the host as root to fix the config if this happens. -Otherwise, you will need to plug your machine back into a monitor and login as root to fix this. -To enable root login over SSH edit your SSHD config, and add `PermitRootLogin yes`, and restart the SSH server.} - -1. First make a backup of your `/etc/sudoers.d/${USER}` file. - - ```bash - sudo cp /etc/sudoers.d/${USER} /etc/sudoers.d/${USER}.backup - ``` - -2. `cd` to the parent dir of the `sunshine-setup.sh` script and take note of the full filepath. -3. Execute the following to edit your sudoer config file. - -@danger{NEVER modify a file in ``sudoers.d`` directly. Always use the ``visudo`` command. This command checks your changes -before saving the file, and if the resulting changes would break sudo on your system, it will prompt you to fix -them. Modifying the file with nano or vim directly does not give you this sanity check and introduces the -possibility of losing sudo access to your machine. Tread carefully, and make a backup.} - -```bash -sudo visudo /etc/sudoers.d/${USER} -``` - -Copy the below configuration into the text editor. Change `${USER}` wherever it occurs to your username -(e.g. if your username is `sunshineisaawesome` you should change `${USER}` to `sunshineisawesome`) -or modify the path if you placed `sunshine-setup.sh` in a different area. - -``` -${USER} ALL=(ALL:ALL) ALL, NOPASSWD: /home/${USER}/scripts/sunshine-setup.sh -``` - -These changes allow the script to use sudo without being prompted with a password. - -e.g. `sudo $(pwd)/sunshine-setup.sh` - -#### Stream Launcher Script -This is the main entrypoint script that will run the `sunshine-setup.sh` script, start up X server, and Sunshine. -The client will call this script that runs on the host via ssh. - - -##### Sunshine Startup Script -This guide will refer to this script as `~/scripts/sunshine.sh`. -The setup script will be referred as `~/scripts/sunshine-setup.sh`. - -```bash -#!/bin/bash -set -e - -export DISPLAY=:0 - -# Check existing X server -ps -e | grep X >/dev/null -[[ ${?} -ne 0 ]] && { - echo "Starting X server" - startx &>/dev/null & - [[ ${?} -eq 0 ]] && { - echo "X server started successfully" - } || echo "X server failed to start" -} || echo "X server already running" - -# Check if sunshine is already running -ps -e | grep -e .*sunshine$ >/dev/null -[[ ${?} -ne 0 ]] && { - sudo ~/scripts/sunshine-setup.sh - echo "Starting Sunshine!" - sunshine > /dev/null & - [[ ${?} -eq 0 ]] && { - echo "Sunshine started successfully" - } || echo "Sunshine failed to start" -} || echo "Sunshine is already running" - -# Add any other Programs that you want to startup automatically -# e.g. -# steam &> /dev/null & -# firefox &> /dev/null & -# kdeconnect-app &> /dev/null & -``` - -#### SSH Client Setup -We will be setting up: - -1. [SSH Key Authentication Setup](#ssh-key-authentication-setup) -2. [SSH Client Script (Optional)](#ssh-client-script-optional) - -##### SSH Key Authentication Setup -1. Setup your SSH keys with `ssh-keygen` and use `ssh-copy-id` to authorize remote login to your host. - Run `ssh @` to login to your host. - SSH keys automate login so you don't need to input your password! -2. Optionally setup a `~/.ssh/config` file to simplify the `ssh` command - - ```txt - Host - Hostname - User - IdentityFile ~/.ssh/ - ``` - - Now you can use `ssh `. - `ssh ` will execute the command or script on the remote host. - -##### Checkpoint -As a sanity check, let's make sure your setup is working so far! - -###### Test Steps -With your monitor still plugged into your Sunshine host PC: - -1. `ssh ` -2. `~/scripts/sunshine.sh` -3. `nvidia-smi` - - You should see the Sunshine and Xorg processing running: - - ```bash - nvidia-smi - ``` - - *Output:* - ```txt - +---------------------------------------------------------------------------------------+ - | NVIDIA-SMI 535.104.05 Driver Version: 535.104.05 CUDA Version: 12.2 | - |-----------------------------------------+----------------------+----------------------+ - | GPU Name Persistence-M | Bus-Id Disp.A | Volatile Uncorr. ECC | - | Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. | - | | | MIG M. | - |=========================================+======================+======================| - | 0 NVIDIA GeForce RTX 3070 Off | 00000000:01:00.0 On | N/A | - | 30% 46C P2 45W / 220W | 549MiB / 8192MiB | 2% Default | - | | | N/A | - +-----------------------------------------+----------------------+----------------------+ - - +---------------------------------------------------------------------------------------+ - | Processes: | - | GPU GI CI PID Type Process name GPU Memory | - | ID ID Usage | - |=======================================================================================| - | 0 N/A N/A 1393 G /usr/lib/Xorg 86MiB | - | 0 N/A N/A 1440 C+G sunshine 293MiB | - +---------------------------------------------------------------------------------------+ - ``` - -4. Check `/dev/uinput` permissions - - ```bash - ls -l /dev/uinput - ``` - - *Output:* - - ```console - crw------- 1 10, 223 Aug 29 17:31 /dev/uinput - ``` - -5. Connect to Sunshine host from a moonlight client - -Now kill X and Sunshine by running `pkill X` on the host, unplug your monitors from your GPU, and repeat steps 1 - 5. -You should get the same result. -With this setup you don't need to modify the Xorg config regardless if monitors are plugged in or not. - -```bash -pkill X -``` - -##### SSH Client Script (Optional) -At this point you have a working setup! For convenience, I created this bash script to automate the -startup of the X server and Sunshine on the host. -This can be run on Unix systems, or on Windows using the `git-bash` or any bash shell. - -For Android/iOS you can install Linux emulators, e.g. `Userland` for Android and `ISH` for iOS. -The neat part is that you can execute one script to launch Sunshine from your phone or tablet! - -```bash -#!/bin/bash -set -e - -ssh_args="@192.168.X.X" # Or use alias set in ~/.ssh/config - -check_ssh(){ - result=1 - # Note this checks infinitely, you could update this to have a max # of retries - while [[ $result -ne 0 ]] - do - echo "checking host..." - ssh $ssh_args "exit 0" 2>/dev/null - result=$? - [[ $result -ne 0 ]] && { - echo "Failed to ssh to $ssh_args, with exit code $result" - } - sleep 3 - done - echo "Host is ready for streaming!" -} - -start_stream(){ - echo "Starting sunshine server on host..." - echo "Start moonlight on your client of choice" - # -f runs ssh in the background - ssh -f $ssh_args "~/scripts/sunshine.sh &" -} - -check_ssh -start_stream -exit_code=${?} - -sleep 3 -exit ${exit_code} -``` - -#### Next Steps -Congratulations, you can now stream your desktop headless! When trying this the first time, -keep your monitors close by incase something isn't working right. - -@seealso{Now that you have a virtual display, you may want to automate changing the resolution -and refresh rate prior to connecting to an app. See -[Changing Resolution and Refresh Rate](md_docs_2app__examples#changing-resolution-and-refresh-rate) -for more information.} - -### Autostart on boot without auto-login - -| Author | [MidwesternRodent](https://github.com/midwesternrodent) | -| ---------- | ------------------------------------------------------- | -| Difficulty | Intermediate | - -After following this guide you will be able to: -1. Turn on the Sunshine host via Moonlight's Wake on LAN (WoL) feature. -2. Have Sunshine initialize to the login screen ready for you to enter your credentials. -3. Login to your desktop session remotely, and have your pipewire audio and Sunshine tray icon work appropriately. - -#### Specifications -This guide was created with the following software on the host: -1. OpenSSH server and client (both on the host machine) -2. Sunshine v2024.1003.1754422 -3. Debian 12 w/ KDE Plasma, SDDM, Wayland (also tested through xorg), and pipewire for audio. - -The host hardware that was used in developing this guide: -1. AMD 7900XTX -2. AMD Ryzen 7 7800X3D -3. 128GB DDR5 RAM -4. 4 displays in total. 2 1080p displays, 1 3440x1440 display, and 1 4k Roku TV which is used as the always-on display -for streaming. (could be subbed with a dummy plug). - -If you have used this guide on any alternative hardware or software (including non-debian based distros) -please, feel free to modify this guide and keep it growing! - -#### Caveats -1. When you login the machine will close your connection and you will have to reconnect. This is necessary due to an -issue similar to why the [Uinput Permissions Workaround](#uinput-permissions-workaround) is needed since SSH -connections are not treated the same as graphical logins. This causes weirdness like sound not working through -pipewire, and the tray icon for Sunshine not appearing. To get around this, we need to close the SSH initiated Sunshine -service, and start a new Sunshine service with the permissions of the graphical desktop. Unfortunately, this closes the -connection and forces you to reconnect through Moonlight. There is no way around this to the best of my knowledge -without enabling auto-login. -3. This guide does not cover using virtual displays. If you are using Nvidia graphics, -see [Remote SSH Headless Setup](#remote-ssh-headless-setup). If you are using AMD hardware, let me know -if you find something or feel free to add it to this guide. -4. I haven't (yet) found a way to disable sleep on the login screen, so if you wait too long after starting your PC, -the display may go to sleep and Moonlight will have trouble connecting. Shutting down and using WoL works great -though. - -@attention{This is definitely safer than enabling auto-login directly, especially for a dual-use PC that is not only -streamed via Moonlight, but is also used as a standard desktop. *However* I do not know the implications of having an -always running SSH client to the localhost on the same machine. It may be possible for someone with significant -knowledge and physical access to the machine to compromise your user account due to this always-running SSH session. -However, that's better than just having the desktop always available, or opening up SSH even just your LAN since this -guide specifically disables non-localhost connections, so I believe this is safer to use than auto-login for general -users. As always, your [threat model](https://en.wikipedia.org/wiki/Threat_model) may vary.} - -#### Prerequisites -In [Remote SSH Headless Setup](#remote-ssh-headless-setup) complete the following sections. - -1. [Static IP Setup](#static-ip-setup) -2. [SSH Server Setup](#ssh-server-setup) -3. [Virtual Display Setup](#virtual-display-setup) -4. [Uinput Permissions Workaround](#uinput-permissions-workaround) -5. [Stream Launcher Script](#stream-launcher-script) - -@note{On a default Debian 12 install using KDE Plasma, you are using the Simple Desktop Display Manager (SDDM). -Even if you are logging in to a Wayland session, SDDM by default starts with an xorg session, so this script -does not need to be modified if you primarily use a Wayland session (the default) when you login.} - -#### Instructions - -##### Enable Wake on LAN - -Wake on LAN (WoL) will allow you to send a magic packet to turn your PC on remotely. This is handled automatically by -Moonlight's "send wake on lan" option in the app but you do need to enable it on your host machine first. The -[instructions on the debian.org](https://wiki.debian.org/WakeOnLan#Enabling_WOL) site are a little hard to parse, so -I've simplified them below. - -@note{This may not work on all deb based distributions. If you know of a better way for POP OS, Ubuntu, or another -debian based distro please feel free to edit the guide yourself, or let me know.} - -First, find the name of your ethernet interface. - -```bash -ip link show -``` - -When I run this command, these are the results I receive -``` -1: lo: mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000 -   link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 -2: enp117s0: mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000 -   link/ether 9c:6b:00:59:33:c1 brd ff:ff:ff:ff:ff:ff -``` - -We can ignore the loopback interface, and I can see my ethernet interface is called `enp117s0`. You might see -wireless interfaces here as well but they can also be ignored. - -@note{If your PC is only connected via Wi-Fi, it is still technically possible to get this working, but it is outside -the scope of this guide and requires more networking knowledge and a Wi-Fi chip that supports WoL. If this is your -first foray into linux, I'd recommend just getting a cable.} - -Now I can install ethtool and modify my interface to allow Wake on LAN. For your use, replace `enp117s0` with whatever -the name of your ethernet interface is from the command `ip link show` - -```bash -sudo apt update -sudo apt install ethtool -sudo ethtool -s enp117s0 wol g -``` - -##### SSH Client Setup -To start, we need to install an SSH client (which is different from the *server* in [Remote SSH Headless Setup](#remote-ssh-headless-setup)) -on our machine if this not already done. Open a terminal and enter the following commands. - -```bash -sudo apt update -sudo apt install openssh-client -``` - -Next we need to generate the keys we will use to connect to our SSH session. This is as simple as running the -following in a terminal: - -```bash -ssh-keygen -``` - -and simply pressing enter through the default options. This will place a file called `id_rsa` and `id_rsa.pub` -in the hidden folder `~/.ssh/`. This is the default key used when this user initiates an SSH session. - -Next, we'll copy that public key to the `~/.ssh/authorized_users` file. These are the public keys -allowed to access this machine over SSH, and will allow us to establish an SSH connection with this user -to the SSH server running on the localhost. - -```bash -cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys -``` - -@tip{If you also want any other machines (e.g. a laptop or Steam Deck) to connect to your machine remotely over ssh, -be sure to generate a pubkey on that machine and append it to the authorized_keys file like we did above.} - -###### SSH Server Modifications - -We'll want to make a few modification to the SSH server on the Sunshine host, both for convenience and security. - -Modify `/etc/ssh/sshd_config` with the following changes: - -@tabs{ - @tab{nano | ```bash - sudo nano /etc/ssh/sshd_config - ```} - @tab{vim | ```bash - sudo vi /etc/ssh/sshd_config - ```} -} - -Find the line with `PasswordAuthentication` and make sure it is set to `no` (removed the `#` if present. -Then find the line `PubkeyAuthentication` and make sure it is set to `yes` and remove the `#` from the beginning -if present. When you're done you should have these two lines in your config somewhere. - -``` -PubkeyAuthentication yes -PasswordAuthentication no -``` - -@tip{Using publickey encryption for SSH connections significantly increases your protection against brute force -attacks, and protects you against a rogue machine pretending to be your SSH server and stealing your password.} - -The next step is optional, but if you do not plan on connecting to your computer remotely via ssh and only have -installed SSH for the purposes of using Sunshine, it's a good idea to disable listening for remote SSH connections. -Do this by changing the following lines near the top of your ``sshd_config``: - -``` -#ListenAddress 0.0.0.0 -#ListenAddress :: -``` - -To the following: - -``` -ListenAddress 127.0.0.1 -ListenAddress ::1 -``` - -This will only allow SSH connections coming from your computer itself. - -@tip{on some distributions, the maintainers have added some files in ``/etc/ssh/sshd_config.d/`` which are pulled into -your ``sshd_config``. These modifications can conflict with what we've just done. If you have any files in -``/etc/ssh/sshd_config.d/``, make sure they do not include any of the changes we've just made or you will experience -problems. If they do, you can comment out those lines by placing a ``#`` at their beginning, or delete the files safely -if you don't plan to use SSH for anything other than Sunshine.} - -###### Quick Test and Accept Host Authenticity. - -Next, let's reboot the machine and try to connect! Accept any warnings about the unidentified host at this time, -you'll never see those appear again unless something changes with your setup. - -```bash -ssh $(whoami)@localhost -``` - -You should see a new login prompt for the machine you're already on, and when you type `exit` you should just see - -```bash -logout -Connection to localhost closed. -``` - -##### Run sunshine-setup on boot over SSH - -Thanks to [this comment from Gavin Haynes on Unix Stack exchange](https://unix.stackexchange.com/questions/669389/how-do-i-get-an-ssh-command-to-run-on-boot/669476#669476), -we can establish an SSH connection on boot to run the sunshine-setup script via a systemd service. - -###### Disable default Sunshine services - -These service files are sometimes overwritten when updating Sunshine with the .deb. -So we'll be making new ones and disabling the included service files for our purposes. - -``` -sudo sytstemctl disable sunshine -systemctl --user disable sunshine -``` - -@note{In order to disable the user service, you must be logged in to the graphical desktop environment and run the -command from a GUI terminal. You'll also likely need to approve a polkit request (a graphical popup that asks -for your password). Trying to disable the user service without being signed in to the graphical environment is a -recipe for pain, and is why ``sudo`` is not invoked on the second line in the command above.} - -###### Create the autossh-sunshine-start script - -@tabs{ - @tab{nano | ```bash - sudo nano /usr/local/bin/autossh-sunshine-start - ```} - @tab{vim | ```bash - sudo vi /usr/local/bin/autossh-sunshine-start - ```} -} - -Copy the below script to that location and replace `{USERNAME}` wherever it occurs with the username you created -the SSH public key for in the previous section. - -```bash -#!/bin/bash -ssh -i /home/{USERNAME}/.ssh/id_rsa {USERNAME}@localhost -"/home/{USERNAME}/scripts/sunshine.sh" -``` - -@attention{This script uses the location of the script in [Stream Launcher Script](#stream-launcher-script). -Please complete that section before continuing.} - -Once you've created the script, be sure to make it executable by running: - -```bash -sudo chmod +x /usr/local/bin/autossh-sunshine-start -``` - -###### Create the autossh systemd service file - -@tabs{ - @tab{nano | ```bash - sudo nano /etc/systemd/system/autossh-sunshine.service - ```} - @tab{vim | ```bash - sudo vi /etc/systemd/system/autossh-sunshine.service - ```} -} - -Copy and paste the below systemd file and save it to the location in the commands above. - -``` -[Unit] -Description=Start sunshine over an localhost SSH connection on boot -Requires=sshd.service -After=sshd.service - -[Service] -ExecStartPre=/bin/sleep 5 -ExecStart=/usr/local/bin/autossh-sunshine-start -Restart=on-failure -RestartSec=5s - -[Install] -WantedBy=multi-user.target -``` - -Make it executable, and enable the service when you're done. - -```bash -sudo chmod +x /etc/systemd/system/autossh-sunshine.service -sudo systemctl start autossh-sunshine -sudo systemctl enable autossh-sunshine -``` - -This point is a good time for a sanity check, so restart your PC and try to sign in to your desktop via Moonlight. -You should be able to access the login screen, enter your credentials, and control the user session. At this point -you'll notice the reason for the next section as your audio will be non-functional and you won't see any tray icon -for Sunshine. If you don't care about audio (and maybe a couple other bugs you might encounter from time to time due -to the permissions difference between an SSH session and the desktop session), you can consider yourself finished at -this point! - -@note{You might also notice some issues if you have multiple monitors setup (including the dummy plug), like the mouse -cursor not being on the right screen for you to login. We will address this in the last step of this guide. It requires -messing with some configs for SDDM.} - -##### Getting the audio working - -To get the audio (and tray icon, etc...) working we will create a systemd user service, that will start on a graphical -login, kill the autossh-sunshine system service, and start Sunshine just like the standard Sunshine service. -This service will also need to call the autossh-sunshine system service before it is stopped as the user service will -be killed when we log out of the graphical session, so we want to make sure we restart that SSH service so we don't -lose the ability to log back in if we need to. - -@tabs{ - @tab{nano | ```bash - sudo nano /usr/lib/systemd/user/sunshine-after-login.service - ```} - @tab{vim | ```bash - sudo vi /usr/lib/systemd/user/sunshine-after-login.service - ```} -} - -Once again, copy the below service file into your text editor at the location above. - -``` -[Unit] -Description=Start Sunshine with the permissions of the graphical desktop session -StartLimitIntervalSec=500 -StartLimitBurst=5 - -[Service] -# Avoid starting Sunshine before the desktop is fully initialized. -ExecStartPre=/usr/bin/pkill sunshine -ExecStartPre=/bin/sleep 5 -ExecStart=/usr/bin/sunshine -ExecStopPost=/usr/bin/systemctl start autossh-sunshine - -Restart=on-failure -RestartSec=5s - -[Install] -WantedBy=xdg-desktop-autostart.target -``` - -Make it executable, and enable it. - -```bash -sudo chmod +x /usr/lib/systemd/user/sunshine-after-login.service -systemctl --user enable sunshine-after-login -``` - -###### Polkit Rules for Sunshine User Service - -Since this is being run with the permissions of the graphical session, we need to make a polkit modification to allow -it to call the system service autossh-sunshine when this user service is stopped, without prompting us for a password. - -@tabs{ - @tab{nano | ```bash - sudo nano /etc/polkit-1/rules.d/sunshine.rules - ```} - @tab{vim | ```bash - sudo vi /etc/polkit-1/rules.d/sunshine.rules - ```} -} - -Once again, copy the below to the .rules file in your text editor. - -```js -polkit.addRule(function(action, subject) { -   if (action.id == "org.freedesktop.systemd1.manage-units" && -       action.lookup("unit") == "autossh-sunshine.service") -   { -       return polkit.Result.YES; -   } -}) -``` - -###### Modifications to sudoers.d files - -Lastly, we need to make a few modifications to the sudoers file for our users. Replace {USERNAME} below with your -username. You will be prompted to select either vi or nano for your editor if you've not used this command before, -choose whichever you prefer. - -``` -sudo visudo /etc/sudoers.d/{USERNAME} -``` - -@danger{NEVER modify a file in ``sudoers.d`` directly. Always use the ``visudo`` command. This command checks your changes -before saving the file, and if the resulting changes would break sudo on your system, it will prompt you to fix -them. Modifying the file with nano or vim directly does not give you this sanity check and introduces the -possibility of losing sudo access to your machine. Tread carefully, and make a backup.} - -As always, copy and paste the below into your user's `sudoers.d` configuration. Replace {USERNAME} with your username, -and {HOSTNAME} with the name of your computer. - -``` -{USERNAME} {HOSTNAME} = (root) NOPASSWD: /home/{USERNAME}/scripts/sunshine-setup.sh -{USERNAME} {HOSTNAME} = (root) NOPASSWD: /bin/sunshine -{USERNAME} {HOSTNAME} = (root) NOPASSWD: /usr/bin/systemctl start autossh-sunshine -{USERNAME} {HOSTNAME} = (root) NOPASSWD: /usr/bin/systemctl --user start sunshine-after-login -# The below is optional, but will allow us to send trigger a shutdown with a sunshine prep command, if desired. -{USERNAME} {HOSTNAME} = (root) NOPASSWD: /usr/sbin/shutdown -``` - -Once again, restart your computer and do a quick test. Make sure you can connect to the PC to login and enter your -credentials. You should be booted out of the system, and then can reconnect a few seconds later to the logged-in -desktop session. You should see a tray icon for Sunshine, and the sound should be working (or you may need to manually -select the sunshine-sink at least the first time). - -If you don't have multiple monitors, at this point you can consider yourself done! - -##### Configuring the login screen layout for multiple monitors - -This is not Sunshine specific, but is a frequent problem I had setting up Sunshine and thought it pertinent to add to -the guide. If you are using multiple monitors (even a single monitor with a dummy plug may have this problem) you -might notice the streamed login screen has one or more of the following problems: - -1. The text is way too small to see (caused by a too-high resolution) -2. The mouse cursor is off on some other screen (caused by not mirroring the displays) -3. There are multiple login screens overlapping each other (caused by differing resolutions, and trying to mirror -the display). - -###### Log in to an X11 Session - -This can be fixed, by modifying some scripts called by SDDM on boot. To start though, we need to make sure we're -logged into an x11 session, not Wayland or the terminal. As the Wayland session will give us incorrect information, -and the terminal will give us no information since no graphical environment exists. SDDM initially starts an x11 -session to display the login screen so we need to use xorg commands to change the display configuration. - -To do this, log out of your desktop session on the Sunshine host, and somewhere on the lower left of your screen -(depending on your SDDM theme) there will be some text that on Debian 12 KDE Plasma defaults to saying -`Session: Plasma (Wayland)`. Select this and choose `Plasma (X11)` from the drop down menu and sign in. - -###### Find your monitor identifiers. - -Open a terminal and run: - -```bash -xrandr | grep -w connected -``` - -This will require some more sleuthing on your part. Different PC hardware, and different monitors / connectors, -display the names differently. Some start at 0, some start 1. Some spell out "DisplayPort" some, say "DP". You will -need to modify all of the commands from here on out based on the output of the above command. I will use the output I -receive below as the example for the rest of this guide. - -```bash -DisplayPort-0 connected (normal left inverted right x axis y axis) -DisplayPort-1 connected (normal left inverted right x axis y axis) -DisplayPort-2 connected (normal left inverted right x axis y axis) -HDMI-A-0 connected primary 1920x1080+0+0 (normal left inverted right x axis y axis) 800mm x 450mm -``` - -@note{If I instead run this command on Wayland, I get the following useless output. Hence the need to sign in to an -x11 session. - -```bash -XWAYLAND0 connected 2592x1458+6031+0 (normal left inverted right x axis y axis) 600mm x 340mm -XWAYLAND1 connected 2592x1458+0+0 (normal left inverted right x axis y axis) 480mm x 270mm -XWAYLAND2 connected primary 3440x1440+2592+0 (normal left inverted right x axis y axis) 800mm x 330mm -XWAYLAND3 connected 2592x1458+0+0 (normal left inverted right x axis y axis) 1440mm x 810mm - -``` -} - - -From this, you can see that my monitors are named the following under an x11 session. - -DisplayPort-0 -DisplayPort-1 -DisplayPort-2 -HDMI-A-0 - -@tip{If you have a label maker, now would be a good time to unplug some cables, determine where they are on your -system, and label the outputs on your graphics card to ease changing your setup in the future.} - -In my setup, after moving some inputs I changed my system so that these cables correspond to the below monitors - -| Display Name | Monitor | -| ------------- | --------------------------- | -| DisplayPort-0 | rightmost 1080p display | -| DisplayPort-1 | leftmost 1080p display | -| DisplayPort-2 | middle 3440x1440 display | -| HDMI-A-0 | 4k Roku TV (and dummy plug) | - -###### Modify the SDDM startup script - -For my purposes, I would prefer to have the Roku TV (which doubles as my always-on dummy plug) to always display a -1080p screen on login (this can be changed automatically after login). And I would like to retain the ability to use -my leftmost monitor to login to my physical desktop, but I'd like to disable my primary and rightmost displays. - -To do this, we need to modify the SDDM startup script to shut off DisplayPort-1 and DisplayPort-2, set HDMI-A-0 to -1080p and mirror it with DisplayPort-1. - -@tabs{ - @tab{nano | ```bash - sudo nano /usr/share/sddm/scripts/Xsetup - ```} - @tab{vim | ```bash - sudo vi /usr/share/sddm/scripts/Xsetup - ```} -} - -Which will open a script that looks like this. We will not be removing these lines. - -```bash -#!/bin/sh -# Xsetup - run as root before the login dialog appears - -if [ -e /sbin/prime-offload ]; then -   echo running NVIDIA Prime setup /sbin/prime-offload -   /sbin/prime-offload -fi -``` - -At the bottom of this Xsetup script though, we can add some xrandr commands - -To shut a display off, we can use: `xrandr --output {DISPLAYNAME} --off`. - -To set a display as the primary and accept -it's automatic (usually the maximum, but not always especially on TVs where the default refresh rate may be lower) -resolution and refresh rate we can use: `xrandr --output {DISPLAYNAME} --auto --primary`. - -To set a display to a specific resolution we can use: `xrandr --output {DISPLAYNAME} --mode {PIXELWIDTH}x{PIXELLENGTH}`. - -And lastly, to mirror a display we can use: `xrandr --output {DISPLAYNAME} --same-as {ANOTHER-DISPLAY}` - -So with my desire to mirror my TV and left displays, my Xsetup script now looks like this: - -```bash -#!/bin/sh -# Xsetup - run as root before the login dialog appears - -if [ -e /sbin/prime-offload ]; then -   echo running NVIDIA Prime setup /sbin/prime-offload -   /sbin/prime-offload -fi - -xrandr --output DisplayPort-0 --off -xrandr --output DisplayPort-2 --off -xrandr --output DisplayPort-1 --auto --primary -xrandr --output HDMI-A-0 --mode 1920x1080 -xrandr --output HDMI-A-0 --same-as DisplayPort-1 -``` - -Save this file, reboot, and you should see your login screen now respects these settings. Make sure when you log -back in, you select a Wayland session (if that is your preferred session manager). - -#### Next Steps - -Congratulations! You now have Sunshine starting on boot, you can login to your session remotely, you get all the -benefits of the graphical session permissions, and you can safely shut down your PC with the confidence you can -turn it back on when needed. - -@seealso{As Eric Dong recommended, I'll also send you to automate changing your displays. -You can add multiple commands, to turn off, or configure as many displays as you'd like with the sunshine prep -commands. See [Changing Resolution and Refresh Rate](md_docs_2app__examples#changing-resolution-and-refresh-rate) -for more information and remember that the display names for your prep commands, may be different than what you -found for SDDM.} - - -## macOS -@todo{It's looking lonely here.} - - -## Windows - -| Author | [BeeLeDev](https://github.com/BeeLeDev) | -|------------|-----------------------------------------| -| Difficulty | Intermediate | - -### Discord call cancellation -Cancel Discord call audio with Voicemeeter (Standard) - -#### Voicemeeter Configuration -1. Click "Hardware Out" -2. Set the physical device you receive audio to as your Hardware Out with MME -3. Turn on BUS A for the Virtual Input - -#### Windows Configuration -1. Open the sound settings -2. Set your default Playback as Voicemeeter Input - -@tip{Run audio in the background to find the device that your Virtual Input is using -(Voicemeeter In #), you will see the bar to the right of the device have green bars -going up and down. This device will be referred to as Voicemeeter Input.} - -#### Discord Configuration -1. Open the settings -2. Go to Voice & Video -3. Set your Output Device as the physical device you receive audio to - -@tip{It is usually the same device you set for Hardware Out in Voicemeeter.} - -#### Sunshine Configuration -1. Go to Configuration -2. Go to the Audio/Video tab -3. Set Virtual Sink as Voicemeeter Input - -@note{This should be the device you set as default previously in Playback.} +@admonition{Community | A collection of guides written by the community is available on our +[blog](https://app.lizardbyte.dev/blog). +Feel free to contribute your own tips and trips by making a PR to +[LizardByte.github.io](https://github.com/LizardByte/LizardByte.github.io).}
-| Previous | Next | -|:--------------------------------|--------------------------------------------:| -| [App Examples](app_examples.md) | [Performance Tuning](performance_tuning.md) | +| Previous | Next | +|:----------------------------------------|--------------------------------------------:| +| [Awesome-Sunshine](awesome_sunshine.md) | [Performance Tuning](performance_tuning.md) |
diff --git a/docs/images/discord_calls_01.png b/docs/images/discord_calls_01.png deleted file mode 100644 index d26e62a811e..00000000000 Binary files a/docs/images/discord_calls_01.png and /dev/null differ diff --git a/docs/images/discord_calls_02.png b/docs/images/discord_calls_02.png deleted file mode 100644 index 6a739be788b..00000000000 Binary files a/docs/images/discord_calls_02.png and /dev/null differ diff --git a/docs/images/discord_calls_03.png b/docs/images/discord_calls_03.png deleted file mode 100644 index 0dd34500233..00000000000 Binary files a/docs/images/discord_calls_03.png and /dev/null differ diff --git a/docs/images/discord_calls_04.png b/docs/images/discord_calls_04.png deleted file mode 100644 index ec38513e25d..00000000000 Binary files a/docs/images/discord_calls_04.png and /dev/null differ diff --git a/docs/images/discord_calls_05.png b/docs/images/discord_calls_05.png deleted file mode 100644 index efb4e2beeab..00000000000 Binary files a/docs/images/discord_calls_05.png and /dev/null differ diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 7260ec6cd0f..8bd22eede26 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -23,6 +23,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.} +### Unusual Mouse Behavior +If you experience unusual mouse behavior, try attaching a physical mouse to the Sunshine host. + ### Web UI Access Can't access the web UI? @@ -115,6 +118,16 @@ system. You may also want to enable decoders, however that is not required for S ``` } +### 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. + +If the input is still not working, you may need to add your user to the `input` group. + +```bash +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.} @@ -190,6 +203,9 @@ has. You may get permission denied errors when attempting to launch a game or ap You will need to modify the security permissions on your disk. Ensure that user/principal SYSTEM has full permissions on the disk. +### Stuttering +If you experience stuttering using NVIDIA, try disabling `vsync:fast` in the NVIDIA Control Panel. +
| Previous | Next | diff --git a/gh-pages-template/.readthedocs.yaml b/gh-pages-template/.readthedocs.yaml new file mode 100644 index 00000000000..681f205b85e --- /dev/null +++ b/gh-pages-template/.readthedocs.yaml @@ -0,0 +1,25 @@ +--- +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +version: 2 + +build: + os: ubuntu-24.04 + tools: + ruby: "3.3" + apt_packages: + - 7zip + - jq + jobs: + install: + - | + mkdir -p "./tmp" + branch="master" + base_url="https://raw.githubusercontent.com/LizardByte/LizardByte.github.io" + url="${base_url}/refs/heads/${branch}/scripts/readthedocs_build.sh" + curl -sSL -o "./tmp/readthedocs_build.sh" "${url}" + chmod +x "./tmp/readthedocs_build.sh" + build: + html: + - "./tmp/readthedocs_build.sh" diff --git a/gh-pages-template/_config.yml b/gh-pages-template/_config.yml new file mode 100644 index 00000000000..17e327744f5 --- /dev/null +++ b/gh-pages-template/_config.yml @@ -0,0 +1,4 @@ +--- +# See https://github.com/daattali/beautiful-jekyll/blob/master/_config.yml for documented options + +avatar: "/Sunshine/assets/img/navbar-avatar.png" diff --git a/gh-pages-template/assets/images/AdobeStock_231616343.jpeg b/gh-pages-template/assets/img/banners/AdobeStock_231616343.jpeg similarity index 100% rename from gh-pages-template/assets/images/AdobeStock_231616343.jpeg rename to gh-pages-template/assets/img/banners/AdobeStock_231616343.jpeg diff --git a/gh-pages-template/assets/images/AdobeStock_231616343_1920x1280.jpg b/gh-pages-template/assets/img/banners/AdobeStock_231616343_1920x1280.jpg similarity index 100% rename from gh-pages-template/assets/images/AdobeStock_231616343_1920x1280.jpg rename to gh-pages-template/assets/img/banners/AdobeStock_231616343_1920x1280.jpg diff --git a/gh-pages-template/assets/images/AdobeStock_303330124.jpeg b/gh-pages-template/assets/img/banners/AdobeStock_303330124.jpeg similarity index 100% rename from gh-pages-template/assets/images/AdobeStock_303330124.jpeg rename to gh-pages-template/assets/img/banners/AdobeStock_303330124.jpeg diff --git a/gh-pages-template/assets/images/AdobeStock_303330124_1920x1280.jpg b/gh-pages-template/assets/img/banners/AdobeStock_303330124_1920x1280.jpg similarity index 100% rename from gh-pages-template/assets/images/AdobeStock_303330124_1920x1280.jpg rename to gh-pages-template/assets/img/banners/AdobeStock_303330124_1920x1280.jpg diff --git a/gh-pages-template/assets/images/AdobeStock_305732536.jpeg b/gh-pages-template/assets/img/banners/AdobeStock_305732536.jpeg similarity index 100% rename from gh-pages-template/assets/images/AdobeStock_305732536.jpeg rename to gh-pages-template/assets/img/banners/AdobeStock_305732536.jpeg diff --git a/gh-pages-template/assets/images/AdobeStock_305732536_1920x1280.jpg b/gh-pages-template/assets/img/banners/AdobeStock_305732536_1920x1280.jpg similarity index 100% rename from gh-pages-template/assets/images/AdobeStock_305732536_1920x1280.jpg rename to gh-pages-template/assets/img/banners/AdobeStock_305732536_1920x1280.jpg diff --git a/gh-pages-template/assets/img/navbar-avatar.png b/gh-pages-template/assets/img/navbar-avatar.png new file mode 100644 index 00000000000..0a44d94a4e3 Binary files /dev/null and b/gh-pages-template/assets/img/navbar-avatar.png differ diff --git a/gh-pages-template/index.html b/gh-pages-template/index.html index ad8cbbc0f70..9b0c2f5ec0b 100644 --- a/gh-pages-template/index.html +++ b/gh-pages-template/index.html @@ -1,805 +1,634 @@ - - - - LizardByte - Sunshine - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - -
-
-
- - -
-
-

- Sunshine is a self-hosted game stream host for Moonlight. Offering low latency, cloud gaming - server capabilities with support for AMD, Intel, and Nvidia GPUs for hardware encoding. Software - encoding is also available. You can connect to Sunshine from any Moonlight client on a variety - of devices. A web UI is provided to allow configuration, and client pairing, from your favorite - web browser. Pair from the local server or any mobile device. -

-
- - -
-
-

Features

- -
-
-
-
-
-
- -
-
-
Self-hosted
-

- Run Sunshine on your own hardware. No need to pay monthly fees to a - cloud gaming provider. -

-
-
-
+
+
+
+
+
+
+ Moonlight
-
-
-
-
-
-
- -
-
-
Moonlight Support
-

- Connect to Sunshine from any Moonlight client. Moonlight is available - for Windows, macOS, Linux, Android, iOS, Xbox, and more. See - clients for more information. -

-
-
-
+
+
Moonlight Support
+

+ Connect to Sunshine from any Moonlight client. Moonlight is available + for Windows, macOS, Linux, Android, iOS, Xbox, and more. See + clients for more information. +

-
-
-
-
-
- -
-
-
Hardware Encoding
-

- Sunshine supports AMD, Intel, and Nvidia GPUs for hardware encoding. - Software encoding is also available. -

-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
- -
-
-
Low Latency
-

- Sunshine is designed to provide the lowest latency possible to achieve optimal gaming performance. -

-
-
-
+
+
Hardware Encoding
+

+ Sunshine supports AMD, Intel, and Nvidia GPUs for hardware encoding. + Software encoding is also available. +

-
-
-
-
-
- -
-
-
Control
-

- Sunshine emulates an Xbox, PlayStation, or Nintendo Switch controller. - Use nearly any controller on your Moonlight client!
- -

    -
  • Nintendo Switch emulation is only available on Linux.
  • -
  • Gamepad emulation is not currently supported on macOS.
  • -
- -

-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
- -
-
-
Configurable
-

- Sunshine offers many configuration options to customize your experience. -

-
-
-
+
+
Low Latency
+

+ Sunshine is designed to provide the lowest latency possible to achieve optimal gaming performance. +

-
- - -
-
-

Clients

- -
- - -
-
-
-
-
- -
- -
- Official -
-
-
- +
+
+
+
+
+
+
-
- - -
-
-
-
-
- -
- -
- Official -
-
-
- +
+
Control
+

+ Sunshine emulates an Xbox, PlayStation, or Nintendo Switch controller. + Use nearly any controller on your Moonlight client!
+ +

    +
  • Nintendo Switch emulation is only available on Linux.
  • +
  • Gamepad emulation is not currently supported on macOS.
  • +
+ +

- - -
-
-
-
-
- -
-
-
- - iOS - -
-
-
- Official -
-
-
- +
+
+
+
+
+
+
+
+
-
- - -
-
-
-
-
- - - - -
-
-
- - QT - -
-
-
- Official -
-
-
- +
+
Configurable
+

+ Sunshine offers many configuration options to customize your experience. +

- - -
-
-
-
-
- -
- -
- Official -
-
-
- +
+
+
+
+
+
+ + +
+
+

Clients

+ +
+ + +
+
+
+
+
+ Android +
+ +
+ Official
+
+ +
+
- -
-
-
-
-
- -
- -
- Community -
-
-
- + +
+
+
+
+
+ Chrome Web Store +
+ +
+ Official
+
+ +
+
- -
-
-
-
-
- -
- -
- Community -
-
-
- + +
+
+
+
+
+ iOS + Apple TV +
+
+
+ + iOS + +
+
+
+ Official
+
+ +
+
- -
-
-
-
-
- -
- -
- Community -
-
-
- + +
+
+
+
+
+ Linux + macOS + Windows + Steam
+
+
+ + QT + +
+
+
+ Official +
+
+
+ +
+
- -
-
-
-
-
- -
- -
- Community -
-
-
- + +
+
+
+
+
+ Raspberry Pi +
+ +
+ Official
+
+ +
+
- -
-
-
-
-
- -
- -
- Community -
-
-
- + +
+
+
+
+
+ Xbox +
+ +
+ Community
- - +
+
-
+
- -
-
- -
-
-
-
- -
-

Documentation

-

- Read the documentation to learn how to install, use, and configure Sunshine. -

-
-
-
-
+
+ +
+
- -
-
-
-
- -
-

Download

-

- Download Sunshine for your platform. -

-
-
-
-
+
+ - - + - -
-
-
-
-
-
Support Center
-
Find answers and ask questions.
-
-
-

- The one who knows all the answers has not been asked all the questions. - – Confucius. -

-
- - - -
- - - - - - - - - - - - - - - - - - + + + + + +
+
+ +
+
+
+
+ +
+

Documentation

+

+ Read the documentation to learn how to install, use, and configure Sunshine. +

+
+
+
+ +
+
+ + +
+
+
+
+ +
+

Download

+

+ Download Sunshine for your platform. +

+
+
+
+ +
+
+
+
+ + + diff --git a/package.json b/package.json index 78d6994c195..f504e49c86c 100644 --- a/package.json +++ b/package.json @@ -8,14 +8,15 @@ "serve": "serve ./tests/fixtures/http --no-port-switching" }, "dependencies": { - "@lizardbyte/shared-web": "2024.921.191855", - "vue": "3.5.12", - "vue-i18n": "9.14.0" + "@lizardbyte/shared-web": "2025.326.11214", + "vue": "3.5.13", + "vue-i18n": "11.1.3" }, "devDependencies": { + "@codecov/vite-plugin": "1.9.0", "@vitejs/plugin-vue": "4.6.2", "serve": "14.2.3", - "vite": "4.5.2", + "vite": "4.5.9", "vite-plugin-ejs": "1.6.4" } } diff --git a/packaging/linux/fedora/Sunshine.spec b/packaging/linux/fedora/Sunshine.spec index c19ddf78c90..3becff3be1a 100644 --- a/packaging/linux/fedora/Sunshine.spec +++ b/packaging/linux/fedora/Sunshine.spec @@ -17,8 +17,6 @@ Source0: tarball.tar.gz # BuildRequires: boost-devel >= 1.86.0 BuildRequires: cmake >= 3.25.0 -BuildRequires: gcc -BuildRequires: gcc-c++ BuildRequires: libayatana-appindicator3-devel BuildRequires: libcap-devel BuildRequires: libcurl-devel @@ -37,6 +35,7 @@ BuildRequires: libXrandr-devel BuildRequires: libXtst-devel BuildRequires: git BuildRequires: mesa-libGL-devel +BuildRequires: mesa-libgbm-devel BuildRequires: miniupnpc-devel BuildRequires: npm BuildRequires: numactl-devel @@ -54,11 +53,22 @@ BuildRequires: which BuildRequires: xorg-x11-server-Xvfb # Conditional BuildRequires for cuda-gcc based on Fedora version -%if 0%{?fedora} >= 40 -# this package conflicts with gcc on f39 -BuildRequires: cuda-gcc-c++ +%if 0%{?fedora} >= 40 && 0%{?fedora} <= 41 +BuildRequires: gcc13 +BuildRequires: gcc13-c++ +%global gcc_version 13 +%global cuda_version 12.6.3 +%global cuda_build 560.35.05 +%elif %{?fedora} >= 42 +BuildRequires: gcc14 +BuildRequires: gcc14-c++ +%global gcc_version 14 +%global cuda_version 12.8.1 +%global cuda_build 570.124.06 %endif +%global cuda_dir %{_builddir}/cuda + Requires: libcap >= 2.22 Requires: libcurl >= 7.0 Requires: libdrm > 2.4.97 @@ -88,22 +98,14 @@ ls -a %{_builddir}/Sunshine %autopatch -p1 %build +# exit on error +set -e + # Detect the architecture and Fedora version architecture=$(uname -m) -fedora_version=%{fedora} cuda_supported_architectures=("x86_64" "aarch64") -# set cuda_version based on Fedora version -# these are the same right now, but leave this structure to make it easier to set different versions -if [ "$fedora_version" == 39 ]; then - cuda_version="12.6.2" - cuda_build="560.35.03" -else - cuda_version="12.6.2" - cuda_build="560.35.03" -fi - # prepare CMAKE args cmake_args=( "-B=%{_builddir}/Sunshine/build" @@ -123,27 +125,23 @@ cmake_args=( "-DSUNSHINE_PUBLISHER_ISSUE_URL=https://app.lizardbyte.dev/support" ) +export CC=gcc-%{gcc_version} +export CXX=g++-%{gcc_version} + function install_cuda() { # check if we need to install cuda - if [ -f "%{_builddir}/cuda/bin/nvcc" ]; then + if [ -f "%{cuda_dir}/bin/nvcc" ]; then echo "cuda already installed" return fi - if [ "$fedora_version" -ge 40 ]; then - # update environment variables for CUDA, necessary when using cuda-gcc-c++ - export NVCC_PREPEND_FLAGS='-ccbin /usr/bin/cuda' - export PATH=/usr/bin/cuda:"%{_builddir}/cuda/bin:${PATH}" - export LD_LIBRARY_PATH="%{_builddir}/cuda/lib64:${LD_LIBRARY_PATH}" - fi - local cuda_prefix="https://developer.download.nvidia.com/compute/cuda/" local cuda_suffix="" if [ "$architecture" == "aarch64" ]; then local cuda_suffix="_sbsa" fi - local url="${cuda_prefix}${cuda_version}/local_installers/cuda_${cuda_version}_${cuda_build}_linux${cuda_suffix}.run" + local url="${cuda_prefix}%{cuda_version}/local_installers/cuda_%{cuda_version}_%{cuda_build}_linux${cuda_suffix}.run" echo "cuda url: ${url}" wget \ "$url" \ @@ -159,21 +157,31 @@ function install_cuda() { --override \ --silent \ --toolkit \ - --toolkitpath="%{_builddir}/cuda" + --toolkitpath="%{cuda_dir}" rm "%{_builddir}/cuda.run" -} -# we need to clear these flags to avoid linkage errors with cuda-gcc-c++ -export CFLAGS="" -export CXXFLAGS="" -export FFLAGS="" -export FCFLAGS="" -export LDFLAGS="" + # 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 + echo "Original math_functions.h:" + find "%{cuda_dir}" -name math_functions.h -exec cat {} \; + + # Apply the patch + patch -p2 \ + --backup \ + --directory="%{cuda_dir}" \ + --verbose \ + < "%{_builddir}/Sunshine/packaging/linux/fedora/patches/f42/${architecture}/01-math_functions.patch" + fi +} -if [ -n "$cuda_version" ] && [[ " ${cuda_supported_architectures[@]} " =~ " ${architecture} " ]]; then +if [ -n "%{cuda_version}" ] && [[ " ${cuda_supported_architectures[@]} " =~ " ${architecture} " ]]; then install_cuda cmake_args+=("-DSUNSHINE_ENABLE_CUDA=ON") - cmake_args+=("-DCMAKE_CUDA_COMPILER:PATH=%{_builddir}/cuda/bin/nvcc") + cmake_args+=("-DCMAKE_CUDA_COMPILER:PATH=%{cuda_dir}/bin/nvcc") + cmake_args+=("-DCMAKE_CUDA_HOST_COMPILER=gcc-%{gcc_version}") +else + cmake_args+=("-DSUNSHINE_ENABLE_CUDA=OFF") fi # setup the version diff --git a/packaging/linux/fedora/patches/f42/aarch64/01-math_functions.patch b/packaging/linux/fedora/patches/f42/aarch64/01-math_functions.patch new file mode 100644 index 00000000000..322fef1c90d --- /dev/null +++ b/packaging/linux/fedora/patches/f42/aarch64/01-math_functions.patch @@ -0,0 +1,39 @@ +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 +@@ -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 diff --git a/packaging/linux/fedora/patches/f42/x86_64/01-math_functions.patch b/packaging/linux/fedora/patches/f42/x86_64/01-math_functions.patch new file mode 100644 index 00000000000..5e522eb141a --- /dev/null +++ b/packaging/linux/fedora/patches/f42/x86_64/01-math_functions.patch @@ -0,0 +1,39 @@ +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 +@@ -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 diff --git a/packaging/linux/flatpak/README.md b/packaging/linux/flatpak/README.md index 9b358b79ec3..684f986e65e 100644 --- a/packaging/linux/flatpak/README.md +++ b/packaging/linux/flatpak/README.md @@ -1,13 +1,22 @@ -# Overview +
+ +

Sunshine

+

Self-hosted game stream host for Moonlight.

+
-[![Flathub installs](https://img.shields.io/flathub/downloads/dev.lizardbyte.app.Sunshine?style=for-the-badge&logo=flathub)](https://flathub.org/apps/dev.lizardbyte.app.Sunshine) -[![Flathub Version](https://img.shields.io/flathub/v/dev.lizardbyte.app.Sunshine?style=for-the-badge&logo=flathub)](https://flathub.org/apps/dev.lizardbyte.app.Sunshine) +
+ Flathub installs + Flathub Version +
-LizardByte has the full documentation hosted on [Read the Docs](https://sunshinestream.readthedocs.io). - -## About +## ℹ️ About Sunshine is a self-hosted game stream host for Moonlight. +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/) + This repo is synced from the upstream [Sunshine](https://github.com/LizardByte/Sunshine) repo. Please report issues and contribute to the upstream repo. diff --git a/packaging/linux/flatpak/deps/flatpak-builder-tools b/packaging/linux/flatpak/deps/flatpak-builder-tools index a1eb29c5f30..bf91cb0bee7 160000 --- a/packaging/linux/flatpak/deps/flatpak-builder-tools +++ b/packaging/linux/flatpak/deps/flatpak-builder-tools @@ -1 +1 @@ -Subproject commit a1eb29c5f3038413ffafd4fea34e62c361c109ad +Subproject commit bf91cb0bee7ce0c8021e223e3ea9c5110ebb82de diff --git a/packaging/linux/flatpak/deps/shared-modules b/packaging/linux/flatpak/deps/shared-modules index 0529b121864..1f8e591b263 160000 --- a/packaging/linux/flatpak/deps/shared-modules +++ b/packaging/linux/flatpak/deps/shared-modules @@ -1 +1 @@ -Subproject commit 0529b121864669aa14fac1c67b5684a4bc6542b8 +Subproject commit 1f8e591b263eef8a0dc04929f2da135af59fac3c diff --git a/packaging/linux/flatpak/dev.lizardbyte.app.Sunshine.metainfo.xml b/packaging/linux/flatpak/dev.lizardbyte.app.Sunshine.metainfo.xml index 43c4cbdfe32..fe5f9d764b3 100644 --- a/packaging/linux/flatpak/dev.lizardbyte.app.Sunshine.metainfo.xml +++ b/packaging/linux/flatpak/dev.lizardbyte.app.Sunshine.metainfo.xml @@ -29,20 +29,24 @@

NOTE: Sunshine requires additional installation steps.

-

flatpak run --command=additional-install.sh @PROJECT_FQDN@

+

+ flatpak run --command=additional-install.sh @PROJECT_FQDN@ +

NOTE: Sunshine uses a self-signed certificate. The web browser will report it as not secure, but it is safe.

NOTE: KMS Grab (Optional)

-

sudo -i PULSE_SERVER=unix:/run/user/$(id -u $whoami)/pulse/native flatpak run @PROJECT_FQDN@

+

+ sudo -i PULSE_SERVER=unix:/run/user/$(id -u $whoami)/pulse/native flatpak run @PROJECT_FQDN@ +

- + LizardByte - https://app.lizardbyte.dev/Sunshine/assets/images/AdobeStock_305732536_1920x1280.jpg + https://app.lizardbyte.dev/Sunshine/assets/img/banners/AdobeStock_305732536_1920x1280.jpg Sunshine diff --git a/packaging/linux/flatpak/dev.lizardbyte.app.Sunshine.yml b/packaging/linux/flatpak/dev.lizardbyte.app.Sunshine.yml index bead37b890c..303f2e7eec3 100644 --- a/packaging/linux/flatpak/dev.lizardbyte.app.Sunshine.yml +++ b/packaging/linux/flatpak/dev.lizardbyte.app.Sunshine.yml @@ -32,6 +32,9 @@ modules: # Test dependencies - "modules/xvfb/xvfb.json" + # Build dependencies + - "modules/nlohmann_json.json" + # Runtime dependencies - shared-modules/libayatana-appindicator/libayatana-appindicator-gtk3.json - "modules/avahi.json" diff --git a/packaging/linux/flatpak/exceptions.json b/packaging/linux/flatpak/exceptions.json new file mode 100644 index 00000000000..957f73849e5 --- /dev/null +++ b/packaging/linux/flatpak/exceptions.json @@ -0,0 +1,8 @@ +{ + "dev.lizardbyte.app.Sunshine": [ + "appstream-missing-screenshots", + "appstream-screenshots-not-mirrored-in-ostree", + "external-gitmodule-url-found", + "finish-args-flatpak-spawn-access" + ] +} diff --git a/packaging/linux/flatpak/flatpak-lint-baseline_manifest.json b/packaging/linux/flatpak/flatpak-lint-baseline_manifest.json deleted file mode 100644 index f1ff672259e..00000000000 --- a/packaging/linux/flatpak/flatpak-lint-baseline_manifest.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "errors": [ - "finish-args-flatpak-spawn-access" - ], - "info": [ - "finish-args-flatpak-spawn-access: finish-args has a talk-name access for org.freedesktop.Flatpak" - ], - "message": "Please consult the documentation at https://docs.flathub.org/docs/for-app-authors/linter" -} diff --git a/packaging/linux/flatpak/flatpak-lint-baseline_repo.json b/packaging/linux/flatpak/flatpak-lint-baseline_repo.json deleted file mode 100644 index 92d24feb051..00000000000 --- a/packaging/linux/flatpak/flatpak-lint-baseline_repo.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "errors": [ - "appstream-missing-screenshots", - "finish-args-flatpak-spawn-access" - ], - "info": [ - "appstream-missing-screenshots: Catalogue file has no screenshots. Please check if screenshot URLs are reachable", - "finish-args-flatpak-spawn-access: finish-args has a talk-name access for org.freedesktop.Flatpak" - ], - "message": "Please consult the documentation at https://docs.flathub.org/docs/for-app-authors/linter" -} diff --git a/packaging/linux/flatpak/modules/boost.json b/packaging/linux/flatpak/modules/boost.json index da111f64816..79eadba48f8 100644 --- a/packaging/linux/flatpak/modules/boost.json +++ b/packaging/linux/flatpak/modules/boost.json @@ -9,8 +9,8 @@ "sources": [ { "type": "archive", - "url": "https://github.com/boostorg/boost/releases/download/boost-1.86.0/boost-1.86.0-cmake.tar.xz", - "sha256": "2c5ec5edcdff47ff55e27ed9560b0a0b94b07bd07ed9928b476150e16b0efc57" + "url": "https://github.com/boostorg/boost/releases/download/boost-1.87.0/boost-1.87.0-cmake.tar.xz", + "sha256": "7da75f171837577a52bbf217e17f8ea576c7c246e4594d617bfde7fafd408be5" } ] } diff --git a/packaging/linux/flatpak/modules/nlohmann_json.json b/packaging/linux/flatpak/modules/nlohmann_json.json new file mode 100644 index 00000000000..158f8b5bc87 --- /dev/null +++ b/packaging/linux/flatpak/modules/nlohmann_json.json @@ -0,0 +1,15 @@ +{ + "name": "nlohmann_json", + "buildsystem": "cmake-ninja", + "config-opts": [ + "-DJSON_MultipleHeaders=OFF", + "-DJSON_BuildTests=OFF" + ], + "sources": [ + { + "type": "archive", + "url": "https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz", + "sha256": "d6c65aca6b1ed68e7a182f4757257b107ae403032760ed6ef121c9d55e81757d" + } + ] +} diff --git a/packaging/linux/flatpak/modules/xvfb/xvfb.json b/packaging/linux/flatpak/modules/xvfb/xvfb.json index 5c3232ec258..d4b056c4c4a 100644 --- a/packaging/linux/flatpak/modules/xvfb/xvfb.json +++ b/packaging/linux/flatpak/modules/xvfb/xvfb.json @@ -11,14 +11,15 @@ ], "sources": [ { - "type": "archive", - "url": "https://gitlab.freedesktop.org/xorg/xserver/-/archive/xorg-server-21.1.13/xserver-xorg-server-21.1.13.tar.bz2", - "sha256": "ee2bf6d65f4b111ce86ca817c3327dc1e70d9c958aa16876f2820caf7bf7cffa", + "type": "git", + "url": "https://github.com/LizardByte-infrastructure/xserver.git", + "tag": "xorg-server-21.1.13", + "commit": "be2767845d6ed3c6dbd25a151051294d0908a995", "x-checker-data": { "type": "anitya", "project-id": 5250, "stable-only": true, - "url-template": "https://gitlab.freedesktop.org/xorg/xserver/-/archive/xorg-server-$version/xserver-xorg-server-$version.tar.bz2" + "tag-template": "xorg-server-$version" } }, { @@ -32,14 +33,15 @@ "buildsystem": "meson", "sources": [ { - "type": "archive", - "url": "https://gitlab.freedesktop.org/xorg/lib/libxcvt/-/archive/libxcvt-0.1.2/libxcvt-libxcvt-0.1.2.tar.bz2", - "sha256": "590e5a6da87ace7aa7857026b207a2c4d378620035441e20ea97efedd15d6d4a", + "type": "git", + "url": "https://github.com/LizardByte-infrastructure/libxcvt.git", + "tag": "libxcvt-0.1.2", + "commit": "d9ca87eea9eecddaccc3a77227bcb3acf84e89df", "x-checker-data": { "type": "anitya", "project-id": 235147, "stable-only": true, - "url-template": "https://gitlab.freedesktop.org/xorg/lib/libxcvt/-/archive/libxcvt-$version/libxcvt-libxcvt-$version.tar.bz2" + "tag-template": "libxcvt-$version" } } ] @@ -48,65 +50,68 @@ "name": "libXmu", "sources": [ { - "type": "archive", - "url": "https://xorg.freedesktop.org/archive/individual/lib/libXmu-1.2.1.tar.gz", - "sha256": "bf0902583dd1123856c11e0a5085bd3c6e9886fbbd44954464975fd7d52eb599", + "type": "git", + "url": "https://github.com/LizardByte-infrastructure/libxmu.git", + "tag": "libXmu-1.2.1", + "commit": "792f80402ee06ce69bca3a8f2a84295999c3a170", "x-checker-data": { "type": "anitya", "project-id": 1785, "stable-only": true, - "url-template": "https://xorg.freedesktop.org/archive/individual/lib/libXmu-$version.tar.gz" + "tag-template": "libXmu-$version" } } ] }, { - "name": "libfontenc", + "name": "font-util", "sources": [ { - "type": "archive", - "url": "https://xorg.freedesktop.org/archive/individual/lib/libfontenc-1.1.8.tar.xz", - "sha256": "7b02c3d405236e0d86806b1de9d6868fe60c313628b38350b032914aa4fd14c6", + "type": "git", + "url": "https://github.com/LizardByte-infrastructure/font-util.git", + "tag": "font-util-1.4.1", + "commit": "b5ca142f81a6f14eddb23be050291d1c25514777", "x-checker-data": { "type": "anitya", - "project-id": 1613, + "project-id": 15055, "stable-only": true, - "url-template": "https://xorg.freedesktop.org/archive/individual/lib/libfontenc-$version.tar.xz" + "tag-template": "font-util-$version" } } ] }, { - "name": "libtirpc", - "config-opts": [ - "--disable-gssapi" - ], + "name": "libfontenc", "sources": [ { - "type": "archive", - "url": "https://downloads.sourceforge.net/sourceforge/libtirpc/libtirpc-1.3.4.tar.bz2", - "sha256": "1e0b0c7231c5fa122e06c0609a76723664d068b0dba3b8219b63e6340b347860", + "type": "git", + "url": "https://github.com/LizardByte-infrastructure/libfontenc.git", + "tag": "libfontenc-1.1.8", + "commit": "92a85fda2acb4e14ec0b2f6d8fe3eaf2b687218c", "x-checker-data": { "type": "anitya", - "project-id": 1740, + "project-id": 1613, "stable-only": true, - "url-template": "https://downloads.sourceforge.net/sourceforge/libtirpc/libtirpc-$version.tar.bz2" + "tag-template": "libfontenc-$version" } } ] }, { - "name": "font-util", + "name": "libtirpc", + "config-opts": [ + "--disable-gssapi" + ], "sources": [ { "type": "archive", - "url": "https://xorg.freedesktop.org/archive/individual/font/font-util-1.4.1.tar.gz", - "sha256": "f029ae80cdd75d89bee7f7af61c21e07982adfb9f72344a158b99f91f77ef5ed", + "url": "https://downloads.sourceforge.net/sourceforge/libtirpc/libtirpc-1.3.4.tar.bz2", + "sha256": "1e0b0c7231c5fa122e06c0609a76723664d068b0dba3b8219b63e6340b347860", "x-checker-data": { "type": "anitya", - "project-id": 15055, + "project-id": 1740, "stable-only": true, - "url-template": "https://xorg.freedesktop.org/archive/individual/font/font-util-$version.tar.gz" + "url-template": "https://downloads.sourceforge.net/sourceforge/libtirpc/libtirpc-$version.tar.bz2" } } ] @@ -115,14 +120,15 @@ "name": "xvfb-libXfont2", "sources": [ { - "type": "archive", - "url": "https://xorg.freedesktop.org/archive/individual/lib/libXfont2-2.0.6.tar.gz", - "sha256": "a944df7b6837c8fa2067f6a5fc25d89b0acc4011cd0bc085106a03557fb502fc", + "type": "git", + "url": "https://github.com/LizardByte-infrastructure/libxfont.git", + "tag": "libXfont2-2.0.6", + "commit": "d54aaf2483df6a1f98fadc09004157e657b7f73e", "x-checker-data": { "type": "anitya", "project-id": 17165, "stable-only": true, - "url-template": "https://xorg.freedesktop.org/archive/individual/lib/libXfont2-$version.tar.gz" + "tag-template": "libXfont2-$version" } } ] @@ -131,14 +137,15 @@ "name": "xvfb-xauth", "sources": [ { - "type": "archive", - "url": "https://gitlab.freedesktop.org/xorg/app/xauth/-/archive/xauth-1.1.1/xauth-xauth-1.1.3.tar.bz2", - "sha256": "3cee16ebe9de0e85c62513f6d6353710407c8ebb1f855b18d03807c27d38a215", + "type": "git", + "url": "https://github.com/LizardByte-infrastructure/xauth.git", + "tag": "xauth-1.1.3", + "commit": "c29eef23683f0e3575a3c60d9314de8156fbe2c2", "x-checker-data": { "type": "anitya", "project-id": 5253, "stable-only": true, - "url-template": "https://gitlab.freedesktop.org/xorg/app/xauth/-/archive/xauth-1.1.1/xauth-xauth-$version.tar.bz2" + "tag-template": "xauth-$version" } } ] diff --git a/packaging/linux/flatpak/scripts/remove-additional-install.sh b/packaging/linux/flatpak/scripts/remove-additional-install.sh index 0d13baeb62c..27d7af03f88 100644 --- a/packaging/linux/flatpak/scripts/remove-additional-install.sh +++ b/packaging/linux/flatpak/scripts/remove-additional-install.sh @@ -8,4 +8,4 @@ echo Sunshine User Service has been removed. # Udev rule flatpak-spawn --host pkexec sh -c "rm /etc/udev/rules.d/60-sunshine.rules" -echo Mouse permission removed. Restart computer to take effect. +echo Input rules removed. Restart computer to take effect. diff --git a/packaging/linux/flatpak/scripts/sunshine.sh b/packaging/linux/flatpak/scripts/sunshine.sh new file mode 100644 index 00000000000..6fb51457f84 --- /dev/null +++ b/packaging/linux/flatpak/scripts/sunshine.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +PORT=47990 + +if ! curl -k https://localhost:$PORT > /dev/null 2>&1; then + (sleep 3 && xdg-open https://localhost:$PORT) & + exec sunshine "$@" +else + echo "Sunshine is already running, opening the web interface..." + xdg-open https://localhost:$PORT +fi diff --git a/packaging/linux/flatpak/sunshine.desktop b/packaging/linux/flatpak/sunshine.desktop index f1753c9cdc3..eca745ef478 100644 --- a/packaging/linux/flatpak/sunshine.desktop +++ b/packaging/linux/flatpak/sunshine.desktop @@ -1,20 +1,9 @@ [Desktop Entry] -Type=Application -Name=@PROJECT_NAME@ -Exec=@PROJECT_FQDN@ -Version=1.0 +Categories=AudioVideo;Network;RemoteAccess; Comment=@PROJECT_DESCRIPTION@ +Exec=sunshine.sh Icon=@SUNSHINE_DESKTOP_ICON@ Keywords=gamestream;stream;moonlight;remote play; -Categories=AudioVideo;Network;RemoteAccess; -Actions=RunInTerminal;KMS; - -[Desktop Action RunInTerminal] -Name=Run in Terminal -Icon=application-x-executable -Exec=gio launch @CMAKE_INSTALL_FULL_DATAROOTDIR@/applications/@PROJECT_FQDN@_terminal.desktop - -[Desktop Action KMS] -Name=Run in Terminal (KMS) -Icon=application-x-executable -Exec=gio launch @CMAKE_INSTALL_FULL_DATAROOTDIR@/applications/@PROJECT_FQDN@_kms.desktop +Name=@PROJECT_NAME@ +Type=Application +Version=1.0 diff --git a/packaging/linux/flatpak/sunshine_kms.desktop b/packaging/linux/flatpak/sunshine_kms.desktop deleted file mode 100644 index 59d35d7168c..00000000000 --- a/packaging/linux/flatpak/sunshine_kms.desktop +++ /dev/null @@ -1,6 +0,0 @@ -[Desktop Entry] -Name=@PROJECT_NAME@ (KMS) -Exec=sudo -i PULSE_SERVER=unix:$(pactl info | awk '/Server String/{print$3}') flatpak run @PROJECT_FQDN@ -Terminal=true -Type=Application -NoDisplay=true diff --git a/packaging/macos/Portfile b/packaging/macos/Portfile deleted file mode 100644 index 678f742ac9b..00000000000 --- a/packaging/macos/Portfile +++ /dev/null @@ -1,77 +0,0 @@ -# -*- coding: utf-8; mode: tcl; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- vim:fenc=utf-8:ft=tcl:et:sw=4:ts=4:sts=4 - -# initial PR into macports: https://github.com/macports/macports-ports/pull/15143 - -PortSystem 1.0 -PortGroup cmake 1.1 -PortGroup github 1.0 - -name @PROJECT_NAME@ -version @PROJECT_VERSION@ -revision 0 -categories multimedia emulators games -platforms darwin -license GPL-3 -maintainers @LizardByte -description @PROJECT_DESCRIPTION@ - -# long_description will not be split into multiple lines as it's configured by CMakeLists -long_description @PROJECT_LONG_DESCRIPTION@ -homepage @PROJECT_HOMEPAGE_URL@ -master_sites https://github.com/lizardbyte/sunshine/releases - -compiler.cxx_standard 2017 -fetch.type git - -git.url @GITHUB_CLONE_URL@ -git.branch @GITHUB_COMMIT@ - -post-fetch { - system -W ${worksrcpath} "${git.cmd} submodule update --init --recursive" -} - -# https://guide.macports.org/chunked/reference.dependencies.html -depends_build-append port:doxygen \ - port:graphviz \ - port:npm9 \ - port:pkgconfig - -depends_lib port:curl \ - port:libopus \ - port:miniupnpc - -configure.args -DBOOST_USE_STATIC=ON \ - -DBUILD_WERROR=ON \ - -DCMAKE_INSTALL_PREFIX=${prefix} \ - -DSUNSHINE_ASSETS_DIR=etc/sunshine/assets \ - -DSUNSHINE_PUBLISHER_NAME='LizardByte' \ - -DSUNSHINE_PUBLISHER_WEBSITE='https://app.lizardbyte.dev' \ - -DSUNSHINE_PUBLISHER_ISSUE_URL='https://app.lizardbyte.dev/support' - -configure.env-append BRANCH=@GITHUB_BRANCH@ -configure.env-append BUILD_VERSION=@BUILD_VERSION@ -configure.env-append COMMIT=@GITHUB_COMMIT@ - -startupitem.create yes -startupitem.executable "${prefix}/bin/sunshine" -startupitem.location LaunchDaemons -startupitem.name ${name} -startupitem.netchange yes - -platform darwin { - if { ${os.major} < 20 } { - # See: https://github.com/LizardByte/Sunshine/discussions/117#discussioncomment-2513494 - notes-append "Port is limited to software encoding, when used with macOS releases prior to Big Sur." - } -} - -notes-append "Run @PROJECT_NAME@ by executing 'sunshine ', e.g. 'sunshine ~/sunshine.conf' " -notes-append "The config file will be created if it doesn't exist." -notes-append "It is recommended to set a location for the apps file in the config." -notes-append "See our documentation at 'https://docs.lizardbyte.dev/projects/sunshine/en/v@PROJECT_VERSION@/' for further info." - -test.run yes -test.dir ${build.dir}/tests -test.target "" -test.cmd ./test_sunshine -test.args --gtest_color=yes --gtest_filter=-*HIDTest.*:-*DeathTest.* diff --git a/packaging/sunshine.rb b/packaging/sunshine.rb index 72d0e9852f9..f13a5a3856e 100644 --- a/packaging/sunshine.rb +++ b/packaging/sunshine.rb @@ -29,16 +29,34 @@ 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 "pkg-config" => :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 "libcap" depends_on "libdrm" @@ -52,10 +70,133 @@ class @PROJECT_NAME@ < Formula depends_on "libxinerama" depends_on "libxrandr" depends_on "libxtst" + depends_on "mesa" depends_on "numactl" depends_on "pulseaudio" depends_on "systemd" depends_on "wayland" + + # 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 + + resource "libayatana-indicator" do + url "https://github.com/AyatanaIndicators/libayatana-indicator/archive/refs/tags/0.9.4.tar.gz" + sha256 "a18d3c682e29afd77db24366f8475b26bda22b0e16ff569a2ec71cd6eb4eac95" + end end def install @@ -65,12 +206,12 @@ def install args = %W[ -DBUILD_WERROR=ON + -DCMAKE_CXX_STANDARD=20 -DCMAKE_INSTALL_PREFIX=#{prefix} -DHOMEBREW_ALLOW_FETCHCONTENT=ON -DOPENSSL_ROOT_DIR=#{Formula["openssl"].opt_prefix} -DSUNSHINE_ASSETS_DIR=sunshine/assets -DSUNSHINE_BUILD_HOMEBREW=ON - -DSUNSHINE_ENABLE_TRAY=OFF -DSUNSHINE_PUBLISHER_NAME='LizardByte' -DSUNSHINE_PUBLISHER_WEBSITE='https://app.lizardbyte.dev' -DSUNSHINE_PUBLISHER_ISSUE_URL='https://app.lizardbyte.dev/support' @@ -105,16 +246,69 @@ 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", *std_cmake_args, *args + # Handle system tray on Linux + if OS.linux? + # Build and install libayatana components - cd "build" do - system "make" - system "make", "install" + # Build libdbusmenu + resource("libdbusmenu").stage do + system "./configure", + "--prefix=#{prefix}", + "--with-gtk=3", + "--disable-dumper", + "--disable-static", + "--disable-tests", + "--disable-gtk-doc", + "--enable-introspection=no", + "--disable-vala" + system "make", "install" + end + + # Build ayatana-ido + resource("ayatana-ido").stage do + system "cmake", "-S", ".", "-B", "build", "-G", "Ninja", + "-DCMAKE_INSTALL_PREFIX=#{prefix}", + "-DENABLE_INTROSPECTION=OFF", + *std_cmake_args + system "ninja", "-C", "build" + system "ninja", "-C", "build", "install" + end - bin.install "tests/test_sunshine" + # Build libayatana-indicator + resource("libayatana-indicator").stage do + ENV.append_path "PKG_CONFIG_PATH", "#{lib}/pkgconfig" + ENV.append "LDFLAGS", "-L#{lib}" + + system "cmake", "-S", ".", "-B", "build", "-G", "Ninja", + "-DCMAKE_INSTALL_PREFIX=#{prefix}", + *std_cmake_args + system "ninja", "-C", "build" + system "ninja", "-C", "build", "install" + end + + # Build libayatana-appindicator + resource("libayatana-appindicator").stage do + system "cmake", "-S", ".", "-B", "build", "-G", "Ninja", + "-DCMAKE_INSTALL_PREFIX=#{prefix}", + "-DENABLE_BINDINGS_MONO=OFF", + "-DENABLE_BINDINGS_VALA=OFF", + "-DENABLE_GTKDOC=OFF", + *std_cmake_args + system "ninja", "-C", "build" + system "ninja", "-C", "build", "install" + end end + 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" + # codesign the binary on intel macs system "codesign", "-s", "-", "--force", "--deep", bin/"sunshine" if OS.mac? && Hardware::CPU.intel? @@ -130,7 +324,7 @@ def caveats Thanks for installing @PROJECT_NAME@! To get started, review the documentation at: - https://docs.lizardbyte.dev/projects/sunshine/en/latest/ + https://docs.lizardbyte.dev/projects/sunshine EOS if OS.linux? @@ -157,6 +351,7 @@ def caveats system bin/"sunshine", "--version" # run the test suite - system bin/"test_sunshine", "--gtest_color=yes" + system bin/"test_sunshine", "--gtest_color=yes", "--gtest_output=xml:test_results.xml" + assert_path_exists testpath/"test_results.xml" end end diff --git a/scripts/_locale.py b/scripts/_locale.py index d035414917d..84ae3385929 100644 --- a/scripts/_locale.py +++ b/scripts/_locale.py @@ -24,6 +24,7 @@ # target locales target_locales = [ + 'bg', # Bulgarian 'de', # German 'en', # English 'en_GB', # English (United Kingdom) @@ -32,9 +33,14 @@ 'fr', # French 'it', # Italian 'ja', # Japanese + 'ko', # Korean + 'pl', # Polish 'pt', # Portuguese + 'pt_BR', # Portuguese (Brazil) 'ru', # Russian 'sv', # Swedish + 'tr', # Turkish + 'uk', # Ukrainian 'zh', # Chinese ] diff --git a/scripts/linux_build.sh b/scripts/linux_build.sh old mode 100644 new mode 100755 index 4cf4c101ad5..1ac5f162650 --- a/scripts/linux_build.sh +++ b/scripts/linux_build.sh @@ -3,6 +3,7 @@ set -e # Default value for arguments appimage_build=0 +num_processors=$(nproc) publisher_name="Third Party Publisher" publisher_website="" publisher_issue_url="https://app.lizardbyte.dev/support" @@ -27,6 +28,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. + --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. --publisher-issue-url The URL of the publisher's support site or issue tracker. @@ -53,6 +55,9 @@ while getopts ":hs-:" opt; do appimage_build=1 skip_libva=1 ;; + num-processors=*) + num_processors="${OPTARG#*=}" + ;; publisher-name=*) publisher_name="${OPTARG#*=}" ;; @@ -85,7 +90,53 @@ shift $((OPTIND -1)) # dependencies array to build out dependencies=() -function add_debain_based_deps() { +function add_arch_deps() { + dependencies+=( + 'avahi' + 'base-devel' + 'cmake' + 'curl' + "gcc${gcc_version}" + "gcc${gcc_version}-libs" + 'git' + 'libayatana-appindicator' + 'libcap' + 'libdrm' + 'libevdev' + 'libmfx' + 'libnotify' + 'libpulse' + 'libva' + 'libx11' + 'libxcb' + 'libxfixes' + 'libxrandr' + 'libxtst' + 'miniupnpc' + 'ninja' + 'nodejs' + 'npm' + 'numactl' + 'openssl' + 'opus' + 'udev' + 'wayland' + ) + + if [ "$skip_libva" == 0 ]; then + dependencies+=( + "libva" # VA-API + ) + fi + + if [ "$skip_cuda" == 0 ]; then + dependencies+=( + "cuda" # VA-API + ) + fi +} + +function add_debian_based_deps() { dependencies+=( "bison" # required if we need to compile doxygen "build-essential" @@ -100,6 +151,7 @@ function add_debain_based_deps() { "libcurl4-openssl-dev" "libdrm-dev" # KMS "libevdev-dev" + "libgbm-dev" "libminiupnpc-dev" "libnotify-dev" "libnuma-dev" @@ -128,8 +180,8 @@ function add_debain_based_deps() { fi } -function add_debain_deps() { - add_debain_based_deps +function add_debian_deps() { + add_debian_based_deps dependencies+=( "libayatana-appindicator3-dev" ) @@ -141,7 +193,7 @@ function add_ubuntu_deps() { ${sudo_cmd} add-apt-repository ppa:ubuntu-toolchain-r/test -y fi - add_debain_based_deps + add_debian_based_deps dependencies+=( "libappindicator3-dev" ) @@ -151,8 +203,8 @@ function add_fedora_deps() { dependencies+=( "cmake" "doxygen" - "gcc" - "g++" + "gcc${gcc_version}" + "gcc${gcc_version}-c++" "git" "graphviz" "libappindicator-gtk3-devel" @@ -170,6 +222,7 @@ function add_fedora_deps() { "libXrandr-devel" # X11 "libXtst-devel" # X11 "mesa-libGL-devel" + "mesa-libgbm-devel" "miniupnpc-devel" "ninja-build" "npm" @@ -191,9 +244,15 @@ 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" + return + fi # check if we need to install cuda if [ -f "${build_dir}/cuda/bin/nvcc" ]; then - echo "cuda already installed" + nvcc_path="${build_dir}/cuda/bin/nvcc" + echo "found local cuda" return fi @@ -230,11 +289,13 @@ function install_cuda() { 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 rm "${build_dir}/cuda.run" + nvcc_path="${build_dir}/cuda/bin/nvcc" } function check_version() { local package_name=$1 local min_version=$2 + local max_version=$3 local installed_version echo "Checking if $package_name is installed and at least version $min_version" @@ -243,6 +304,8 @@ function check_version() { installed_version=$(dpkg -s "$package_name" 2>/dev/null | grep '^Version:' | awk '{print $2}') elif [ "$distro" == "fedora" ]; then installed_version=$(rpm -q --queryformat '%{VERSION}' "$package_name" 2>/dev/null) + elif [ "$distro" == "arch" ]; then + installed_version=$(pacman -Q "$package_name" | awk '{print $2}' ) else echo "Unsupported Distro" return 1 @@ -253,11 +316,12 @@ function check_version() { return 1 fi - if [ "$(printf '%s\n' "$installed_version" "$min_version" | sort -V | head -n1)" = "$min_version" ]; then - echo "$package_name version $installed_version is at least $min_version" +if [[ "$(printf '%s\n' "$installed_version" "$min_version" | sort -V | head -n1)" = "$min_version" ]] && \ + [[ "$(printf '%s\n' "$installed_version" "$max_version" | sort -V | head -n1)" = "$installed_version" ]]; then + echo "Installed version is within range" return 0 else - echo "$package_name version $installed_version is less than $min_version" + echo "$package_name version $installed_version is out of range" return 1 fi } @@ -296,13 +360,15 @@ function run_install() { # Update the package list $package_update_command - if [ "$distro" == "debian" ]; then - add_debain_deps + if [ "$distro" == "arch" ]; then + add_arch_deps + elif [ "$distro" == "debian" ]; then + add_debian_deps elif [ "$distro" == "ubuntu" ]; then add_ubuntu_deps elif [ "$distro" == "fedora" ]; then add_fedora_deps - ${sudo_cmd} dnf group install "Development Tools" -y + ${sudo_cmd} dnf group install "$dev_tools_group" -y fi # Install the dependencies @@ -320,8 +386,11 @@ function run_install() { "gcc-ranlib" ) - # update alternatives for gcc and g++ if a debian based distro - if [ "$distro" == "debian" ] || [ "$distro" == "ubuntu" ]; then + #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 @@ -340,7 +409,7 @@ function run_install() { # 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"; then + if ! check_version "cmake" "$cmake_min" "inf"; then cmake_prefix="https://github.com/Kitware/CMake/releases/download/v" if [ "$architecture" == "x86_64" ]; then cmake_arch="x86_64" @@ -358,7 +427,8 @@ function run_install() { # compile doxygen if version is too low doxygen_min="1.10.0" _doxygen_min="1_10_0" - if ! check_version "doxygen" "$doxygen_min"; then + doxygen_max="1.12.0" + if ! check_version "doxygen" "$doxygen_min" "$doxygen_max"; then if [ "${SUNSHINE_COMPILE_DOXYGEN}" == "true" ]; then echo "Compiling doxygen" doxygen_url="https://github.com/doxygen/doxygen/releases/download/Release_${_doxygen_min}/doxygen-${doxygen_min}.src.tar.gz" @@ -367,10 +437,10 @@ function run_install() { tar -xzf "${build_dir}/doxygen.tar.gz" cd "doxygen-${doxygen_min}" cmake -DCMAKE_BUILD_TYPE=Release -G="Ninja" -B="build" -S="." - ninja -C "build" + ninja -C "build" -j"${num_processors}" ninja -C "build" install else - echo "Doxygen version too low, skipping docs" + echo "Doxygen version not in range, skipping docs" cmake_args+=("-DBUILD_DOCS=OFF") fi fi @@ -386,10 +456,10 @@ function run_install() { fi # run the cuda install - if [ -n "$cuda_version" ] && [ "$skip_cuda" == 0 ]; then + if [ "$skip_cuda" == 0 ]; then install_cuda cmake_args+=("-DSUNSHINE_ENABLE_CUDA=ON") - cmake_args+=("-DCMAKE_CUDA_COMPILER:PATH=${build_dir}/cuda/bin/nvcc") + cmake_args+=("-DCMAKE_CUDA_COMPILER:PATH=$nvcc_path") fi # Cmake stuff here @@ -429,7 +499,15 @@ function run_install() { # Determine the OS and call the appropriate function cat /etc/os-release -if grep -q "Debian GNU/Linux 12 (bookworm)" /etc/os-release; then + +if grep -q "Arch Linux" /etc/os-release; then + distro="arch" + version="" + package_update_command="${sudo_cmd} pacman -Syu --noconfirm" + package_install_command="${sudo_cmd} pacman -Sy --needed" + nvm_node=0 + gcc_version="14" +elif grep -q "Debian GNU/Linux 12 (bookworm)" /etc/os-release; then distro="debian" version="12" package_update_command="${sudo_cmd} apt-get update" @@ -438,24 +516,36 @@ if grep -q "Debian GNU/Linux 12 (bookworm)" /etc/os-release; then cuda_build="525.60.13" gcc_version="12" nvm_node=0 -elif grep -q "PLATFORM_ID=\"platform:f39\"" /etc/os-release; then +elif grep -q "PLATFORM_ID=\"platform:f40\"" /etc/os-release; then distro="fedora" - version="39" + version="40" package_update_command="${sudo_cmd} dnf update -y" package_install_command="${sudo_cmd} dnf install -y" - cuda_version="12.4.0" - cuda_build="550.54.14" + cuda_version=12.6.3 + cuda_build=560.35.05 gcc_version="13" nvm_node=0 -elif grep -q "PLATFORM_ID=\"platform:f40\"" /etc/os-release; then + dev_tools_group="Development Tools" +elif grep -q "PLATFORM_ID=\"platform:f41\"" /etc/os-release; then distro="fedora" - version="40" + version="41" package_update_command="${sudo_cmd} dnf update -y" package_install_command="${sudo_cmd} dnf install -y" - cuda_version= - cuda_build= + cuda_version=12.6.3 + cuda_build=560.35.05 gcc_version="13" nvm_node=0 + dev_tools_group="development-tools" +elif grep -q "PLATFORM_ID=\"platform:f42\"" /etc/os-release; then + distro="fedora" + 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 + gcc_version="14" + nvm_node=0 + dev_tools_group="development-tools" elif grep -q "Ubuntu 22.04" /etc/os-release; then distro="ubuntu" version="22.04" diff --git a/scripts/requirements.txt b/scripts/requirements.txt index cd2ef869b17..b2c5001402d 100644 --- a/scripts/requirements.txt +++ b/scripts/requirements.txt @@ -1,2 +1,2 @@ -Babel==2.16.0 -clang-format +Babel==2.17.0 +clang-format==20.* diff --git a/scripts/update_clang_format.py b/scripts/update_clang_format.py index 9e0dacda847..7ae027faaf8 100644 --- a/scripts/update_clang_format.py +++ b/scripts/update_clang_format.py @@ -7,12 +7,12 @@ 'src', 'tests', 'tools', - os.path.join('third-party', 'glad'), - os.path.join('third-party', 'nvfbc'), ] file_types = [ 'cpp', + 'cu', 'h', + 'hpp', 'm', 'mm' ] diff --git a/src/audio.cpp b/src/audio.cpp index b24ae61350f..0d287071a25 100644 --- a/src/audio.cpp +++ b/src/audio.cpp @@ -2,16 +2,18 @@ * @file src/audio.cpp * @brief Definitions for audio capture and encoding. */ +// standard includes #include +// lib includes #include -#include "platform/common.h" - +// local includes #include "audio.h" #include "config.h" #include "globals.h" #include "logging.h" +#include "platform/common.h" #include "thread_safe.h" #include "utility.h" @@ -20,25 +22,11 @@ namespace audio { using opus_t = util::safe_ptr; using sample_queue_t = std::shared_ptr>>; - struct audio_ctx_t { - // We want to change the sink for the first stream only - std::unique_ptr sink_flag; - - std::unique_ptr control; - - bool restore_sink; - platf::sink_t sink; - }; - - static int - start_audio_control(audio_ctx_t &ctx); - static void - stop_audio_control(audio_ctx_t &); - static void - apply_surround_params(opus_stream_config_t &stream, const stream_params_t ¶ms); + static int start_audio_control(audio_ctx_t &ctx); + static void stop_audio_control(audio_ctx_t &); + static void apply_surround_params(opus_stream_config_t &stream, const stream_params_t ¶ms); - int - map_stream(int channels, bool quality); + int map_stream(int channels, bool quality); constexpr auto SAMPLE_RATE = 48000; @@ -95,10 +83,7 @@ namespace audio { }, }; - auto control_shared = safe::make_shared(start_audio_control, stop_audio_control); - - void - encodeThread(sample_queue_t samples, config_t config, void *channel_data) { + void encodeThread(sample_queue_t samples, config_t config, void *channel_data) { auto packets = mail::man->queue(mail::audio_packets); auto stream = stream_configs[map_stream(config.channels, config.flags[config_t::HIGH_QUALITY])]; if (config.flags[config_t::CUSTOM_SURROUND_PARAMS]) { @@ -108,14 +93,15 @@ namespace audio { // Encoding takes place on this thread platf::adjust_thread_priority(platf::thread_priority_e::high); - opus_t opus { opus_multistream_encoder_create( + opus_t opus {opus_multistream_encoder_create( stream.sampleRate, stream.channelCount, stream.streams, stream.coupledStreams, stream.mapping, OPUS_APPLICATION_RESTRICTED_LOWDELAY, - nullptr) }; + nullptr + )}; opus_multistream_encoder_ctl(opus.get(), OPUS_SET_BITRATE(stream.bitrate)); opus_multistream_encoder_ctl(opus.get(), OPUS_SET_VBR(0)); @@ -126,7 +112,7 @@ namespace audio { auto frame_size = config.packetDuration * stream.sampleRate / 1000; while (auto sample = samples->pop()) { - buffer_t packet { 1400 }; + buffer_t packet {1400}; int bytes = opus_multistream_encode_float(opus.get(), sample->data(), frame_size, std::begin(packet), packet.size()); if (bytes < 0) { @@ -141,15 +127,18 @@ namespace audio { } } - void - capture(safe::mail_t mail, config_t config, void *channel_data) { + void capture(safe::mail_t mail, config_t config, void *channel_data) { auto shutdown_event = mail->event(mail::shutdown); + if (!config::audio.stream) { + shutdown_event->view(); + return; + } auto stream = stream_configs[map_stream(config.channels, config.flags[config_t::HIGH_QUALITY])]; if (config.flags[config_t::CUSTOM_SURROUND_PARAMS]) { apply_surround_params(stream, config.customStreamParams); } - auto ref = control_shared.ref(); + auto ref = get_audio_ctx_ref(); if (!ref) { return; } @@ -216,7 +205,7 @@ namespace audio { platf::adjust_thread_priority(platf::thread_priority_e::critical); auto samples = std::make_shared(30); - std::thread thread { encodeThread, samples, config, channel_data }; + std::thread thread {encodeThread, samples, config, channel_data}; auto fg = util::fail_guard([&]() { samples->stop(); @@ -255,8 +244,25 @@ namespace audio { } } - int - map_stream(int channels, bool quality) { + audio_ctx_ref_t get_audio_ctx_ref() { + static auto control_shared {safe::make_shared(start_audio_control, stop_audio_control)}; + return control_shared.ref(); + } + + bool is_audio_ctx_sink_available(const audio_ctx_t &ctx) { + if (!ctx.control) { + return false; + } + + const std::string &sink = ctx.sink.host.empty() ? config::audio.sink : ctx.sink.host; + if (sink.empty()) { + return false; + } + + return ctx.control->is_sink_available(sink); + } + + int map_stream(int channels, bool quality) { int shift = quality ? 1 : 0; switch (channels) { case 2: @@ -269,8 +275,7 @@ namespace audio { return STEREO; } - int - start_audio_control(audio_ctx_t &ctx) { + int start_audio_control(audio_ctx_t &ctx) { auto fg = util::fail_guard([]() { BOOST_LOG(warning) << "There will be no audio"sv; }); @@ -297,8 +302,7 @@ namespace audio { return 0; } - void - stop_audio_control(audio_ctx_t &ctx) { + void stop_audio_control(audio_ctx_t &ctx) { // restore audio-sink if applicable if (!ctx.restore_sink) { return; @@ -312,8 +316,7 @@ namespace audio { } } - void - apply_surround_params(opus_stream_config_t &stream, const stream_params_t ¶ms) { + void apply_surround_params(opus_stream_config_t &stream, const stream_params_t ¶ms) { stream.channelCount = params.channelCount; stream.streams = params.streams; stream.coupledStreams = params.coupledStreams; diff --git a/src/audio.h b/src/audio.h index 208a5775871..2afb42e5f70 100644 --- a/src/audio.h +++ b/src/audio.h @@ -4,6 +4,8 @@ */ #pragma once +// local includes +#include "platform/common.h" #include "thread_safe.h" #include "utility.h" @@ -55,8 +57,47 @@ namespace audio { std::bitset flags; }; + struct audio_ctx_t { + // We want to change the sink for the first stream only + std::unique_ptr sink_flag; + + std::unique_ptr control; + + bool restore_sink; + platf::sink_t sink; + }; + using buffer_t = util::buffer_t; using packet_t = std::pair; - void - capture(safe::mail_t mail, config_t config, void *channel_data); + using audio_ctx_ref_t = safe::shared_t::ptr_t; + + void capture(safe::mail_t mail, config_t config, void *channel_data); + + /** + * @brief Get the reference to the audio context. + * @returns A shared pointer reference to audio context. + * @note Aside from the configuration purposes, it can be used to extend the + * audio sink lifetime to capture sink earlier and restore it later. + * + * @examples + * audio_ctx_ref_t audio = get_audio_ctx_ref() + * @examples_end + */ + audio_ctx_ref_t get_audio_ctx_ref(); + + /** + * @brief Check if the audio sink held by audio context is available. + * @returns True if available (and can probably be restored), false otherwise. + * @note Useful for delaying the release of audio context shared pointer (which + * tries to restore original sink). + * + * @examples + * audio_ctx_ref_t audio = get_audio_ctx_ref() + * if (audio.get()) { + * return is_audio_ctx_sink_available(*audio.get()); + * } + * return false; + * @examples_end + */ + bool is_audio_ctx_sink_available(const audio_ctx_t &ctx); } // namespace audio diff --git a/src/cbs.cpp b/src/cbs.cpp index 0b795a55f53..67cda0c1f49 100644 --- a/src/cbs.cpp +++ b/src/cbs.cpp @@ -3,6 +3,7 @@ * @brief Definitions for FFmpeg Coded Bitstream API. */ extern "C" { +// lib includes #include #include #include @@ -10,14 +11,15 @@ extern "C" { #include } +// local includes #include "cbs.h" #include "logging.h" #include "utility.h" using namespace std::literals; + namespace cbs { - void - close(CodedBitstreamContext *c) { + void close(CodedBitstreamContext *c) { ff_cbs_close(&c); } @@ -36,8 +38,7 @@ namespace cbs { std::fill_n((std::uint8_t *) this, sizeof(*this), 0); } - frag_t & - operator=(frag_t &&o) { + frag_t &operator=(frag_t &&o) { std::copy((std::uint8_t *) &o, (std::uint8_t *) (&o + 1), (std::uint8_t *) this); o.data = nullptr; @@ -53,12 +54,11 @@ namespace cbs { } }; - util::buffer_t - write(cbs::ctx_t &cbs_ctx, std::uint8_t nal, void *uh, AVCodecID codec_id) { + util::buffer_t write(cbs::ctx_t &cbs_ctx, std::uint8_t nal, void *uh, AVCodecID codec_id) { cbs::frag_t frag; auto err = ff_cbs_insert_unit_content(&frag, -1, nal, uh, nullptr); if (err < 0) { - char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 }; + char err_str[AV_ERROR_MAX_STRING_SIZE] {0}; BOOST_LOG(error) << "Could not insert NAL unit SPS: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err); return {}; @@ -66,29 +66,27 @@ namespace cbs { err = ff_cbs_write_fragment_data(cbs_ctx.get(), &frag); if (err < 0) { - char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 }; + char err_str[AV_ERROR_MAX_STRING_SIZE] {0}; BOOST_LOG(error) << "Could not write fragment data: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err); return {}; } // frag.data_size * 8 - frag.data_bit_padding == bits in fragment - util::buffer_t data { frag.data_size }; + util::buffer_t data {frag.data_size}; std::copy_n(frag.data, frag.data_size, std::begin(data)); return data; } - util::buffer_t - write(std::uint8_t nal, void *uh, AVCodecID codec_id) { + util::buffer_t write(std::uint8_t nal, void *uh, AVCodecID codec_id) { cbs::ctx_t cbs_ctx; ff_cbs_init(&cbs_ctx, codec_id, nullptr); return write(cbs_ctx, nal, uh, codec_id); } - h264_t - make_sps_h264(const AVCodecContext *avctx, const AVPacket *packet) { + h264_t make_sps_h264(const AVCodecContext *avctx, const AVPacket *packet) { cbs::ctx_t ctx; if (ff_cbs_init(&ctx, AV_CODEC_ID_H264, nullptr)) { return {}; @@ -98,7 +96,7 @@ namespace cbs { int err = ff_cbs_read_packet(ctx.get(), &frag, packet); if (err < 0) { - char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 }; + char err_str[AV_ERROR_MAX_STRING_SIZE] {0}; BOOST_LOG(error) << "Couldn't read packet: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err); return {}; @@ -144,8 +142,7 @@ namespace cbs { }; } - hevc_t - make_sps_hevc(const AVCodecContext *avctx, const AVPacket *packet) { + hevc_t make_sps_hevc(const AVCodecContext *avctx, const AVPacket *packet) { cbs::ctx_t ctx; if (ff_cbs_init(&ctx, AV_CODEC_ID_H265, nullptr)) { return {}; @@ -155,7 +152,7 @@ namespace cbs { int err = ff_cbs_read_packet(ctx.get(), &frag, packet); if (err < 0) { - char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 }; + char err_str[AV_ERROR_MAX_STRING_SIZE] {0}; BOOST_LOG(error) << "Couldn't read packet: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err); return {}; @@ -222,8 +219,7 @@ namespace cbs { * It then checks if the SPS->VUI (Video Usability Information) is present in the active SPS of the packet. * This is done for both H264 and H265 codecs. */ - bool - validate_sps(const AVPacket *packet, int codec_id) { + bool validate_sps(const AVPacket *packet, int codec_id) { cbs::ctx_t ctx; if (ff_cbs_init(&ctx, (AVCodecID) codec_id, nullptr)) { return false; @@ -233,7 +229,7 @@ namespace cbs { int err = ff_cbs_read_packet(ctx.get(), &frag, packet); if (err < 0) { - char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 }; + char err_str[AV_ERROR_MAX_STRING_SIZE] {0}; BOOST_LOG(error) << "Couldn't read packet: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err); return false; diff --git a/src/cbs.h b/src/cbs.h index a22a756b7ed..5dfddab5d77 100644 --- a/src/cbs.h +++ b/src/cbs.h @@ -4,6 +4,7 @@ */ #pragma once +// local includes #include "utility.h" struct AVPacket; @@ -25,10 +26,8 @@ namespace cbs { nal_t sps; }; - hevc_t - make_sps_hevc(const AVCodecContext *ctx, const AVPacket *packet); - h264_t - make_sps_h264(const AVCodecContext *ctx, const AVPacket *packet); + hevc_t make_sps_hevc(const AVCodecContext *ctx, const AVPacket *packet); + h264_t make_sps_h264(const AVCodecContext *ctx, const AVPacket *packet); /** * @brief Validates the Sequence Parameter Set (SPS) of a given packet. @@ -36,6 +35,5 @@ namespace cbs { * @param codec_id The ID of the codec used (either AV_CODEC_ID_H264 or AV_CODEC_ID_H265). * @return True if the SPS->VUI is present in the active SPS of the packet, false otherwise. */ - bool - validate_sps(const AVPacket *packet, int codec_id); + bool validate_sps(const AVPacket *packet, int codec_id); } // namespace cbs diff --git a/src/config.cpp b/src/config.cpp index 77531cc06b7..7147fd6cfdb 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -2,6 +2,7 @@ * @file src/config.cpp * @brief Definitions for the configuration of Sunshine. */ +// standard includes #include #include #include @@ -11,21 +12,22 @@ #include #include +// lib includes #include #include #include #include +// local includes #include "config.h" #include "entry_handler.h" #include "file_handler.h" #include "logging.h" #include "nvhttp.h" +#include "platform/common.h" #include "rtsp.h" #include "utility.h" -#include "platform/common.h" - #ifdef _WIN32 #include #endif @@ -43,15 +45,21 @@ using namespace std::literals; #define CERTIFICATE_FILE CA_DIR "/cacert.pem" #define APPS_JSON_PATH platf::appdata().string() + "/apps.json" + namespace config { namespace nv { - nvenc::nvenc_two_pass - twopass_from_view(const std::string_view &preset) { - if (preset == "disabled") return nvenc::nvenc_two_pass::disabled; - if (preset == "quarter_res") return nvenc::nvenc_two_pass::quarter_resolution; - if (preset == "full_res") return nvenc::nvenc_two_pass::full_resolution; + nvenc::nvenc_two_pass twopass_from_view(const std::string_view &preset) { + if (preset == "disabled") { + return nvenc::nvenc_two_pass::disabled; + } + if (preset == "quarter_res") { + return nvenc::nvenc_two_pass::quarter_resolution; + } + if (preset == "full_res") { + return nvenc::nvenc_two_pass::full_resolution; + } BOOST_LOG(warning) << "config: unknown nvenc_twopass value: " << preset; return nvenc::nvenc_two_pass::quarter_resolution; } @@ -59,7 +67,7 @@ namespace config { } // namespace nv namespace amd { -#ifndef _WIN32 +#if !defined(_WIN32) || defined(DOXYGEN) // values accurate as of 27/12/2022, but aren't strictly necessary for MacOS build #define AMF_VIDEO_ENCODER_AV1_QUALITY_PRESET_SPEED 100 #define AMF_VIDEO_ENCODER_AV1_QUALITY_PRESET_QUALITY 30 @@ -178,11 +186,11 @@ namespace config { cavlc = AMF_VIDEO_ENCODER_CALV ///< CAVLC }; - template - std::optional - quality_from_view(const std::string_view &quality_type, const std::optional(&original)) { + template + std::optional quality_from_view(const std::string_view &quality_type, const std::optional(&original)) { #define _CONVERT_(x) \ - if (quality_type == #x##sv) return (int) T::x + if (quality_type == #x##sv) \ + return (int) T::x _CONVERT_(balanced); _CONVERT_(quality); _CONVERT_(speed); @@ -190,11 +198,11 @@ namespace config { return original; } - template - std::optional - rc_from_view(const std::string_view &rc, const std::optional(&original)) { + template + std::optional rc_from_view(const std::string_view &rc, const std::optional(&original)) { #define _CONVERT_(x) \ - if (rc == #x##sv) return (int) T::x + if (rc == #x##sv) \ + return (int) T::x _CONVERT_(cbr); _CONVERT_(cqp); _CONVERT_(vbr_latency); @@ -203,11 +211,11 @@ namespace config { return original; } - template - std::optional - usage_from_view(const std::string_view &usage, const std::optional(&original)) { + template + std::optional usage_from_view(const std::string_view &usage, const std::optional(&original)) { #define _CONVERT_(x) \ - if (usage == #x##sv) return (int) T::x + if (usage == #x##sv) \ + return (int) T::x _CONVERT_(lowlatency); _CONVERT_(lowlatency_high_quality); _CONVERT_(transcoding); @@ -217,11 +225,16 @@ namespace config { return original; } - int - coder_from_view(const std::string_view &coder) { - if (coder == "auto"sv) return _auto; - if (coder == "cabac"sv || coder == "ac"sv) return cabac; - if (coder == "cavlc"sv || coder == "vlc"sv) return cavlc; + int coder_from_view(const std::string_view &coder) { + if (coder == "auto"sv) { + return _auto; + } + if (coder == "cabac"sv || coder == "ac"sv) { + return cabac; + } + if (coder == "cavlc"sv || coder == "vlc"sv) { + return cavlc; + } return _auto; } @@ -244,10 +257,10 @@ namespace config { disabled = false ///< Disabled }; - std::optional - preset_from_view(const std::string_view &preset) { + std::optional preset_from_view(const std::string_view &preset) { #define _CONVERT_(x) \ - if (preset == #x##sv) return x + if (preset == #x##sv) \ + return x _CONVERT_(veryslow); _CONVERT_(slower); _CONVERT_(slow); @@ -259,11 +272,16 @@ namespace config { return std::nullopt; } - std::optional - coder_from_view(const std::string_view &coder) { - if (coder == "auto"sv) return _auto; - if (coder == "cabac"sv || coder == "ac"sv) return disabled; - if (coder == "cavlc"sv || coder == "vlc"sv) return enabled; + std::optional coder_from_view(const std::string_view &coder) { + if (coder == "auto"sv) { + return _auto; + } + if (coder == "cabac"sv || coder == "ac"sv) { + return disabled; + } + if (coder == "cavlc"sv || coder == "vlc"sv) { + return enabled; + } return std::nullopt; } @@ -277,32 +295,40 @@ namespace config { cavlc ///< CAVLC }; - int - coder_from_view(const std::string_view &coder) { - if (coder == "auto"sv) return _auto; - if (coder == "cabac"sv || coder == "ac"sv) return cabac; - if (coder == "cavlc"sv || coder == "vlc"sv) return cavlc; + int coder_from_view(const std::string_view &coder) { + if (coder == "auto"sv) { + return _auto; + } + if (coder == "cabac"sv || coder == "ac"sv) { + return cabac; + } + if (coder == "cavlc"sv || coder == "vlc"sv) { + return cavlc; + } return -1; } - int - allow_software_from_view(const std::string_view &software) { - if (software == "allowed"sv || software == "forced") return 1; + int allow_software_from_view(const std::string_view &software) { + if (software == "allowed"sv || software == "forced") { + return 1; + } return 0; } - int - force_software_from_view(const std::string_view &software) { - if (software == "forced") return 1; + int force_software_from_view(const std::string_view &software) { + if (software == "forced") { + return 1; + } return 0; } - int - rt_from_view(const std::string_view &rt) { - if (rt == "disabled" || rt == "off" || rt == "0") return 0; + int rt_from_view(const std::string_view &rt) { + if (rt == "disabled" || rt == "off" || rt == "0") { + return 0; + } return 1; } @@ -310,10 +336,10 @@ namespace config { } // namespace vt namespace sw { - int - svtav1_preset_from_view(const std::string_view &preset) { + int svtav1_preset_from_view(const std::string_view &preset) { #define _CONVERT_(x, y) \ - if (preset == #x##sv) return y + if (preset == #x##sv) \ + return y _CONVERT_(veryslow, 1); _CONVERT_(slower, 2); _CONVERT_(slow, 4); @@ -328,13 +354,92 @@ namespace config { } } // namespace sw + namespace dd { + video_t::dd_t::config_option_e config_option_from_view(const std::string_view value) { +#define _CONVERT_(x) \ + if (value == #x##sv) \ + return video_t::dd_t::config_option_e::x + _CONVERT_(disabled); + _CONVERT_(verify_only); + _CONVERT_(ensure_active); + _CONVERT_(ensure_primary); + _CONVERT_(ensure_only_display); +#undef _CONVERT_ + return video_t::dd_t::config_option_e::disabled; // Default to this if value is invalid + } + + video_t::dd_t::resolution_option_e resolution_option_from_view(const std::string_view value) { +#define _CONVERT_2_ARG_(str, val) \ + if (value == #str##sv) \ + return video_t::dd_t::resolution_option_e::val +#define _CONVERT_(x) _CONVERT_2_ARG_(x, x) + _CONVERT_(disabled); + _CONVERT_2_ARG_(auto, automatic); + _CONVERT_(manual); +#undef _CONVERT_ +#undef _CONVERT_2_ARG_ + return video_t::dd_t::resolution_option_e::disabled; // Default to this if value is invalid + } + + video_t::dd_t::refresh_rate_option_e refresh_rate_option_from_view(const std::string_view value) { +#define _CONVERT_2_ARG_(str, val) \ + if (value == #str##sv) \ + return video_t::dd_t::refresh_rate_option_e::val +#define _CONVERT_(x) _CONVERT_2_ARG_(x, x) + _CONVERT_(disabled); + _CONVERT_2_ARG_(auto, automatic); + _CONVERT_(manual); +#undef _CONVERT_ +#undef _CONVERT_2_ARG_ + return video_t::dd_t::refresh_rate_option_e::disabled; // Default to this if value is invalid + } + + video_t::dd_t::hdr_option_e hdr_option_from_view(const std::string_view value) { +#define _CONVERT_2_ARG_(str, val) \ + if (value == #str##sv) \ + return video_t::dd_t::hdr_option_e::val +#define _CONVERT_(x) _CONVERT_2_ARG_(x, x) + _CONVERT_(disabled); + _CONVERT_2_ARG_(auto, automatic); +#undef _CONVERT_ +#undef _CONVERT_2_ARG_ + return video_t::dd_t::hdr_option_e::disabled; // Default to this if value is invalid + } + + video_t::dd_t::mode_remapping_t mode_remapping_from_view(const std::string_view value) { + const auto parse_entry_list {[](const auto &entry_list, auto &output_field) { + for (auto &[_, entry] : entry_list) { + auto requested_resolution = entry.template get_optional("requested_resolution"s); + auto requested_fps = entry.template get_optional("requested_fps"s); + auto final_resolution = entry.template get_optional("final_resolution"s); + auto final_refresh_rate = entry.template get_optional("final_refresh_rate"s); + + output_field.push_back(video_t::dd_t::mode_remapping_entry_t {requested_resolution.value_or(""), requested_fps.value_or(""), final_resolution.value_or(""), final_refresh_rate.value_or("")}); + } + }}; + + // We need to add a wrapping object to make it valid JSON, otherwise ptree cannot parse it. + std::stringstream json_stream; + json_stream << "{\"dd_mode_remapping\":" << value << "}"; + + boost::property_tree::ptree json_tree; + boost::property_tree::read_json(json_stream, json_tree); + + video_t::dd_t::mode_remapping_t output; + parse_entry_list(json_tree.get_child("dd_mode_remapping.mixed"), output.mixed); + parse_entry_list(json_tree.get_child("dd_mode_remapping.resolution_only"), output.resolution_only); + parse_entry_list(json_tree.get_child("dd_mode_remapping.refresh_rate_only"), output.refresh_rate_only); + + return output; + } + } // namespace dd + video_t video { 28, // qp 0, // hevc_mode 0, // av1_mode - 1, // min_fps_factor 2, // min_threads { "superfast"s, // preset @@ -385,11 +490,28 @@ namespace config { {}, // encoder {}, // adapter_name {}, // output_name + + { + video_t::dd_t::config_option_e::disabled, // configuration_option + video_t::dd_t::resolution_option_e::automatic, // resolution_option + {}, // manual_resolution + video_t::dd_t::refresh_rate_option_e::automatic, // refresh_rate_option + {}, // manual_refresh_rate + video_t::dd_t::hdr_option_e::automatic, // hdr_option + 3s, // config_revert_delay + {}, // config_revert_on_disconnect + {}, // mode_remapping + {} // wa + }, // display_device + + 1, // min_fps_factor + 0 // max_bitrate }; audio_t audio { {}, // audio_sink {}, // virtual_sink + true, // stream audio true, // install_steam_drivers }; @@ -417,13 +539,13 @@ namespace config { input_t input { { - { 0x10, 0xA0 }, - { 0x11, 0xA2 }, - { 0x12, 0xA4 }, + {0x10, 0xA0}, + {0x11, 0xA2}, + {0x12, 0xA4}, }, -1ms, // back_button_timeout 500ms, // key_repeat_delay - std::chrono::duration { 1 / 24.9 }, // key_repeat_period + std::chrono::duration {1 / 24.9}, // key_repeat_period { platf::supported_gamepads(nullptr).front().name.data(), @@ -458,23 +580,19 @@ namespace config { {}, // prep commands }; - bool - endline(char ch) { + bool endline(char ch) { return ch == '\r' || ch == '\n'; } - bool - space_tab(char ch) { + bool space_tab(char ch) { return ch == ' ' || ch == '\t'; } - bool - whitespace(char ch) { + bool whitespace(char ch) { return space_tab(ch) || endline(ch); } - std::string - to_string(const char *begin, const char *end) { + std::string to_string(const char *begin, const char *end) { std::string result; KITTY_WHILE_LOOP(auto pos = begin, pos != end, { @@ -489,9 +607,8 @@ namespace config { return result; } - template - It - skip_list(It skipper, It end) { + template + It skip_list(It skipper, It end) { int stack = 1; while (skipper != end && stack) { if (*skipper == '[') { @@ -510,7 +627,7 @@ namespace config { std::pair< std::string_view::const_iterator, std::optional>> - parse_option(std::string_view::const_iterator begin, std::string_view::const_iterator end) { + parse_option(std::string_view::const_iterator begin, std::string_view::const_iterator end) { begin = std::find_if_not(begin, end, whitespace); auto endl = std::find_if(begin, end, endline); auto endc = std::find(begin, endl, '#'); @@ -531,20 +648,24 @@ namespace config { // Lists might contain newlines if (*begin_val == '[') { endl = skip_list(begin_val + 1, end); - if (endl == end) { - std::cout << "Warning: Config option ["sv << to_string(begin, end_name) << "] Missing ']'"sv; + // Check if we reached the end of the file without finding a closing bracket + // We know we have a valid closing bracket if: + // 1. We didn't reach the end, or + // 2. We reached the end but the last character was the matching closing bracket + if (endl == end && end == begin_val + 1) { + BOOST_LOG(warning) << "config: Missing ']' in config option: " << to_string(begin, end_name); return std::make_pair(endl, std::nullopt); } } return std::make_pair( endl, - std::make_pair(to_string(begin, end_name), to_string(begin_val, endl))); + std::make_pair(to_string(begin, end_name), to_string(begin_val, endl)) + ); } - std::unordered_map - parse_config(const std::string_view &file_content) { + std::unordered_map parse_config(const std::string_view &file_content) { std::unordered_map vars; auto pos = std::begin(file_content); @@ -569,8 +690,7 @@ namespace config { return vars; } - void - string_f(std::unordered_map &vars, const std::string &name, std::string &input) { + void string_f(std::unordered_map &vars, const std::string &name, std::string &input) { auto it = vars.find(name); if (it == std::end(vars)) { return; @@ -581,9 +701,8 @@ namespace config { vars.erase(it); } - template - void - generic_f(std::unordered_map &vars, const std::string &name, T &input, F &&f) { + template + void generic_f(std::unordered_map &vars, const std::string &name, T &input, F &&f) { std::string tmp; string_f(vars, name, tmp); if (!tmp.empty()) { @@ -591,8 +710,7 @@ namespace config { } } - void - string_restricted_f(std::unordered_map &vars, const std::string &name, std::string &input, const std::vector &allowed_vals) { + void string_restricted_f(std::unordered_map &vars, const std::string &name, std::string &input, const std::vector &allowed_vals) { std::string temp; string_f(vars, name, temp); @@ -604,8 +722,7 @@ namespace config { } } - void - path_f(std::unordered_map &vars, const std::string &name, fs::path &input) { + void path_f(std::unordered_map &vars, const std::string &name, fs::path &input) { // appdata needs to be retrieved once only static auto appdata = platf::appdata(); @@ -629,8 +746,7 @@ namespace config { } } - void - path_f(std::unordered_map &vars, const std::string &name, std::string &input) { + void path_f(std::unordered_map &vars, const std::string &name, std::string &input) { fs::path temp = input; path_f(vars, name, temp); @@ -638,8 +754,7 @@ namespace config { input = temp.string(); } - void - int_f(std::unordered_map &vars, const std::string &name, int &input) { + void int_f(std::unordered_map &vars, const std::string &name, int &input) { auto it = vars.find(name); if (it == std::end(vars)) { @@ -656,16 +771,14 @@ namespace config { // If that integer is in hexadecimal if (val.size() >= 2 && val.substr(0, 2) == "0x"sv) { input = util::from_hex(val.substr(2)); - } - else { + } else { input = util::from_view(val); } vars.erase(it); } - void - int_f(std::unordered_map &vars, const std::string &name, std::optional &input) { + void int_f(std::unordered_map &vars, const std::string &name, std::optional &input) { auto it = vars.find(name); if (it == std::end(vars)) { @@ -682,17 +795,15 @@ namespace config { // If that integer is in hexadecimal if (val.size() >= 2 && val.substr(0, 2) == "0x"sv) { input = util::from_hex(val.substr(2)); - } - else { + } else { input = util::from_view(val); } vars.erase(it); } - template - void - int_f(std::unordered_map &vars, const std::string &name, int &input, F &&f) { + template + void int_f(std::unordered_map &vars, const std::string &name, int &input, F &&f) { std::string tmp; string_f(vars, name, tmp); if (!tmp.empty()) { @@ -700,9 +811,8 @@ namespace config { } } - template - void - int_f(std::unordered_map &vars, const std::string &name, std::optional &input, F &&f) { + template + void int_f(std::unordered_map &vars, const std::string &name, std::optional &input, F &&f) { std::string tmp; string_f(vars, name, tmp); if (!tmp.empty()) { @@ -710,8 +820,7 @@ namespace config { } } - void - int_between_f(std::unordered_map &vars, const std::string &name, int &input, const std::pair &range) { + void int_between_f(std::unordered_map &vars, const std::string &name, int &input, const std::pair &range) { int temp = input; int_f(vars, name, temp); @@ -722,9 +831,10 @@ namespace config { } } - bool - to_bool(std::string &boolean) { - std::for_each(std::begin(boolean), std::end(boolean), [](char ch) { return (char) std::tolower(ch); }); + bool to_bool(std::string &boolean) { + std::for_each(std::begin(boolean), std::end(boolean), [](char ch) { + return (char) std::tolower(ch); + }); return boolean == "true"sv || boolean == "yes"sv || @@ -734,8 +844,7 @@ namespace config { (std::find(std::begin(boolean), std::end(boolean), '1') != std::end(boolean)); } - void - bool_f(std::unordered_map &vars, const std::string &name, bool &input) { + void bool_f(std::unordered_map &vars, const std::string &name, bool &input) { std::string tmp; string_f(vars, name, tmp); @@ -746,8 +855,7 @@ namespace config { input = to_bool(tmp); } - void - double_f(std::unordered_map &vars, const std::string &name, double &input) { + void double_f(std::unordered_map &vars, const std::string &name, double &input) { std::string tmp; string_f(vars, name, tmp); @@ -765,8 +873,7 @@ namespace config { input = val; } - void - double_between_f(std::unordered_map &vars, const std::string &name, double &input, const std::pair &range) { + void double_between_f(std::unordered_map &vars, const std::string &name, double &input, const std::pair &range) { double temp = input; double_f(vars, name, temp); @@ -777,8 +884,7 @@ namespace config { } } - void - list_string_f(std::unordered_map &vars, const std::string &name, std::vector &input) { + void list_string_f(std::unordered_map &vars, const std::string &name, std::vector &input) { std::string string; string_f(vars, name, string); @@ -802,15 +908,12 @@ namespace config { while (pos < std::cend(string)) { if (*pos == '[') { pos = skip_list(pos + 1, std::cend(string)) + 1; - } - else if (*pos == ']') { + } else if (*pos == ']') { break; - } - else if (*pos == ',') { + } else if (*pos == ',') { input.emplace_back(begin, pos); pos = begin = std::find_if_not(pos + 1, std::cend(string), whitespace); - } - else { + } else { ++pos; } } @@ -820,8 +923,7 @@ namespace config { } } - void - list_prep_cmd_f(std::unordered_map &vars, const std::string &name, std::vector &input) { + void list_prep_cmd_f(std::unordered_map &vars, const std::string &name, std::vector &input) { std::string string; string_f(vars, name, string); @@ -847,8 +949,7 @@ namespace config { } } - void - list_int_f(std::unordered_map &vars, const std::string &name, std::vector &input) { + void list_int_f(std::unordered_map &vars, const std::string &name, std::vector &input) { std::vector list; list_string_f(vars, name, list); @@ -874,22 +975,20 @@ namespace config { // If the integer is a hexadecimal if (val.size() >= 2 && val.substr(0, 2) == "0x"sv) { tmp = util::from_hex(val.substr(2)); - } - else { + } else { tmp = util::from_view(val); } input.emplace_back(tmp); } } - void - map_int_int_f(std::unordered_map &vars, const std::string &name, std::unordered_map &input) { + void map_int_int_f(std::unordered_map &vars, const std::string &name, std::unordered_map &input) { std::vector list; list_int_f(vars, name, list); // The list needs to be a multiple of 2 if (list.size() % 2) { - std::cout << "Warning: expected "sv << name << " to have a multiple of two elements --> not "sv << list.size() << std::endl; + BOOST_LOG(warning) << "config: expected "sv << name << " to have a multiple of two elements --> not "sv << list.size(); return; } @@ -902,8 +1001,7 @@ namespace config { } } - int - apply_flags(const char *line) { + int apply_flags(const char *line) { int ret = 0; while (*line != '\0') { switch (*line) { @@ -920,7 +1018,7 @@ namespace config { config::sunshine.flags[config::flag::UPNP].flip(); break; default: - std::cout << "Warning: Unrecognized flag: ["sv << *line << ']' << std::endl; + BOOST_LOG(warning) << "config: Unrecognized flag: ["sv << *line << ']' << std::endl; ret = -1; } @@ -930,8 +1028,7 @@ namespace config { return ret; } - std::vector & - get_supported_gamepad_options() { + std::vector &get_supported_gamepad_options() { const auto options = platf::supported_gamepads(nullptr); static std::vector opts {}; opts.reserve(options.size()); @@ -941,28 +1038,28 @@ namespace config { return opts; } - void - apply_config(std::unordered_map &&vars) { + void apply_config(std::unordered_map &&vars) { if (!fs::exists(stream.file_apps.c_str())) { fs::copy_file(SUNSHINE_ASSETS_DIR "/apps.json", stream.file_apps); } for (auto &[name, val] : vars) { - std::cout << "["sv << name << "] -- ["sv << val << ']' << std::endl; + BOOST_LOG(info) << "config: '"sv << name << "' = "sv << val; + modified_config_settings[name] = val; } int_f(vars, "qp", video.qp); + int_between_f(vars, "hevc_mode", video.hevc_mode, {0, 3}); + int_between_f(vars, "av1_mode", video.av1_mode, {0, 3}); int_f(vars, "min_threads", video.min_threads); - int_between_f(vars, "hevc_mode", video.hevc_mode, { 0, 3 }); - int_between_f(vars, "av1_mode", video.av1_mode, { 0, 3 }); string_f(vars, "sw_preset", video.sw.sw_preset); if (!video.sw.sw_preset.empty()) { video.sw.svtav1_preset = sw::svtav1_preset_from_view(video.sw.sw_preset); } string_f(vars, "sw_tune", video.sw.sw_tune); - int_between_f(vars, "nvenc_preset", video.nv.quality_preset, { 1, 7 }); - int_between_f(vars, "nvenc_vbv_increase", video.nv.vbv_percentage_increase, { 0, 400 }); + int_between_f(vars, "nvenc_preset", video.nv.quality_preset, {1, 7}); + int_between_f(vars, "nvenc_vbv_increase", video.nv.vbv_percentage_increase, {0, 400}); bool_f(vars, "nvenc_spatial_aq", video.nv.adaptive_quantization); generic_f(vars, "nvenc_twopass", video.nv.two_pass, nv::twopass_from_view); bool_f(vars, "nvenc_h264_cavlc", video.nv.h264_cavlc); @@ -1024,7 +1121,30 @@ namespace config { string_f(vars, "encoder", video.encoder); string_f(vars, "adapter_name", video.adapter_name); string_f(vars, "output_name", video.output_name); - int_between_f(vars, "min_fps_factor", video.min_fps_factor, { 1, 3 }); + + generic_f(vars, "dd_configuration_option", video.dd.configuration_option, dd::config_option_from_view); + generic_f(vars, "dd_resolution_option", video.dd.resolution_option, dd::resolution_option_from_view); + string_f(vars, "dd_manual_resolution", video.dd.manual_resolution); + generic_f(vars, "dd_refresh_rate_option", video.dd.refresh_rate_option, dd::refresh_rate_option_from_view); + string_f(vars, "dd_manual_refresh_rate", video.dd.manual_refresh_rate); + generic_f(vars, "dd_hdr_option", video.dd.hdr_option, dd::hdr_option_from_view); + { + int value = -1; + int_between_f(vars, "dd_config_revert_delay", value, {0, std::numeric_limits::max()}); + if (value >= 0) { + video.dd.config_revert_delay = std::chrono::milliseconds {value}; + } + } + bool_f(vars, "dd_config_revert_on_disconnect", video.dd.config_revert_on_disconnect); + generic_f(vars, "dd_mode_remapping", video.dd.mode_remapping, dd::mode_remapping_from_view); + { + int value = 0; + int_between_f(vars, "dd_wa_hdr_toggle_delay", value, {0, 3000}); + video.dd.wa.hdr_toggle_delay = std::chrono::milliseconds {value}; + } + + int_between_f(vars, "min_fps_factor", video.min_fps_factor, {1, 3}); + int_f(vars, "max_bitrate", video.max_bitrate); path_f(vars, "pkey", nvhttp.pkey); path_f(vars, "cert", nvhttp.cert); @@ -1041,21 +1161,22 @@ namespace config { string_f(vars, "audio_sink", audio.sink); string_f(vars, "virtual_sink", audio.virtual_sink); + bool_f(vars, "stream_audio", audio.stream); bool_f(vars, "install_steam_audio_drivers", audio.install_steam_drivers); - string_restricted_f(vars, "origin_web_ui_allowed", nvhttp.origin_web_ui_allowed, { "pc"sv, "lan"sv, "wan"sv }); + string_restricted_f(vars, "origin_web_ui_allowed", nvhttp.origin_web_ui_allowed, {"pc"sv, "lan"sv, "wan"sv}); int to = -1; - int_between_f(vars, "ping_timeout", to, { -1, std::numeric_limits::max() }); + int_between_f(vars, "ping_timeout", to, {-1, std::numeric_limits::max()}); if (to != -1) { stream.ping_timeout = std::chrono::milliseconds(to); } - int_between_f(vars, "lan_encryption_mode", stream.lan_encryption_mode, { 0, 2 }); - int_between_f(vars, "wan_encryption_mode", stream.wan_encryption_mode, { 0, 2 }); + int_between_f(vars, "lan_encryption_mode", stream.lan_encryption_mode, {0, 2}); + int_between_f(vars, "wan_encryption_mode", stream.wan_encryption_mode, {0, 2}); path_f(vars, "file_apps", stream.file_apps); - int_between_f(vars, "fec_percentage", stream.fec_percentage, { 1, 255 }); + int_between_f(vars, "fec_percentage", stream.fec_percentage, {1, 255}); map_int_int_f(vars, "keybindings"s, input.keybindings); @@ -1072,20 +1193,20 @@ namespace config { int_f(vars, "back_button_timeout", to); if (to > std::numeric_limits::min()) { - input.back_button_timeout = std::chrono::milliseconds { to }; + input.back_button_timeout = std::chrono::milliseconds {to}; } - double repeat_frequency { 0 }; - double_between_f(vars, "key_repeat_frequency", repeat_frequency, { 0, std::numeric_limits::max() }); + double repeat_frequency {0}; + double_between_f(vars, "key_repeat_frequency", repeat_frequency, {0, std::numeric_limits::max()}); if (repeat_frequency > 0) { - config::input.key_repeat_period = std::chrono::duration { 1 / repeat_frequency }; + config::input.key_repeat_period = std::chrono::duration {1 / repeat_frequency}; } to = -1; int_f(vars, "key_repeat_delay", to); if (to >= 0) { - input.key_repeat_delay = std::chrono::milliseconds { to }; + input.key_repeat_delay = std::chrono::milliseconds {to}; } string_restricted_f(vars, "gamepad"s, input.gamepad, get_supported_gamepad_options()); @@ -1105,10 +1226,10 @@ namespace config { bool_f(vars, "notify_pre_releases", sunshine.notify_pre_releases); int port = sunshine.port; - int_between_f(vars, "port"s, port, { 1024 + nvhttp::PORT_HTTPS, 65535 - rtsp_stream::RTSP_SETUP_PORT }); + int_between_f(vars, "port"s, port, {1024 + nvhttp::PORT_HTTPS, 65535 - rtsp_stream::RTSP_SETUP_PORT}); sunshine.port = (std::uint16_t) port; - string_restricted_f(vars, "address_family", sunshine.address_family, { "ipv4"sv, "both"sv }); + string_restricted_f(vars, "address_family", sunshine.address_family, {"ipv4"sv, "both"sv}); bool upnp = false; bool_f(vars, "upnp"s, upnp); @@ -1118,6 +1239,7 @@ namespace config { } string_restricted_f(vars, "locale", config::sunshine.locale, { + "bg"sv, // Bulgarian "de"sv, // German "en"sv, // English "en_GB"sv, // English (UK) @@ -1126,10 +1248,14 @@ namespace config { "fr"sv, // French "it"sv, // Italian "ja"sv, // Japanese + "ko"sv, // Korean + "pl"sv, // Polish "pt"sv, // Portuguese + "pt_BR"sv, // Portuguese (Brazilian) "ru"sv, // Russian "sv"sv, // Swedish "tr"sv, // Turkish + "uk"sv, // Ukrainian "zh"sv, // Chinese }); @@ -1139,26 +1265,19 @@ namespace config { if (!log_level_string.empty()) { if (log_level_string == "verbose"sv) { sunshine.min_log_level = 0; - } - else if (log_level_string == "debug"sv) { + } else if (log_level_string == "debug"sv) { sunshine.min_log_level = 1; - } - else if (log_level_string == "info"sv) { + } else if (log_level_string == "info"sv) { sunshine.min_log_level = 2; - } - else if (log_level_string == "warning"sv) { + } else if (log_level_string == "warning"sv) { sunshine.min_log_level = 3; - } - else if (log_level_string == "error"sv) { + } else if (log_level_string == "error"sv) { sunshine.min_log_level = 4; - } - else if (log_level_string == "fatal"sv) { + } else if (log_level_string == "fatal"sv) { sunshine.min_log_level = 5; - } - else if (log_level_string == "none"sv) { + } else if (log_level_string == "none"sv) { sunshine.min_log_level = 6; - } - else { + } else { // accept digit directly auto val = log_level_string[0]; if (val >= '0' && val < '7') { @@ -1181,8 +1300,7 @@ namespace config { } } - int - parse(int argc, char *argv[]) { + int parse(int argc, char *argv[]) { std::unordered_map cmd_vars; #ifdef _WIN32 bool shortcut_launch = false; @@ -1199,8 +1317,7 @@ namespace config { #ifdef _WIN32 else if (line == "--shortcut"sv) { shortcut_launch = true; - } - else if (line == "--shortcut-admin"sv) { + } else if (line == "--shortcut-admin"sv) { service_admin_launch = true; } #endif @@ -1216,15 +1333,13 @@ namespace config { logging::print_help(*argv); return -1; } - } - else { + } else { auto line_end = line + strlen(line); auto pos = std::find(line, line_end, '='); if (pos == line_end) { sunshine.config_file = line; - } - else { + } else { TUPLE_EL(var, 1, parse_option(line, line_end)); if (!var) { logging::print_help(*argv); @@ -1250,7 +1365,7 @@ namespace config { // Create empty config file if it does not exist if (!fs::exists(sunshine.config_file)) { - std::ofstream { sunshine.config_file }; + std::ofstream {sunshine.config_file}; } // Read config file @@ -1265,11 +1380,9 @@ namespace config { // the path is incorrect or inaccessible. apply_config(std::move(vars)); config_loaded = true; - } - catch (const std::filesystem::filesystem_error &err) { + } catch (const std::filesystem::filesystem_error &err) { BOOST_LOG(fatal) << "Failed to apply config: "sv << err.what(); - } - catch (const boost::filesystem::filesystem_error &err) { + } catch (const boost::filesystem::filesystem_error &err) { BOOST_LOG(fatal) << "Failed to apply config: "sv << err.what(); } @@ -1299,7 +1412,7 @@ namespace config { // Always return 1 to ensure Sunshine doesn't start normally return 1; } - else if (shortcut_launch) { + if (shortcut_launch) { if (!service_ctrl::is_service_running()) { // If the service isn't running, relaunch ourselves as admin to start it WCHAR executable[MAX_PATH]; @@ -1314,7 +1427,7 @@ namespace config { shell_exec_info.nShow = SW_NORMAL; if (!ShellExecuteExW(&shell_exec_info)) { auto winerr = GetLastError(); - std::cout << "Error: ShellExecuteEx() failed:"sv << winerr << std::endl; + BOOST_LOG(error) << "Failed executing shell command: " << winerr << std::endl; return 1; } diff --git a/src/config.h b/src/config.h index 891a4079772..066b95df182 100644 --- a/src/config.h +++ b/src/config.h @@ -4,6 +4,7 @@ */ #pragma once +// standard includes #include #include #include @@ -11,9 +12,13 @@ #include #include +// local includes #include "nvenc/nvenc_config.h" namespace config { + // track modified config options + inline std::unordered_map modified_config_settings; + struct video_t { // ffmpeg params int qp; // higher == more compression and less quality @@ -21,8 +26,8 @@ namespace config { int hevc_mode; int av1_mode; - int min_fps_factor; // Minimum fps target, determines minimum frame time int min_threads; // Minimum number of threads/slices for CPU encoding + struct { std::string sw_preset; std::string sw_tune; @@ -79,11 +84,70 @@ namespace config { std::string encoder; std::string adapter_name; std::string output_name; + + struct dd_t { + struct workarounds_t { + std::chrono::milliseconds hdr_toggle_delay; ///< Specify whether to apply HDR high-contrast color workaround and what delay to use. + }; + + enum class config_option_e { + disabled, ///< Disable the configuration for the device. + verify_only, ///< @seealso{display_device::SingleDisplayConfiguration::DevicePreparation} + ensure_active, ///< @seealso{display_device::SingleDisplayConfiguration::DevicePreparation} + ensure_primary, ///< @seealso{display_device::SingleDisplayConfiguration::DevicePreparation} + ensure_only_display ///< @seealso{display_device::SingleDisplayConfiguration::DevicePreparation} + }; + + enum class resolution_option_e { + disabled, ///< Do not change resolution. + automatic, ///< Change resolution and use the one received from Moonlight. + manual ///< Change resolution and use the manually provided one. + }; + + enum class refresh_rate_option_e { + disabled, ///< Do not change refresh rate. + automatic, ///< Change refresh rate and use the one received from Moonlight. + manual ///< Change refresh rate and use the manually provided one. + }; + + enum class hdr_option_e { + disabled, ///< Do not change HDR settings. + automatic ///< Change HDR settings and use the state requested by Moonlight. + }; + + struct mode_remapping_entry_t { + std::string requested_resolution; + std::string requested_fps; + std::string final_resolution; + std::string final_refresh_rate; + }; + + struct mode_remapping_t { + std::vector mixed; ///< To be used when `resolution_option` and `refresh_rate_option` is set to `automatic`. + std::vector resolution_only; ///< To be use when only `resolution_option` is set to `automatic`. + std::vector refresh_rate_only; ///< To be use when only `refresh_rate_option` is set to `automatic`. + }; + + config_option_e configuration_option; + resolution_option_e resolution_option; + std::string manual_resolution; ///< Manual resolution in case `resolution_option == resolution_option_e::manual`. + refresh_rate_option_e refresh_rate_option; + std::string manual_refresh_rate; ///< Manual refresh rate in case `refresh_rate_option == refresh_rate_option_e::manual`. + hdr_option_e hdr_option; + std::chrono::milliseconds config_revert_delay; ///< Time to wait until settings are reverted (after stream ends/app exists). + bool config_revert_on_disconnect; ///< Specify whether to revert display configuration on client disconnect. + mode_remapping_t mode_remapping; + workarounds_t wa; + } dd; + + int min_fps_factor; // Minimum fps target, determines minimum frame time + int max_bitrate; // Maximum bitrate, sets ceiling in kbps for bitrate requested from client }; struct audio_t { std::string sink; std::string virtual_sink; + bool stream; bool install_steam_drivers; }; @@ -149,17 +213,25 @@ namespace config { CONST_PIN, ///< Use "universal" pin FLAG_SIZE ///< Number of flags }; - } + } // namespace flag struct prep_cmd_t { prep_cmd_t(std::string &&do_cmd, std::string &&undo_cmd, bool &&elevated): - do_cmd(std::move(do_cmd)), undo_cmd(std::move(undo_cmd)), elevated(std::move(elevated)) {} + do_cmd(std::move(do_cmd)), + undo_cmd(std::move(undo_cmd)), + elevated(std::move(elevated)) { + } + explicit prep_cmd_t(std::string &&do_cmd, bool &&elevated): - do_cmd(std::move(do_cmd)), elevated(std::move(elevated)) {} + do_cmd(std::move(do_cmd)), + elevated(std::move(elevated)) { + } + std::string do_cmd; std::string undo_cmd; bool elevated; }; + struct sunshine_t { std::string locale; int min_log_level; @@ -193,8 +265,6 @@ namespace config { extern input_t input; extern sunshine_t sunshine; - int - parse(int argc, char *argv[]); - std::unordered_map - parse_config(const std::string_view &file_content); + int parse(int argc, char *argv[]); + std::unordered_map parse_config(const std::string_view &file_content); } // namespace config diff --git a/src/confighttp.cpp b/src/confighttp.cpp index 756a4688259..4ba09b88a5a 100644 --- a/src/confighttp.cpp +++ b/src/confighttp.cpp @@ -6,28 +6,24 @@ */ #define BOOST_BIND_GLOBAL_PLACEHOLDERS -#include "process.h" - +// standard includes #include +#include #include -#include -#include -#include - +// lib includes #include - #include - #include - +#include #include #include -#include +// local includes #include "config.h" #include "confighttp.h" #include "crypto.h" +#include "display_device.h" #include "file_handler.h" #include "globals.h" #include "httpcommon.h" @@ -35,7 +31,7 @@ #include "network.h" #include "nvhttp.h" #include "platform/common.h" -#include "rtsp.h" +#include "process.h" #include "utility.h" #include "uuid.h" #include "version.h" @@ -44,7 +40,6 @@ using namespace std::literals; namespace confighttp { namespace fs = std::filesystem; - namespace pt = boost::property_tree; using https_server_t = SimpleWeb::Server; @@ -57,8 +52,11 @@ namespace confighttp { REMOVE ///< Remove client }; - void - print_req(const req_https_t &request) { + /** + * @brief Log the request details. + * @param request The HTTP request object. + */ + void print_req(const req_https_t &request) { BOOST_LOG(debug) << "METHOD :: "sv << request->method; BOOST_LOG(debug) << "DESTINATION :: "sv << request->path; @@ -75,28 +73,64 @@ namespace confighttp { BOOST_LOG(debug) << " [--] "sv; } - void - send_unauthorized(resp_https_t response, req_https_t request) { + /** + * @brief Send a response. + * @param response The HTTP response object. + * @param output_tree The JSON tree to send. + */ + void send_response(resp_https_t response, const nlohmann::json &output_tree) { + SimpleWeb::CaseInsensitiveMultimap headers; + headers.emplace("Content-Type", "application/json"); + + response->write(output_tree.dump(), headers); + } + + /** + * @brief Send a 401 Unauthorized response. + * @param response The HTTP response object. + * @param request The HTTP request object. + */ + void send_unauthorized(resp_https_t response, req_https_t request) { auto address = net::addr_to_normalized_string(request->remote_endpoint().address()); BOOST_LOG(info) << "Web UI: ["sv << address << "] -- not authorized"sv; + + constexpr SimpleWeb::StatusCode code = SimpleWeb::StatusCode::client_error_unauthorized; + + nlohmann::json tree; + tree["status_code"] = code; + tree["status"] = false; + tree["error"] = "Unauthorized"; + const SimpleWeb::CaseInsensitiveMultimap headers { - { "WWW-Authenticate", R"(Basic realm="Sunshine Gamestream Host", charset="UTF-8")" } + {"Content-Type", "application/json"}, + {"WWW-Authenticate", R"(Basic realm="Sunshine Gamestream Host", charset="UTF-8")"} }; - response->write(SimpleWeb::StatusCode::client_error_unauthorized, headers); + + response->write(code, tree.dump(), headers); } - void - send_redirect(resp_https_t response, req_https_t request, const char *path) { + /** + * @brief Send a redirect response. + * @param response The HTTP response object. + * @param request The HTTP request object. + * @param path The path to redirect to. + */ + void send_redirect(resp_https_t response, req_https_t request, const char *path) { auto address = net::addr_to_normalized_string(request->remote_endpoint().address()); BOOST_LOG(info) << "Web UI: ["sv << address << "] -- not authorized"sv; const SimpleWeb::CaseInsensitiveMultimap headers { - { "Location", path } + {"Location", path} }; response->write(SimpleWeb::StatusCode::redirection_temporary_redirect, headers); } - bool - authenticate(resp_https_t response, req_https_t request) { + /** + * @brief Authenticate the user. + * @param response The HTTP response object. + * @param request The HTTP request object. + * @return True if the user is authenticated, false otherwise. + */ + bool authenticate(resp_https_t response, req_https_t request) { auto address = net::addr_to_normalized_string(request->remote_endpoint().address()); auto ip_type = net::from_address(address); @@ -141,26 +175,54 @@ namespace confighttp { return true; } - void - not_found(resp_https_t response, req_https_t request) { - pt::ptree tree; - tree.put("root..status_code", 404); + /** + * @brief Send a 404 Not Found response. + * @param response The HTTP response object. + * @param request The HTTP request object. + */ + void not_found(resp_https_t response, [[maybe_unused]] req_https_t request) { + constexpr SimpleWeb::StatusCode code = SimpleWeb::StatusCode::client_error_not_found; + + nlohmann::json tree; + tree["status_code"] = code; + tree["error"] = "Not Found"; + + SimpleWeb::CaseInsensitiveMultimap headers; + headers.emplace("Content-Type", "application/json"); + + response->write(code, tree.dump(), headers); + } + + /** + * @brief Send a 400 Bad Request response. + * @param response The HTTP response object. + * @param request The HTTP request object. + * @param error_message The error message to include in the response. + */ + void bad_request(resp_https_t response, [[maybe_unused]] req_https_t request, const std::string &error_message = "Bad Request") { + constexpr SimpleWeb::StatusCode code = SimpleWeb::StatusCode::client_error_bad_request; - std::ostringstream data; + nlohmann::json tree; + tree["status_code"] = code; + tree["status"] = false; + tree["error"] = error_message; - pt::write_xml(data, tree); - response->write(data.str()); + SimpleWeb::CaseInsensitiveMultimap headers; + headers.emplace("Content-Type", "application/json"); - *response << "HTTP/1.1 404 NOT FOUND\r\n" - << data.str(); + response->write(code, tree.dump(), headers); } /** + * @brief Get the index page. + * @param response The HTTP response object. + * @param request The HTTP request object. * @todo combine these functions into a single function that accepts the page, i.e "index", "pin", "apps" */ - void - getIndexPage(resp_https_t response, req_https_t request) { - if (!authenticate(response, request)) return; + void getIndexPage(resp_https_t response, req_https_t request) { + if (!authenticate(response, request)) { + return; + } print_req(request); @@ -170,9 +232,15 @@ namespace confighttp { response->write(content, headers); } - void - getPinPage(resp_https_t response, req_https_t request) { - if (!authenticate(response, request)) return; + /** + * @brief Get the PIN page. + * @param response The HTTP response object. + * @param request The HTTP request object. + */ + void getPinPage(resp_https_t response, req_https_t request) { + if (!authenticate(response, request)) { + return; + } print_req(request); @@ -182,9 +250,15 @@ namespace confighttp { response->write(content, headers); } - void - getAppsPage(resp_https_t response, req_https_t request) { - if (!authenticate(response, request)) return; + /** + * @brief Get the apps page. + * @param response The HTTP response object. + * @param request The HTTP request object. + */ + void getAppsPage(resp_https_t response, req_https_t request) { + if (!authenticate(response, request)) { + return; + } print_req(request); @@ -195,9 +269,15 @@ namespace confighttp { response->write(content, headers); } - void - getClientsPage(resp_https_t response, req_https_t request) { - if (!authenticate(response, request)) return; + /** + * @brief Get the clients page. + * @param response The HTTP response object. + * @param request The HTTP request object. + */ + void getClientsPage(resp_https_t response, req_https_t request) { + if (!authenticate(response, request)) { + return; + } print_req(request); @@ -207,9 +287,15 @@ namespace confighttp { response->write(content, headers); } - void - getConfigPage(resp_https_t response, req_https_t request) { - if (!authenticate(response, request)) return; + /** + * @brief Get the configuration page. + * @param response The HTTP response object. + * @param request The HTTP request object. + */ + void getConfigPage(resp_https_t response, req_https_t request) { + if (!authenticate(response, request)) { + return; + } print_req(request); @@ -219,9 +305,15 @@ namespace confighttp { response->write(content, headers); } - void - getPasswordPage(resp_https_t response, req_https_t request) { - if (!authenticate(response, request)) return; + /** + * @brief Get the password page. + * @param response The HTTP response object. + * @param request The HTTP request object. + */ + void getPasswordPage(resp_https_t response, req_https_t request) { + if (!authenticate(response, request)) { + return; + } print_req(request); @@ -231,8 +323,12 @@ namespace confighttp { response->write(content, headers); } - void - getWelcomePage(resp_https_t response, req_https_t request) { + /** + * @brief Get the welcome page. + * @param response The HTTP response object. + * @param request The HTTP request object. + */ + void getWelcomePage(resp_https_t response, req_https_t request) { print_req(request); if (!config::sunshine.username.empty()) { send_redirect(response, request, "/"); @@ -244,9 +340,15 @@ namespace confighttp { response->write(content, headers); } - void - getTroubleshootingPage(resp_https_t response, req_https_t request) { - if (!authenticate(response, request)) return; + /** + * @brief Get the troubleshooting page. + * @param response The HTTP response object. + * @param request The HTTP request object. + */ + void getTroubleshootingPage(resp_https_t response, req_https_t request) { + if (!authenticate(response, request)) { + return; + } print_req(request); @@ -257,11 +359,13 @@ namespace confighttp { } /** + * @brief Get the favicon image. + * @param response The HTTP response object. + * @param request The HTTP request object. * @todo combine function with getSunshineLogoImage and possibly getNodeModules * @todo use mime_types map */ - void - getFaviconImage(resp_https_t response, req_https_t request) { + void getFaviconImage(resp_https_t response, req_https_t request) { print_req(request); std::ifstream in(WEB_DIR "images/sunshine.ico", std::ios::binary); @@ -271,11 +375,13 @@ namespace confighttp { } /** + * @brief Get the Sunshine logo image. + * @param response The HTTP response object. + * @param request The HTTP request object. * @todo combine function with getFaviconImage and possibly getNodeModules * @todo use mime_types map */ - void - getSunshineLogoImage(resp_https_t response, req_https_t request) { + void getSunshineLogoImage(resp_https_t response, req_https_t request) { print_req(request); std::ifstream in(WEB_DIR "images/logo-sunshine-45.png", std::ios::binary); @@ -284,14 +390,23 @@ namespace confighttp { response->write(SimpleWeb::StatusCode::success_ok, in, headers); } - bool - isChildPath(fs::path const &base, fs::path const &query) { + /** + * @brief Check if a path is a child of another path. + * @param base The base path. + * @param query The path to check. + * @return True if the path is a child of the base path, false otherwise. + */ + bool isChildPath(fs::path const &base, fs::path const &query) { auto relPath = fs::relative(base, query); return *(relPath.begin()) != fs::path(".."); } - void - getNodeModules(resp_https_t response, req_https_t request) { + /** + * @brief Get an asset from the node_modules directory. + * @param response The HTTP response object. + * @param request The HTTP request object. + */ + void getNodeModules(resp_https_t response, req_https_t request) { print_req(request); fs::path webDirPath(WEB_DIR); fs::path nodeModulesPath(webDirPath / "assets"); @@ -302,64 +417,93 @@ namespace confighttp { // Don't do anything if file does not exist or is outside the assets directory if (!isChildPath(filePath, nodeModulesPath)) { BOOST_LOG(warning) << "Someone requested a path " << filePath << " that is outside the assets folder"; - response->write(SimpleWeb::StatusCode::client_error_bad_request, "Bad Request"); - } - else if (!fs::exists(filePath)) { - response->write(SimpleWeb::StatusCode::client_error_not_found); - } - else { - auto relPath = fs::relative(filePath, webDirPath); - // get the mime type from the file extension mime_types map - // remove the leading period from the extension - auto mimeType = mime_types.find(relPath.extension().string().substr(1)); - // check if the extension is in the map at the x position - if (mimeType != mime_types.end()) { - // if it is, set the content type to the mime type - SimpleWeb::CaseInsensitiveMultimap headers; - headers.emplace("Content-Type", mimeType->second); - std::ifstream in(filePath.string(), std::ios::binary); - response->write(SimpleWeb::StatusCode::success_ok, in, headers); - } - // do not return any file if the type is not in the map + bad_request(response, request); + return; + } + if (!fs::exists(filePath)) { + not_found(response, request); + return; } - } - - /** - * @brief Get the list of available applications. - * @param response The HTTP response object. - * @param request The HTTP request object. - */ - void - getApps(resp_https_t response, req_https_t request) { - if (!authenticate(response, request)) return; - print_req(request); + auto relPath = fs::relative(filePath, webDirPath); + // get the mime type from the file extension mime_types map + // remove the leading period from the extension + auto mimeType = mime_types.find(relPath.extension().string().substr(1)); + // check if the extension is in the map at the x position + if (mimeType == mime_types.end()) { + bad_request(response, request); + return; + } - std::string content = file_handler::read_file(config::stream.file_apps.c_str()); + // if it is, set the content type to the mime type SimpleWeb::CaseInsensitiveMultimap headers; - headers.emplace("Content-Type", "application/json"); - response->write(content, headers); + headers.emplace("Content-Type", mimeType->second); + std::ifstream in(filePath.string(), std::ios::binary); + response->write(SimpleWeb::StatusCode::success_ok, in, headers); } /** - * @brief Get the logs from the log file. + * @brief Get the list of available applications. * @param response The HTTP response object. * @param request The HTTP request object. + * + * @api_examples{/api/apps| GET| null} */ - void - getLogs(resp_https_t response, req_https_t request) { - if (!authenticate(response, request)) return; + void getApps(resp_https_t response, req_https_t request) { + if (!authenticate(response, request)) { + return; + } print_req(request); - std::string content = file_handler::read_file(config::sunshine.log_file.c_str()); - SimpleWeb::CaseInsensitiveMultimap headers; - headers.emplace("Content-Type", "text/plain"); - response->write(SimpleWeb::StatusCode::success_ok, content, headers); + try { + std::string content = file_handler::read_file(config::stream.file_apps.c_str()); + nlohmann::json file_tree = nlohmann::json::parse(content); + + // Legacy versions of Sunshine used strings for boolean and integers, let's convert them + // List of keys to convert to boolean + std::vector boolean_keys = { + "exclude-global-prep-cmd", + "elevated", + "auto-detach", + "wait-all" + }; + + // List of keys to convert to integers + std::vector integer_keys = { + "exit-timeout" + }; + + // Walk fileTree and convert true/false strings to boolean or integer values + for (auto &app : file_tree["apps"]) { + for (const auto &key : boolean_keys) { + if (app.contains(key) && app[key].is_string()) { + app[key] = app[key] == "true"; + } + } + for (const auto &key : integer_keys) { + if (app.contains(key) && app[key].is_string()) { + app[key] = std::stoi(app[key].get()); + } + } + if (app.contains("prep-cmd")) { + for (auto &prep : app["prep-cmd"]) { + if (prep.contains("elevated") && prep["elevated"].is_string()) { + prep["elevated"] = prep["elevated"] == "true"; + } + } + } + } + + send_response(response, file_tree); + } catch (std::exception &e) { + BOOST_LOG(warning) << "GetApps: "sv << e.what(); + bad_request(response, request, e.what()); + } } /** - * @brief Save an application. If the application already exists, it will be updated, otherwise it will be added. + * @brief Save an application. To save a new application the index must be `-1`. To update an existing application, you must provide the current index of the application. * @param response The HTTP response object. * @param request The HTTP request object. * The body for the post request should be JSON serialized in the following format: @@ -384,275 +528,269 @@ namespace confighttp { * "detached": [ * "Detached command" * ], - * "image-path": "Full path to the application image. Must be a png file.", + * "image-path": "Full path to the application image. Must be a png file." * } * @endcode + * + * @api_examples{/api/apps| POST| {"name":"Hello, World!","index":-1}} */ - void - saveApp(resp_https_t response, req_https_t request) { - if (!authenticate(response, request)) return; + void saveApp(resp_https_t response, req_https_t request) { + if (!authenticate(response, request)) { + return; + } print_req(request); std::stringstream ss; ss << request->content.rdbuf(); - - pt::ptree outputTree; - auto g = util::fail_guard([&]() { - std::ostringstream data; - - pt::write_json(data, outputTree); - response->write(data.str()); - }); - - pt::ptree inputTree, fileTree; - - BOOST_LOG(info) << config::stream.file_apps; try { // TODO: Input Validation - pt::read_json(ss, inputTree); - pt::read_json(config::stream.file_apps, fileTree); - - if (inputTree.get_child("prep-cmd").empty()) { - inputTree.erase("prep-cmd"); + nlohmann::json output_tree; + nlohmann::json input_tree = nlohmann::json::parse(ss); + std::string file = file_handler::read_file(config::stream.file_apps.c_str()); + BOOST_LOG(info) << file; + nlohmann::json file_tree = nlohmann::json::parse(file); + + if (input_tree["prep-cmd"].empty()) { + input_tree.erase("prep-cmd"); } - if (inputTree.get_child("detached").empty()) { - inputTree.erase("detached"); + if (input_tree["detached"].empty()) { + input_tree.erase("detached"); } - auto &apps_node = fileTree.get_child("apps"s); - int index = inputTree.get("index"); + auto &apps_node = file_tree["apps"]; + int index = input_tree["index"].get(); // this will intentionally cause exception if the provided value is the wrong type - inputTree.erase("index"); + input_tree.erase("index"); if (index == -1) { - apps_node.push_back(std::make_pair("", inputTree)); - } - else { - // Unfortunately Boost PT does not allow to directly edit the array, copy should do the trick - pt::ptree newApps; - int i = 0; - for (const auto &[k, v] : apps_node) { + apps_node.push_back(input_tree); + } else { + nlohmann::json newApps = nlohmann::json::array(); + for (size_t i = 0; i < apps_node.size(); ++i) { if (i == index) { - newApps.push_back(std::make_pair("", inputTree)); - } - else { - newApps.push_back(std::make_pair("", v)); + newApps.push_back(input_tree); + } else { + newApps.push_back(apps_node[i]); } - i++; } - fileTree.erase("apps"); - fileTree.push_back(std::make_pair("apps", newApps)); + file_tree["apps"] = newApps; } // Sort the apps array by name - std::vector apps_vector; - for (const auto &[k, v] : fileTree.get_child("apps")) { - apps_vector.push_back(v); - } - std::ranges::sort(apps_vector, [](const pt::ptree &a, const pt::ptree &b) { - return a.get("name") < b.get("name"); + std::sort(apps_node.begin(), apps_node.end(), [](const nlohmann::json &a, const nlohmann::json &b) { + return a["name"].get() < b["name"].get(); }); - pt::ptree sorted_apps; - for (const auto &app : apps_vector) { - sorted_apps.push_back(std::make_pair("", app)); - } - fileTree.erase("apps"); - fileTree.add_child("apps", sorted_apps); + file_handler::write_file(config::stream.file_apps.c_str(), file_tree.dump(4)); + proc::refresh(config::stream.file_apps); - pt::write_json(config::stream.file_apps, fileTree); - } - catch (std::exception &e) { + output_tree["status"] = true; + send_response(response, output_tree); + } catch (std::exception &e) { BOOST_LOG(warning) << "SaveApp: "sv << e.what(); + bad_request(response, request, e.what()); + } + } - outputTree.put("status", "false"); - outputTree.put("error", "Invalid Input JSON"); + /** + * @brief Close the currently running application. + * @param response The HTTP response object. + * @param request The HTTP request object. + * + * @api_examples{/api/apps/close| POST| null} + */ + void closeApp(resp_https_t response, req_https_t request) { + if (!authenticate(response, request)) { return; } - outputTree.put("status", "true"); - proc::refresh(config::stream.file_apps); + print_req(request); + + proc::proc.terminate(); + + nlohmann::json output_tree; + output_tree["status"] = true; + send_response(response, output_tree); } /** * @brief Delete an application. * @param response The HTTP response object. * @param request The HTTP request object. + * + * @api_examples{/api/apps/9999| DELETE| null} */ - void - deleteApp(resp_https_t response, req_https_t request) { - if (!authenticate(response, request)) return; + void deleteApp(resp_https_t response, req_https_t request) { + if (!authenticate(response, request)) { + return; + } print_req(request); - pt::ptree outputTree; - auto g = util::fail_guard([&]() { - std::ostringstream data; - - pt::write_json(data, outputTree); - response->write(data.str()); - }); - pt::ptree fileTree; try { - pt::read_json(config::stream.file_apps, fileTree); - auto &apps_node = fileTree.get_child("apps"s); - int index = stoi(request->path_match[1]); - - if (index < 0) { - outputTree.put("status", "false"); - outputTree.put("error", "Invalid Index"); + nlohmann::json output_tree; + nlohmann::json new_apps = nlohmann::json::array(); + std::string file = file_handler::read_file(config::stream.file_apps.c_str()); + nlohmann::json file_tree = nlohmann::json::parse(file); + auto &apps_node = file_tree["apps"]; + const int index = std::stoi(request->path_match[1]); + + if (index < 0 || index >= static_cast(apps_node.size())) { + std::string error; + 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); + } + bad_request(response, request, error); return; } - else { - // Unfortunately Boost PT does not allow to directly edit the array, copy should do the trick - pt::ptree newApps; - int i = 0; - for (const auto &[k, v] : apps_node) { - if (i++ != index) { - newApps.push_back(std::make_pair("", v)); - } + + for (size_t i = 0; i < apps_node.size(); ++i) { + if (i != index) { + new_apps.push_back(apps_node[i]); } - fileTree.erase("apps"); - fileTree.push_back(std::make_pair("apps", newApps)); } - pt::write_json(config::stream.file_apps, fileTree); - } - catch (std::exception &e) { + file_tree["apps"] = new_apps; + + file_handler::write_file(config::stream.file_apps.c_str(), file_tree.dump(4)); + proc::refresh(config::stream.file_apps); + + output_tree["status"] = true; + output_tree["result"] = "application " + std::to_string(index) + " deleted"; + send_response(response, output_tree); + } catch (std::exception &e) { BOOST_LOG(warning) << "DeleteApp: "sv << e.what(); - outputTree.put("status", "false"); - outputTree.put("error", "Invalid File JSON"); + bad_request(response, request, e.what()); + } + } + + /** + * @brief Get the list of paired clients. + * @param response The HTTP response object. + * @param request The HTTP request object. + * + * @api_examples{/api/clients/list| GET| null} + */ + void getClients(resp_https_t response, req_https_t request) { + if (!authenticate(response, request)) { return; } - outputTree.put("status", "true"); - proc::refresh(config::stream.file_apps); + print_req(request); + + const nlohmann::json named_certs = nvhttp::get_all_clients(); + + nlohmann::json output_tree; + output_tree["named_certs"] = named_certs; + output_tree["status"] = true; + send_response(response, output_tree); } /** - * @brief Upload a cover image. + * @brief Unpair a client. * @param response The HTTP response object. * @param request The HTTP request object. * The body for the post request should be JSON serialized in the following format: * @code{.json} * { - * "key": "igdb_", - * "url": "https://images.igdb.com/igdb/image/upload/t_cover_big_2x/.png", + * "uuid": "" * } * @endcode + * + * @api_examples{/api/unpair| POST| {"uuid":"1234"}} */ - void - uploadCover(resp_https_t response, req_https_t request) { - if (!authenticate(response, request)) return; + void unpair(resp_https_t response, req_https_t request) { + if (!authenticate(response, request)) { + return; + } + + print_req(request); std::stringstream ss; - std::stringstream configStream; ss << request->content.rdbuf(); - pt::ptree outputTree; - auto g = util::fail_guard([&]() { - std::ostringstream data; - SimpleWeb::StatusCode code = SimpleWeb::StatusCode::success_ok; - if (outputTree.get_child_optional("error").has_value()) { - code = SimpleWeb::StatusCode::client_error_bad_request; - } - - pt::write_json(data, outputTree); - response->write(code, data.str()); - }); - pt::ptree inputTree; try { - pt::read_json(ss, inputTree); - } - catch (std::exception &e) { - BOOST_LOG(warning) << "UploadCover: "sv << e.what(); - outputTree.put("status", "false"); - outputTree.put("error", e.what()); - return; + // TODO: Input Validation + nlohmann::json output_tree; + const nlohmann::json input_tree = nlohmann::json::parse(ss); + const std::string uuid = input_tree.value("uuid", ""); + output_tree["status"] = nvhttp::unpair_client(uuid); + send_response(response, output_tree); + } catch (std::exception &e) { + BOOST_LOG(warning) << "Unpair: "sv << e.what(); + bad_request(response, request, e.what()); } + } - auto key = inputTree.get("key", ""); - if (key.empty()) { - outputTree.put("error", "Cover key is required"); + /** + * @brief Unpair all clients. + * @param response The HTTP response object. + * @param request The HTTP request object. + * + * @api_examples{/api/clients/unpair-all| POST| null} + */ + void unpairAll(resp_https_t response, req_https_t request) { + if (!authenticate(response, request)) { return; } - auto url = inputTree.get("url", ""); - const std::string coverdir = platf::appdata().string() + "/covers/"; - file_handler::make_directory(coverdir); + print_req(request); - std::basic_string path = coverdir + http::url_escape(key) + ".png"; - if (!url.empty()) { - if (http::url_get_host(url) != "images.igdb.com") { - outputTree.put("error", "Only images.igdb.com is allowed"); - return; - } - if (!http::download_file(url, path)) { - outputTree.put("error", "Failed to download cover"); - return; - } - } - else { - auto data = SimpleWeb::Crypto::Base64::decode(inputTree.get("data")); + nvhttp::erase_all_clients(); + proc::proc.terminate(); - std::ofstream imgfile(path); - imgfile.write(data.data(), (int) data.size()); - } - outputTree.put("path", path); + nlohmann::json output_tree; + output_tree["status"] = true; + send_response(response, output_tree); } /** * @brief Get the configuration settings. * @param response The HTTP response object. * @param request The HTTP request object. + * + * @api_examples{/api/config| GET| null} */ - void - getConfig(resp_https_t response, req_https_t request) { - if (!authenticate(response, request)) return; + void getConfig(resp_https_t response, req_https_t request) { + if (!authenticate(response, request)) { + return; + } print_req(request); - pt::ptree outputTree; - auto g = util::fail_guard([&]() { - std::ostringstream data; - - pt::write_json(data, outputTree); - response->write(data.str()); - }); - - outputTree.put("status", "true"); - outputTree.put("platform", SUNSHINE_PLATFORM); - outputTree.put("version", PROJECT_VER); + nlohmann::json output_tree; + output_tree["status"] = true; + output_tree["platform"] = SUNSHINE_PLATFORM; + output_tree["version"] = PROJECT_VER; auto vars = config::parse_config(file_handler::read_file(config::sunshine.config_file.c_str())); for (auto &[name, value] : vars) { - outputTree.put(std::move(name), std::move(value)); + output_tree[name] = std::move(value); } + + send_response(response, output_tree); } /** * @brief Get the locale setting. This endpoint does not require authentication. * @param response The HTTP response object. * @param request The HTTP request object. + * + * @api_examples{/api/configLocale| GET| null} */ - void - getLocale(resp_https_t response, req_https_t request) { + void getLocale(resp_https_t response, req_https_t request) { // we need to return the locale whether authenticated or not print_req(request); - pt::ptree outputTree; - auto g = util::fail_guard([&]() { - std::ostringstream data; - - pt::write_json(data, outputTree); - response->write(data.str()); - }); - - outputTree.put("status", "true"); - outputTree.put("locale", config::sunshine.locale); + nlohmann::json output_tree; + output_tree["status"] = true; + output_tree["locale"] = config::sunshine.locale; + send_response(response, output_tree); } /** @@ -667,56 +805,119 @@ namespace confighttp { * @endcode * * @attention{It is recommended to ONLY save the config settings that differ from the default behavior.} + * + * @api_examples{/api/config| POST| {"key":"value"}} */ - void - saveConfig(resp_https_t response, req_https_t request) { - if (!authenticate(response, request)) return; + void saveConfig(resp_https_t response, req_https_t request) { + if (!authenticate(response, request)) { + return; + } print_req(request); std::stringstream ss; - std::stringstream configStream; ss << request->content.rdbuf(); - pt::ptree outputTree; - auto g = util::fail_guard([&]() { - std::ostringstream data; - - pt::write_json(data, outputTree); - response->write(data.str()); - }); - pt::ptree inputTree; try { // TODO: Input Validation - pt::read_json(ss, inputTree); - for (const auto &[k, v] : inputTree) { - std::string value = inputTree.get(k); - if (value.length() == 0 || value.compare("null") == 0) continue; + std::stringstream config_stream; + nlohmann::json output_tree; + nlohmann::json input_tree = nlohmann::json::parse(ss); + for (const auto &[k, v] : input_tree.items()) { + if (v.is_null() || (v.is_string() && v.get().empty())) { + continue; + } - configStream << k << " = " << value << std::endl; + // v.dump() will dump valid json, which we do not want for strings in the config right now + // we should migrate the config file to straight json and get rid of all this nonsense + config_stream << k << " = " << (v.is_string() ? v.get() : v.dump()) << std::endl; } - file_handler::write_file(config::sunshine.config_file.c_str(), configStream.str()); - } - catch (std::exception &e) { + file_handler::write_file(config::sunshine.config_file.c_str(), config_stream.str()); + output_tree["status"] = true; + send_response(response, output_tree); + } catch (std::exception &e) { BOOST_LOG(warning) << "SaveConfig: "sv << e.what(); - outputTree.put("status", "false"); - outputTree.put("error", e.what()); + bad_request(response, request, e.what()); + } + } + + /** + * @brief Upload a cover image. + * @param response The HTTP response object. + * @param request The HTTP request object. + * The body for the post request should be JSON serialized in the following format: + * @code{.json} + * { + * "key": "igdb_", + * "url": "https://images.igdb.com/igdb/image/upload/t_cover_big_2x/.png" + * } + * @endcode + * + * @api_examples{/api/covers/upload| POST| {"key":"igdb_1234","url":"https://images.igdb.com/igdb/image/upload/t_cover_big_2x/abc123.png"}} + */ + void uploadCover(resp_https_t response, req_https_t request) { + if (!authenticate(response, request)) { return; } + + std::stringstream ss; + ss << request->content.rdbuf(); + try { + nlohmann::json output_tree; + nlohmann::json input_tree = nlohmann::json::parse(ss); + + std::string key = input_tree.value("key", ""); + if (key.empty()) { + bad_request(response, request, "Cover key is required"); + return; + } + std::string url = input_tree.value("url", ""); + + const std::string coverdir = platf::appdata().string() + "/covers/"; + file_handler::make_directory(coverdir); + + std::basic_string path = coverdir + http::url_escape(key) + ".png"; + if (!url.empty()) { + if (http::url_get_host(url) != "images.igdb.com") { + bad_request(response, request, "Only images.igdb.com is allowed"); + return; + } + if (!http::download_file(url, path)) { + bad_request(response, request, "Failed to download cover"); + return; + } + } else { + auto data = SimpleWeb::Crypto::Base64::decode(input_tree.value("data", "")); + + std::ofstream imgfile(path); + imgfile.write(data.data(), static_cast(data.size())); + } + output_tree["status"] = true; + output_tree["path"] = path; + send_response(response, output_tree); + } catch (std::exception &e) { + BOOST_LOG(warning) << "UploadCover: "sv << e.what(); + bad_request(response, request, e.what()); + } } /** - * @brief Restart Sunshine. + * @brief Get the logs from the log file. * @param response The HTTP response object. * @param request The HTTP request object. + * + * @api_examples{/api/logs| GET| null} */ - void - restart(resp_https_t response, req_https_t request) { - if (!authenticate(response, request)) return; + void getLogs(resp_https_t response, req_https_t request) { + if (!authenticate(response, request)) { + return; + } print_req(request); - // We may not return from this call - platf::restart(); + std::string content = file_handler::read_file(config::sunshine.log_file.c_str()); + SimpleWeb::CaseInsensitiveMultimap headers; + headers.emplace("Content-Type", "text/plain"); + response->write(SimpleWeb::StatusCode::success_ok, content, headers); } /** @@ -733,62 +934,62 @@ namespace confighttp { * "confirmNewPassword": "Confirm New Password" * } * @endcode + * + * @api_examples{/api/password| POST| {"currentUsername":"admin","currentPassword":"admin","newUsername":"admin","newPassword":"admin","confirmNewPassword":"admin"}} */ - void - savePassword(resp_https_t response, req_https_t request) { - if (!config::sunshine.username.empty() && !authenticate(response, request)) return; + void savePassword(resp_https_t response, req_https_t request) { + if (!config::sunshine.username.empty() && !authenticate(response, request)) { + return; + } print_req(request); + std::vector errors = {}; std::stringstream ss; - std::stringstream configStream; + std::stringstream config_stream; ss << request->content.rdbuf(); - - pt::ptree inputTree, outputTree; - - auto g = util::fail_guard([&]() { - std::ostringstream data; - pt::write_json(data, outputTree); - response->write(data.str()); - }); - try { // TODO: Input Validation - pt::read_json(ss, inputTree); - auto username = inputTree.count("currentUsername") > 0 ? inputTree.get("currentUsername") : ""; - auto newUsername = inputTree.get("newUsername"); - auto password = inputTree.count("currentPassword") > 0 ? inputTree.get("currentPassword") : ""; - auto newPassword = inputTree.count("newPassword") > 0 ? inputTree.get("newPassword") : ""; - auto confirmPassword = inputTree.count("confirmNewPassword") > 0 ? inputTree.get("confirmNewPassword") : ""; - if (newUsername.length() == 0) newUsername = username; - if (newUsername.length() == 0) { - outputTree.put("status", false); - outputTree.put("error", "Invalid Username"); + nlohmann::json output_tree; + nlohmann::json input_tree = nlohmann::json::parse(ss); + std::string username = input_tree.value("currentUsername", ""); + std::string newUsername = input_tree.value("newUsername", ""); + std::string password = input_tree.value("currentPassword", ""); + std::string newPassword = input_tree.value("newPassword", ""); + std::string confirmPassword = input_tree.value("confirmNewPassword", ""); + if (newUsername.empty()) { + newUsername = username; } - else { + if (newUsername.empty()) { + errors.emplace_back("Invalid Username"); + } else { auto hash = util::hex(crypto::hash(password + config::sunshine.salt)).to_string(); if (config::sunshine.username.empty() || (boost::iequals(username, config::sunshine.username) && hash == config::sunshine.password)) { if (newPassword.empty() || newPassword != confirmPassword) { - outputTree.put("status", false); - outputTree.put("error", "Password Mismatch"); - } - else { + errors.emplace_back("Password Mismatch"); + } else { http::save_user_creds(config::sunshine.credentials_file, newUsername, newPassword); http::reload_user_creds(config::sunshine.credentials_file); - outputTree.put("status", true); + output_tree["status"] = true; } - } - else { - outputTree.put("status", false); - outputTree.put("error", "Invalid Current Credentials"); + } else { + errors.emplace_back("Invalid Current Credentials"); } } - } - catch (std::exception &e) { + + if (!errors.empty()) { + // join the errors array + std::string error = std::accumulate(errors.begin(), errors.end(), std::string(), [](const std::string &a, const std::string &b) { + return a.empty() ? b : a + ", " + b; + }); + bad_request(response, request, error); + return; + } + + send_response(response, output_tree); + } catch (std::exception &e) { BOOST_LOG(warning) << "SavePassword: "sv << e.what(); - outputTree.put("status", false); - outputTree.put("error", e.what()); - return; + bad_request(response, request, e.what()); } } @@ -803,162 +1004,94 @@ namespace confighttp { * "name": "Friendly Client Name" * } * @endcode + * + * @api_examples{/api/pin| POST| {"pin":"1234","name":"My PC"}} */ - void - savePin(resp_https_t response, req_https_t request) { - if (!authenticate(response, request)) return; + void savePin(resp_https_t response, req_https_t request) { + if (!authenticate(response, request)) { + return; + } print_req(request); std::stringstream ss; ss << request->content.rdbuf(); - - pt::ptree inputTree, outputTree; - - auto g = util::fail_guard([&]() { - std::ostringstream data; - pt::write_json(data, outputTree); - response->write(data.str()); - }); - try { - // TODO: Input Validation - pt::read_json(ss, inputTree); - std::string pin = inputTree.get("pin"); - std::string name = inputTree.get("name"); - outputTree.put("status", nvhttp::pin(pin, name)); - } - catch (std::exception &e) { + nlohmann::json output_tree; + nlohmann::json input_tree = nlohmann::json::parse(ss); + const std::string name = input_tree.value("name", ""); + const std::string pin = input_tree.value("pin", ""); + + int _pin = 0; + _pin = std::stoi(pin); + if (_pin < 0 || _pin > 9999) { + bad_request(response, request, "PIN must be between 0000 and 9999"); + } + + output_tree["status"] = nvhttp::pin(pin, name); + send_response(response, output_tree); + } catch (std::exception &e) { BOOST_LOG(warning) << "SavePin: "sv << e.what(); - outputTree.put("status", false); - outputTree.put("error", e.what()); - return; + bad_request(response, request, e.what()); } } /** - * @brief Unpair all clients. + * @brief Reset the display device persistence. * @param response The HTTP response object. * @param request The HTTP request object. + * + * @api_examples{/api/reset-display-device-persistence| POST| null} */ - void - unpairAll(resp_https_t response, req_https_t request) { - if (!authenticate(response, request)) return; - - print_req(request); - - pt::ptree outputTree; - - auto g = util::fail_guard([&]() { - std::ostringstream data; - pt::write_json(data, outputTree); - response->write(data.str()); - }); - nvhttp::erase_all_clients(); - proc::proc.terminate(); - outputTree.put("status", true); - } - - /** - * @brief Unpair a client. - * @param response The HTTP response object. - * @param request The HTTP request object. - * The body for the post request should be JSON serialized in the following format: - * @code{.json} - * { - * "uuid": "" - * } - * @endcode - */ - void - unpair(resp_https_t response, req_https_t request) { - if (!authenticate(response, request)) return; - - print_req(request); - - std::stringstream ss; - ss << request->content.rdbuf(); - - pt::ptree inputTree, outputTree; - - auto g = util::fail_guard([&]() { - std::ostringstream data; - pt::write_json(data, outputTree); - response->write(data.str()); - }); - - try { - // TODO: Input Validation - pt::read_json(ss, inputTree); - std::string uuid = inputTree.get("uuid"); - outputTree.put("status", nvhttp::unpair_client(uuid)); - } - catch (std::exception &e) { - BOOST_LOG(warning) << "Unpair: "sv << e.what(); - outputTree.put("status", false); - outputTree.put("error", e.what()); + void resetDisplayDevicePersistence(resp_https_t response, req_https_t request) { + if (!authenticate(response, request)) { return; } - } - - /** - * @brief Get the list of paired clients. - * @param response The HTTP response object. - * @param request The HTTP request object. - */ - void - listClients(resp_https_t response, req_https_t request) { - if (!authenticate(response, request)) return; print_req(request); - pt::ptree named_certs = nvhttp::get_all_clients(); - - pt::ptree outputTree; - - outputTree.put("status", false); - - auto g = util::fail_guard([&]() { - std::ostringstream data; - pt::write_json(data, outputTree); - response->write(data.str()); - }); - - outputTree.add_child("named_certs", named_certs); - outputTree.put("status", true); + nlohmann::json output_tree; + output_tree["status"] = display_device::reset_persistence(); + send_response(response, output_tree); } /** - * @brief Close the currently running application. + * @brief Restart Sunshine. * @param response The HTTP response object. * @param request The HTTP request object. + * + * @api_examples{/api/restart| POST| null} */ - void - closeApp(resp_https_t response, req_https_t request) { - if (!authenticate(response, request)) return; + void restart(resp_https_t response, req_https_t request) { + if (!authenticate(response, request)) { + return; + } print_req(request); - pt::ptree outputTree; - - auto g = util::fail_guard([&]() { - std::ostringstream data; - pt::write_json(data, outputTree); - response->write(data.str()); - }); - - proc::proc.terminate(); - outputTree.put("status", true); + // We may not return from this call + platf::restart(); } - void - start() { + void start() { auto shutdown_event = mail::man->event(mail::shutdown); auto port_https = net::map_port(PORT_HTTPS); auto address_family = net::af_from_enum_string(config::sunshine.address_family); - https_server_t server { config::nvhttp.cert, config::nvhttp.pkey }; + https_server_t server {config::nvhttp.cert, config::nvhttp.pkey}; + server.default_resource["DELETE"] = [](resp_https_t response, req_https_t request) { + bad_request(response, request); + }; + server.default_resource["PATCH"] = [](resp_https_t response, req_https_t request) { + bad_request(response, request); + }; + server.default_resource["POST"] = [](resp_https_t response, req_https_t request) { + bad_request(response, request); + }; + server.default_resource["PUT"] = [](resp_https_t response, req_https_t request) { + bad_request(response, request); + }; server.default_resource["GET"] = not_found; server.resource["^/$"]["GET"] = getIndexPage; server.resource["^/pin/?$"]["GET"] = getPinPage; @@ -976,10 +1109,11 @@ namespace confighttp { server.resource["^/api/config$"]["POST"] = saveConfig; server.resource["^/api/configLocale$"]["GET"] = getLocale; server.resource["^/api/restart$"]["POST"] = restart; + server.resource["^/api/reset-display-device-persistence$"]["POST"] = resetDisplayDevicePersistence; server.resource["^/api/password$"]["POST"] = savePassword; server.resource["^/api/apps/([0-9]+)$"]["DELETE"] = deleteApp; server.resource["^/api/clients/unpair-all$"]["POST"] = unpairAll; - server.resource["^/api/clients/list$"]["GET"] = listClients; + server.resource["^/api/clients/list$"]["GET"] = getClients; server.resource["^/api/clients/unpair$"]["POST"] = unpair; server.resource["^/api/apps/close$"]["POST"] = closeApp; server.resource["^/api/covers/upload$"]["POST"] = uploadCover; @@ -995,8 +1129,7 @@ namespace confighttp { server->start([](unsigned short port) { BOOST_LOG(info) << "Configuration UI available at [https://localhost:"sv << port << "]"; }); - } - catch (boost::system::system_error &err) { + } catch (boost::system::system_error &err) { // It's possible the exception gets thrown after calling server->stop() from a different thread if (shutdown_event->peek()) { return; @@ -1007,7 +1140,7 @@ namespace confighttp { return; } }; - std::thread tcp { accept_and_run, &server }; + std::thread tcp {accept_and_run, &server}; // Wait for any event shutdown_event->view(); diff --git a/src/confighttp.h b/src/confighttp.h index db202cb665b..9925603927c 100644 --- a/src/confighttp.h +++ b/src/confighttp.h @@ -4,34 +4,34 @@ */ #pragma once -#include +// standard includes #include +// local includes #include "thread_safe.h" #define WEB_DIR SUNSHINE_ASSETS_DIR "/web/" namespace confighttp { constexpr auto PORT_HTTPS = 1; - void - start(); + void start(); } // namespace confighttp // mime types map const std::map mime_types = { - { "css", "text/css" }, - { "gif", "image/gif" }, - { "htm", "text/html" }, - { "html", "text/html" }, - { "ico", "image/x-icon" }, - { "jpeg", "image/jpeg" }, - { "jpg", "image/jpeg" }, - { "js", "application/javascript" }, - { "json", "application/json" }, - { "png", "image/png" }, - { "svg", "image/svg+xml" }, - { "ttf", "font/ttf" }, - { "txt", "text/plain" }, - { "woff2", "font/woff2" }, - { "xml", "text/xml" }, + {"css", "text/css"}, + {"gif", "image/gif"}, + {"htm", "text/html"}, + {"html", "text/html"}, + {"ico", "image/x-icon"}, + {"jpeg", "image/jpeg"}, + {"jpg", "image/jpeg"}, + {"js", "application/javascript"}, + {"json", "application/json"}, + {"png", "image/png"}, + {"svg", "image/svg+xml"}, + {"ttf", "font/ttf"}, + {"txt", "text/plain"}, + {"woff2", "font/woff2"}, + {"xml", "text/xml"}, }; diff --git a/src/crypto.cpp b/src/crypto.cpp index 2b30a3bc54b..a6f326e7011 100644 --- a/src/crypto.cpp +++ b/src/crypto.cpp @@ -2,29 +2,33 @@ * @file src/crypto.cpp * @brief Definitions for cryptography functions. */ -#include "crypto.h" +// lib includes #include #include +// local includes +#include "crypto.h" + namespace crypto { using asn1_string_t = util::safe_ptr; cert_chain_t::cert_chain_t(): - _certs {}, _cert_ctx { X509_STORE_CTX_new() } {} - void - cert_chain_t::add(x509_t &&cert) { - x509_store_t x509_store { X509_STORE_new() }; + _certs {}, + _cert_ctx {X509_STORE_CTX_new()} { + } + + void cert_chain_t::add(x509_t &&cert) { + x509_store_t x509_store {X509_STORE_new()}; X509_STORE_add_cert(x509_store.get(), cert.get()); _certs.emplace_back(std::make_pair(std::move(cert), std::move(x509_store))); } - void - cert_chain_t::clear() { + + void cert_chain_t::clear() { _certs.clear(); } - static int - openssl_verify_cb(int ok, X509_STORE_CTX *ctx) { + static int openssl_verify_cb(int ok, X509_STORE_CTX *ctx) { int err_code = X509_STORE_CTX_get_error(ctx); switch (err_code) { @@ -52,8 +56,7 @@ namespace crypto { * @param cert The certificate to verify. * @return nullptr if the certificate is valid, otherwise an error string. */ - const char * - cert_chain_t::verify(x509_t::element_type *cert) { + const char *cert_chain_t::verify(x509_t::element_type *cert) { int err_code = 0; for (auto &[_, x509_store] : _certs) { auto fg = util::fail_guard([this]() { @@ -86,8 +89,7 @@ namespace crypto { namespace cipher { - static int - init_decrypt_gcm(cipher_ctx_t &ctx, aes_t *key, aes_t *iv, bool padding) { + static int init_decrypt_gcm(cipher_ctx_t &ctx, aes_t *key, aes_t *iv, bool padding) { ctx.reset(EVP_CIPHER_CTX_new()); if (!ctx) { @@ -110,8 +112,7 @@ namespace crypto { return 0; } - static int - init_encrypt_gcm(cipher_ctx_t &ctx, aes_t *key, aes_t *iv, bool padding) { + static int init_encrypt_gcm(cipher_ctx_t &ctx, aes_t *key, aes_t *iv, bool padding) { ctx.reset(EVP_CIPHER_CTX_new()); // Gen 7 servers use 128-bit AES ECB @@ -131,8 +132,7 @@ namespace crypto { return 0; } - static int - init_encrypt_cbc(cipher_ctx_t &ctx, aes_t *key, aes_t *iv, bool padding) { + static int init_encrypt_cbc(cipher_ctx_t &ctx, aes_t *key, aes_t *iv, bool padding) { ctx.reset(EVP_CIPHER_CTX_new()); // Gen 7 servers use 128-bit AES ECB @@ -145,8 +145,7 @@ namespace crypto { return 0; } - int - gcm_t::decrypt(const std::string_view &tagged_cipher, std::vector &plaintext, aes_t *iv) { + int gcm_t::decrypt(const std::string_view &tagged_cipher, std::vector &plaintext, aes_t *iv) { if (!decrypt_ctx && init_decrypt_gcm(decrypt_ctx, &key, iv, padding)) { return -1; } @@ -185,8 +184,7 @@ namespace crypto { * The function handles the creation and initialization of the encryption context, and manages the encryption process. * The resulting ciphertext and the GCM tag are written into the tagged_cipher buffer. */ - int - gcm_t::encrypt(const std::string_view &plaintext, std::uint8_t *tag, std::uint8_t *ciphertext, aes_t *iv) { + int gcm_t::encrypt(const std::string_view &plaintext, std::uint8_t *tag, std::uint8_t *ciphertext, aes_t *iv) { if (!encrypt_ctx && init_encrypt_gcm(encrypt_ctx, &key, iv, padding)) { return -1; } @@ -216,14 +214,12 @@ namespace crypto { return update_outlen + final_outlen; } - int - gcm_t::encrypt(const std::string_view &plaintext, std::uint8_t *tagged_cipher, aes_t *iv) { + int gcm_t::encrypt(const std::string_view &plaintext, std::uint8_t *tagged_cipher, aes_t *iv) { // This overload handles the common case of [GCM tag][cipher text] buffer layout return encrypt(plaintext, tagged_cipher, tagged_cipher + tag_size, iv); } - int - ecb_t::decrypt(const std::string_view &cipher, std::vector &plaintext) { + int ecb_t::decrypt(const std::string_view &cipher, std::vector &plaintext) { auto fg = util::fail_guard([this]() { EVP_CIPHER_CTX_reset(decrypt_ctx.get()); }); @@ -250,8 +246,7 @@ namespace crypto { return 0; } - int - ecb_t::encrypt(const std::string_view &plaintext, std::vector &cipher) { + int ecb_t::encrypt(const std::string_view &plaintext, std::vector &cipher) { auto fg = util::fail_guard([this]() { EVP_CIPHER_CTX_reset(encrypt_ctx.get()); }); @@ -284,8 +279,7 @@ namespace crypto { * The function handles the creation and initialization of the encryption context, and manages the encryption process. * The resulting ciphertext is written into the cipher buffer. */ - int - cbc_t::encrypt(const std::string_view &plaintext, std::uint8_t *cipher, aes_t *iv) { + int cbc_t::encrypt(const std::string_view &plaintext, std::uint8_t *cipher, aes_t *iv) { if (!encrypt_ctx && init_encrypt_cbc(encrypt_ctx, &key, iv, padding)) { return -1; } @@ -311,18 +305,20 @@ namespace crypto { } ecb_t::ecb_t(const aes_t &key, bool padding): - cipher_t { EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_new(), key, padding } {} + cipher_t {EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_new(), key, padding} { + } cbc_t::cbc_t(const aes_t &key, bool padding): - cipher_t { nullptr, nullptr, key, padding } {} + cipher_t {nullptr, nullptr, key, padding} { + } gcm_t::gcm_t(const crypto::aes_t &key, bool padding): - cipher_t { nullptr, nullptr, key, padding } {} + cipher_t {nullptr, nullptr, key, padding} { + } } // namespace cipher - aes_t - gen_aes_key(const std::array &salt, const std::string_view &pin) { + aes_t gen_aes_key(const std::array &salt, const std::string_view &pin) { aes_t key(16); std::string salt_pin; @@ -338,16 +334,14 @@ namespace crypto { return key; } - sha256_t - hash(const std::string_view &plaintext) { + sha256_t hash(const std::string_view &plaintext) { sha256_t hsh; EVP_Digest(plaintext.data(), plaintext.size(), hsh.data(), nullptr, EVP_sha256(), nullptr); return hsh; } - x509_t - x509(const std::string_view &x) { - bio_t io { BIO_new(BIO_s_mem()) }; + x509_t x509(const std::string_view &x) { + bio_t io {BIO_new(BIO_s_mem())}; BIO_write(io.get(), x.data(), x.size()); @@ -357,9 +351,8 @@ namespace crypto { return p; } - pkey_t - pkey(const std::string_view &k) { - bio_t io { BIO_new(BIO_s_mem()) }; + pkey_t pkey(const std::string_view &k) { + bio_t io {BIO_new(BIO_s_mem())}; BIO_write(io.get(), k.data(), k.size()); @@ -369,40 +362,36 @@ namespace crypto { return p; } - std::string - pem(x509_t &x509) { - bio_t bio { BIO_new(BIO_s_mem()) }; + std::string pem(x509_t &x509) { + bio_t bio {BIO_new(BIO_s_mem())}; PEM_write_bio_X509(bio.get(), x509.get()); BUF_MEM *mem_ptr; BIO_get_mem_ptr(bio.get(), &mem_ptr); - return { mem_ptr->data, mem_ptr->length }; + return {mem_ptr->data, mem_ptr->length}; } - std::string - pem(pkey_t &pkey) { - bio_t bio { BIO_new(BIO_s_mem()) }; + std::string pem(pkey_t &pkey) { + bio_t bio {BIO_new(BIO_s_mem())}; PEM_write_bio_PrivateKey(bio.get(), pkey.get(), nullptr, nullptr, 0, nullptr, nullptr); BUF_MEM *mem_ptr; BIO_get_mem_ptr(bio.get(), &mem_ptr); - return { mem_ptr->data, mem_ptr->length }; + return {mem_ptr->data, mem_ptr->length}; } - std::string_view - signature(const x509_t &x) { + std::string_view signature(const x509_t &x) { // X509_ALGOR *_ = nullptr; const ASN1_BIT_STRING *asn1 = nullptr; X509_get0_signature(&asn1, nullptr, x.get()); - return { (const char *) asn1->data, (std::size_t) asn1->length }; + return {(const char *) asn1->data, (std::size_t) asn1->length}; } - std::string - rand(std::size_t bytes) { + std::string rand(std::size_t bytes) { std::string r; r.resize(bytes); @@ -411,9 +400,8 @@ namespace crypto { return r; } - std::vector - sign(const pkey_t &pkey, const std::string_view &data, const EVP_MD *md) { - md_ctx_t ctx { EVP_MD_CTX_create() }; + std::vector sign(const pkey_t &pkey, const std::string_view &data, const EVP_MD *md) { + md_ctx_t ctx {EVP_MD_CTX_create()}; if (EVP_DigestSignInit(ctx.get(), nullptr, md, nullptr, (EVP_PKEY *) pkey.get()) != 1) { return {}; @@ -436,10 +424,9 @@ namespace crypto { return digest; } - creds_t - gen_creds(const std::string_view &cn, std::uint32_t key_bits) { - x509_t x509 { X509_new() }; - pkey_ctx_t ctx { EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, nullptr) }; + creds_t gen_creds(const std::string_view &cn, std::uint32_t key_bits) { + x509_t x509 {X509_new()}; + pkey_ctx_t ctx {EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, nullptr)}; pkey_t pkey; EVP_PKEY_keygen_init(ctx.get()); @@ -449,7 +436,7 @@ namespace crypto { X509_set_version(x509.get(), 2); // Generate a real serial number to avoid SEC_ERROR_REUSED_ISSUER_AND_SERIAL with Firefox - bignum_t serial { BN_new() }; + bignum_t serial {BN_new()}; BN_rand(serial.get(), 159, BN_RAND_TOP_ANY, BN_RAND_BOTTOM_ANY); // 159 bits to fit in 20 bytes in DER format BN_set_negative(serial.get(), 0); // Serial numbers must be positive BN_to_ASN1_INTEGER(serial.get(), X509_get_serialNumber(x509.get())); @@ -459,8 +446,8 @@ namespace crypto { X509_gmtime_adj(X509_get_notBefore(x509.get()), 0); X509_gmtime_adj(X509_get_notAfter(x509.get()), 20 * year); #else - asn1_string_t not_before { ASN1_STRING_dup(X509_get0_notBefore(x509.get())) }; - asn1_string_t not_after { ASN1_STRING_dup(X509_get0_notAfter(x509.get())) }; + asn1_string_t not_before {ASN1_STRING_dup(X509_get0_notBefore(x509.get()))}; + asn1_string_t not_after {ASN1_STRING_dup(X509_get0_notAfter(x509.get()))}; X509_gmtime_adj(not_before.get(), 0); X509_gmtime_adj(not_after.get(), 20 * year); @@ -472,26 +459,22 @@ namespace crypto { X509_set_pubkey(x509.get(), pkey.get()); auto name = X509_get_subject_name(x509.get()); - X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, - (const std::uint8_t *) cn.data(), cn.size(), - -1, 0); + X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, (const std::uint8_t *) cn.data(), cn.size(), -1, 0); X509_set_issuer_name(x509.get(), name); X509_sign(x509.get(), pkey.get(), EVP_sha256()); - return { pem(x509), pem(pkey) }; + return {pem(x509), pem(pkey)}; } - std::vector - sign256(const pkey_t &pkey, const std::string_view &data) { + std::vector sign256(const pkey_t &pkey, const std::string_view &data) { return sign(pkey, data, EVP_sha256()); } - bool - verify(const x509_t &x509, const std::string_view &data, const std::string_view &signature, const EVP_MD *md) { + bool verify(const x509_t &x509, const std::string_view &data, const std::string_view &signature, const EVP_MD *md) { auto pkey = X509_get0_pubkey(x509.get()); - md_ctx_t ctx { EVP_MD_CTX_create() }; + md_ctx_t ctx {EVP_MD_CTX_create()}; if (EVP_DigestVerifyInit(ctx.get(), nullptr, md, nullptr, pkey) != 1) { return false; @@ -508,18 +491,15 @@ namespace crypto { return true; } - bool - verify256(const x509_t &x509, const std::string_view &data, const std::string_view &signature) { + bool verify256(const x509_t &x509, const std::string_view &data, const std::string_view &signature) { return verify(x509, data, signature, EVP_sha256()); } - void - md_ctx_destroy(EVP_MD_CTX *ctx) { + void md_ctx_destroy(EVP_MD_CTX *ctx) { EVP_MD_CTX_destroy(ctx); } - std::string - rand_alphabet(std::size_t bytes, const std::string_view &alphabet) { + std::string rand_alphabet(std::size_t bytes, const std::string_view &alphabet) { auto value = rand(bytes); for (std::size_t i = 0; i != value.size(); ++i) { diff --git a/src/crypto.h b/src/crypto.h index 859c6675e25..944cd77e279 100644 --- a/src/crypto.h +++ b/src/crypto.h @@ -4,12 +4,16 @@ */ #pragma once +// standard includes #include + +// lib includes #include #include #include #include +// local includes #include "utility.h" namespace crypto { @@ -18,8 +22,7 @@ namespace crypto { std::string pkey; }; - void - md_ctx_destroy(EVP_MD_CTX *); + void md_ctx_destroy(EVP_MD_CTX *); using sha256_t = std::array; @@ -39,50 +42,33 @@ namespace crypto { * @param plaintext * @return The SHA-256 hash of the plaintext. */ - sha256_t - hash(const std::string_view &plaintext); - - aes_t - gen_aes_key(const std::array &salt, const std::string_view &pin); - - x509_t - x509(const std::string_view &x); - pkey_t - pkey(const std::string_view &k); - std::string - pem(x509_t &x509); - std::string - pem(pkey_t &pkey); - - std::vector - sign256(const pkey_t &pkey, const std::string_view &data); - bool - verify256(const x509_t &x509, const std::string_view &data, const std::string_view &signature); - - creds_t - gen_creds(const std::string_view &cn, std::uint32_t key_bits); - - std::string_view - signature(const x509_t &x); - - std::string - rand(std::size_t bytes); - std::string - rand_alphabet(std::size_t bytes, - const std::string_view &alphabet = std::string_view { "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!%&()=-" }); + sha256_t hash(const std::string_view &plaintext); + + aes_t gen_aes_key(const std::array &salt, const std::string_view &pin); + x509_t x509(const std::string_view &x); + pkey_t pkey(const std::string_view &k); + std::string pem(x509_t &x509); + std::string pem(pkey_t &pkey); + + std::vector sign256(const pkey_t &pkey, const std::string_view &data); + bool verify256(const x509_t &x509, const std::string_view &data, const std::string_view &signature); + + creds_t gen_creds(const std::string_view &cn, std::uint32_t key_bits); + + std::string_view signature(const x509_t &x); + + std::string rand(std::size_t bytes); + std::string rand_alphabet(std::size_t bytes, const std::string_view &alphabet = std::string_view {"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!%&()=-"}); class cert_chain_t { public: KITTY_DECL_CONSTR(cert_chain_t) - void - add(x509_t &&cert); + void add(x509_t &&cert); - void - clear(); + void clear(); - const char * - verify(x509_t::element_type *cert); + const char *verify(x509_t::element_type *cert); private: std::vector> _certs; @@ -91,8 +77,8 @@ namespace crypto { namespace cipher { constexpr std::size_t tag_size = 16; - constexpr std::size_t - round_to_pkcs7_padded(std::size_t size) { + + constexpr std::size_t round_to_pkcs7_padded(std::size_t size) { return ((size + 15) / 16) * 16; } @@ -110,23 +96,19 @@ namespace crypto { public: ecb_t() = default; ecb_t(ecb_t &&) noexcept = default; - ecb_t & - operator=(ecb_t &&) noexcept = default; + ecb_t &operator=(ecb_t &&) noexcept = default; ecb_t(const aes_t &key, bool padding = true); - int - encrypt(const std::string_view &plaintext, std::vector &cipher); - int - decrypt(const std::string_view &cipher, std::vector &plaintext); + int encrypt(const std::string_view &plaintext, std::vector &cipher); + int decrypt(const std::string_view &cipher, std::vector &plaintext); }; class gcm_t: public cipher_t { public: gcm_t() = default; gcm_t(gcm_t &&) noexcept = default; - gcm_t & - operator=(gcm_t &&) noexcept = default; + gcm_t &operator=(gcm_t &&) noexcept = default; gcm_t(const crypto::aes_t &key, bool padding = true); @@ -138,8 +120,7 @@ namespace crypto { * @param iv The initialization vector to be used for the encryption. * @return The total length of the ciphertext and GCM tag. Returns -1 in case of an error. */ - int - encrypt(const std::string_view &plaintext, std::uint8_t *tag, std::uint8_t *ciphertext, aes_t *iv); + int encrypt(const std::string_view &plaintext, std::uint8_t *tag, std::uint8_t *ciphertext, aes_t *iv); /** * @brief Encrypts the plaintext using AES GCM mode. @@ -149,19 +130,16 @@ namespace crypto { * @param iv The initialization vector to be used for the encryption. * @return The total length of the ciphertext and GCM tag written into tagged_cipher. Returns -1 in case of an error. */ - int - encrypt(const std::string_view &plaintext, std::uint8_t *tagged_cipher, aes_t *iv); + int encrypt(const std::string_view &plaintext, std::uint8_t *tagged_cipher, aes_t *iv); - int - decrypt(const std::string_view &cipher, std::vector &plaintext, aes_t *iv); + int decrypt(const std::string_view &cipher, std::vector &plaintext, aes_t *iv); }; class cbc_t: public cipher_t { public: cbc_t() = default; cbc_t(cbc_t &&) noexcept = default; - cbc_t & - operator=(cbc_t &&) noexcept = default; + cbc_t &operator=(cbc_t &&) noexcept = default; cbc_t(const crypto::aes_t &key, bool padding = true); @@ -173,8 +151,7 @@ namespace crypto { * @param iv The initialization vector to be used for the encryption. * @return The total length of the ciphertext written into cipher. Returns -1 in case of an error. */ - int - encrypt(const std::string_view &plaintext, std::uint8_t *cipher, aes_t *iv); + int encrypt(const std::string_view &plaintext, std::uint8_t *cipher, aes_t *iv); }; } // namespace cipher } // namespace crypto diff --git a/src/display_device.cpp b/src/display_device.cpp new file mode 100644 index 00000000000..7988cba58a6 --- /dev/null +++ b/src/display_device.cpp @@ -0,0 +1,853 @@ +/** + * @file src/display_device.cpp + * @brief Definitions for display device handling. + */ +// header include +#include "display_device.h" + +// lib includes +#include +#include +#include +#include +#include +#include +#include +#include + +// local includes +#include "audio.h" +#include "platform/common.h" +#include "rtsp.h" + +// platform-specific includes +#ifdef _WIN32 + #include + #include + #include +#endif + +namespace display_device { + namespace { + constexpr std::chrono::milliseconds DEFAULT_RETRY_INTERVAL {5000}; + + /** + * @brief A global for the settings manager interface and other settings whose lifetime is managed by `display_device::init(...)`. + */ + struct { + std::mutex mutex {}; + std::chrono::milliseconds config_revert_delay {0}; + std::unique_ptr> sm_instance {nullptr}; + } DD_DATA; + + /** + * @brief Helper class for capturing audio context when the API demands it. + * + * The capture is needed to be done in case some of the displays are going + * to be deactivated before the stream starts. In this case the audio context + * will be captured for this display and can be restored once it is turned back. + */ + class sunshine_audio_context_t: public AudioContextInterface { + public: + [[nodiscard]] bool capture() override { + return context_scheduler.execute([](auto &audio_context) { + // Explicitly releasing the context first in case it was not release yet so that it can be potentially cleaned up. + audio_context = boost::none; + audio_context = audio_context_t {}; + + // Always say that we have captured it successfully as otherwise the settings change procedure will be aborted. + return true; + }); + } + + [[nodiscard]] bool isCaptured() const override { + return context_scheduler.execute([](const auto &audio_context) { + if (audio_context) { + // In case we still have context we need to check whether it was released or not. + // If it was released we can pretend that we no longer have it as it will be immediately cleaned up in `capture` method before we acquire new context. + return !audio_context->released; + } + + return false; + }); + } + + void release() override { + context_scheduler.schedule([](auto &audio_context, auto &stop_token) { + if (audio_context) { + audio_context->released = true; + + const auto *audio_ctx_ptr = audio_context->audio_ctx_ref.get(); + if (audio_ctx_ptr && !audio::is_audio_ctx_sink_available(*audio_ctx_ptr) && audio_context->retry_counter > 0) { + // It is possible that the audio sink is not immediately available after the display is turned on. + // Therefore, we will hold on to the audio context a little longer, until it is either available + // or we time out. + --audio_context->retry_counter; + return; + } + } + + audio_context = boost::none; + stop_token.requestStop(); + }, + SchedulerOptions {.m_sleep_durations = {2s}}); + } + + private: + struct audio_context_t { + /** + * @brief A reference to the audio context that will automatically extend the audio session. + * @note It is auto-initialized here for convenience. + */ + decltype(audio::get_audio_ctx_ref()) audio_ctx_ref {audio::get_audio_ctx_ref()}; + + /** + * @brief Will be set to true if the capture was released, but we still have to keep the context around, because the device is not available. + */ + bool released {false}; + + /** + * @brief How many times to check if the audio sink is available before giving up. + */ + int retry_counter {15}; + }; + + RetryScheduler> context_scheduler {std::make_unique>(boost::none)}; + }; + + /** + * @brief Convert string to unsigned int. + * @note For random reason there is std::stoi, but not std::stou... + * @param value String to be converted + * @return Parsed unsigned integer. + */ + unsigned int stou(const std::string &value) { + unsigned long result {std::stoul(value)}; + if (result > std::numeric_limits::max()) { + throw std::out_of_range("stou"); + } + return result; + } + + /** + * @brief Parse resolution value from the string. + * @param input String to be parsed. + * @param output Reference to output variable to fill in. + * @returns True on successful parsing (empty string allowed), false otherwise. + * + * @examples + * std::optional resolution; + * if (parse_resolution_string("1920x1080", resolution)) { + * if (resolution) { + * BOOST_LOG(info) << "Value was specified"; + * } + * else { + * BOOST_LOG(info) << "Value was empty"; + * } + * } + * @examples_end + */ + bool parse_resolution_string(const std::string &input, std::optional &output) { + const std::string trimmed_input {boost::algorithm::trim_copy(input)}; + const std::regex resolution_regex {R"(^(\d+)x(\d+)$)"}; + + if (std::smatch match; std::regex_match(trimmed_input, match, resolution_regex)) { + try { + output = Resolution { + stou(match[1].str()), + stou(match[2].str()) + }; + return true; + } catch (const std::out_of_range &) { + BOOST_LOG(error) << "Failed to parse resolution string " << trimmed_input << " (number out of range)."; + } catch (const std::exception &err) { + BOOST_LOG(error) << "Failed to parse resolution string " << trimmed_input << ":\n" + << err.what(); + } + } else { + if (trimmed_input.empty()) { + output = std::nullopt; + return true; + } + + BOOST_LOG(error) << "Failed to parse resolution string " << trimmed_input << R"(. It must match a "1920x1080" pattern!)"; + } + + return false; + } + + /** + * @brief Parse refresh rate value from the string. + * @param input String to be parsed. + * @param output Reference to output variable to fill in. + * @param allow_decimal_point Specify whether the decimal point is allowed or not. + * @returns True on successful parsing (empty string allowed), false otherwise. + * + * @examples + * std::optional refresh_rate; + * if (parse_refresh_rate_string("59.95", refresh_rate)) { + * if (refresh_rate) { + * BOOST_LOG(info) << "Value was specified"; + * } + * else { + * BOOST_LOG(info) << "Value was empty"; + * } + * } + * @examples_end + */ + bool parse_refresh_rate_string(const std::string &input, std::optional &output, const bool allow_decimal_point = true) { + static const auto is_zero {[](const auto &character) { + return character == '0'; + }}; + const std::string trimmed_input {boost::algorithm::trim_copy(input)}; + const std::regex refresh_rate_regex {allow_decimal_point ? R"(^(\d+)(?:\.(\d+))?$)" : R"(^(\d+)$)"}; + + if (std::smatch match; std::regex_match(trimmed_input, match, refresh_rate_regex)) { + try { + // Here we are trimming zeros from the string to possibly reduce out of bounds case + std::string trimmed_match_1 {boost::algorithm::trim_left_copy_if(match[1].str(), is_zero)}; + if (trimmed_match_1.empty()) { + trimmed_match_1 = "0"s; // Just in case ALL the string is full of zeros, we want to leave one + } + + std::string trimmed_match_2; + if (allow_decimal_point && match[2].matched) { + trimmed_match_2 = boost::algorithm::trim_right_copy_if(match[2].str(), is_zero); + } + + if (!trimmed_match_2.empty()) { + // We have a decimal point and will have to split it into numerator and denominator. + // For example: + // 59.995: + // numerator = 59995 + // denominator = 1000 + + // We are essentially removing the decimal point here: 59.995 -> 59995 + const std::string numerator_str {trimmed_match_1 + trimmed_match_2}; + const auto numerator {stou(numerator_str)}; + + // Here we are counting decimal places and calculating denominator: 10^decimal_places + const auto denominator {static_cast(std::pow(10, trimmed_match_2.size()))}; + + output = Rational {numerator, denominator}; + } else { + // We do not have a decimal point, just a valid number. + // For example: + // 60: + // numerator = 60 + // denominator = 1 + output = Rational {stou(trimmed_match_1), 1}; + } + return true; + } catch (const std::out_of_range &) { + BOOST_LOG(error) << "Failed to parse refresh rate string " << trimmed_input << " (number out of range)."; + } catch (const std::exception &err) { + BOOST_LOG(error) << "Failed to parse refresh rate string " << trimmed_input << ":\n" + << err.what(); + } + } else { + if (trimmed_input.empty()) { + output = std::nullopt; + return true; + } + + BOOST_LOG(error) << "Failed to parse refresh rate string " << trimmed_input << ". Must have a pattern of " << (allow_decimal_point ? R"("123" or "123.456")" : R"("123")") << "!"; + } + + return false; + } + + /** + * @brief Parse device preparation option from the user configuration and the session information. + * @param video_config User's video related configuration. + * @returns Parsed device preparation value we need to use. + * Empty optional if no preparation nor configuration shall take place. + * + * @examples + * const config::video_t &video_config { config::video }; + * const auto device_prep_option = parse_device_prep_option(video_config); + * @examples_end + */ + std::optional parse_device_prep_option(const config::video_t &video_config) { + using enum config::video_t::dd_t::config_option_e; + using enum SingleDisplayConfiguration::DevicePreparation; + + switch (video_config.dd.configuration_option) { + case verify_only: + return VerifyOnly; + case ensure_active: + return EnsureActive; + case ensure_primary: + return EnsurePrimary; + case ensure_only_display: + return EnsureOnlyDisplay; + case disabled: + break; + } + + return std::nullopt; + } + + /** + * @brief Parse resolution option from the user configuration and the session information. + * @param video_config User's video related configuration. + * @param session Session information. + * @param config A reference to a display config object that will be modified on success. + * @returns True on successful parsing, false otherwise. + * + * @examples + * const std::shared_ptr launch_session; + * const config::video_t &video_config { config::video }; + * + * SingleDisplayConfiguration config; + * const bool success = parse_resolution_option(video_config, *launch_session, config); + * @examples_end + */ + bool parse_resolution_option(const config::video_t &video_config, const rtsp_stream::launch_session_t &session, SingleDisplayConfiguration &config) { + using resolution_option_e = config::video_t::dd_t::resolution_option_e; + + switch (video_config.dd.resolution_option) { + case resolution_option_e::automatic: + { + if (!session.enable_sops) { + BOOST_LOG(warning) << R"(Sunshine is configured to change resolution automatically, but the "Optimize game settings" is not set in the client! Resolution will not be changed.)"; + } else if (session.width >= 0 && session.height >= 0) { + config.m_resolution = Resolution { + static_cast(session.width), + static_cast(session.height) + }; + } else { + BOOST_LOG(error) << "Resolution provided by client session config is invalid: " << session.width << "x" << session.height; + return false; + } + break; + } + case resolution_option_e::manual: + { + if (!session.enable_sops) { + BOOST_LOG(warning) << R"(Sunshine is configured to change resolution manually, but the "Optimize game settings" is not set in the client! Resolution will not be changed.)"; + } else { + if (!parse_resolution_string(video_config.dd.manual_resolution, config.m_resolution)) { + BOOST_LOG(error) << "Failed to parse manual resolution string!"; + return false; + } + + if (!config.m_resolution) { + BOOST_LOG(error) << "Manual resolution must be specified!"; + return false; + } + } + break; + } + case resolution_option_e::disabled: + break; + } + + return true; + } + + /** + * @brief Parse refresh rate option from the user configuration and the session information. + * @param video_config User's video related configuration. + * @param session Session information. + * @param config A reference to a config object that will be modified on success. + * @returns True on successful parsing, false otherwise. + * + * @examples + * const std::shared_ptr launch_session; + * const config::video_t &video_config { config::video }; + * + * SingleDisplayConfiguration config; + * const bool success = parse_refresh_rate_option(video_config, *launch_session, config); + * @examples_end + */ + bool parse_refresh_rate_option(const config::video_t &video_config, const rtsp_stream::launch_session_t &session, SingleDisplayConfiguration &config) { + using refresh_rate_option_e = config::video_t::dd_t::refresh_rate_option_e; + + switch (video_config.dd.refresh_rate_option) { + case refresh_rate_option_e::automatic: + { + if (session.fps >= 0) { + config.m_refresh_rate = Rational {static_cast(session.fps), 1}; + } else { + BOOST_LOG(error) << "FPS value provided by client session config is invalid: " << session.fps; + return false; + } + break; + } + case refresh_rate_option_e::manual: + { + if (!parse_refresh_rate_string(video_config.dd.manual_refresh_rate, config.m_refresh_rate)) { + BOOST_LOG(error) << "Failed to parse manual refresh rate string!"; + return false; + } + + if (!config.m_refresh_rate) { + BOOST_LOG(error) << "Manual refresh rate must be specified!"; + return false; + } + break; + } + case refresh_rate_option_e::disabled: + break; + } + + return true; + } + + /** + * @brief Parse HDR option from the user configuration and the session information. + * @param video_config User's video related configuration. + * @param session Session information. + * @returns Parsed HDR state value we need to switch to. + * Empty optional if no action is required. + * + * @examples + * const std::shared_ptr launch_session; + * const config::video_t &video_config { config::video }; + * const auto hdr_option = parse_hdr_option(video_config, *launch_session); + * @examples_end + */ + std::optional parse_hdr_option(const config::video_t &video_config, const rtsp_stream::launch_session_t &session) { + using hdr_option_e = config::video_t::dd_t::hdr_option_e; + + switch (video_config.dd.hdr_option) { + case hdr_option_e::automatic: + return session.enable_hdr ? HdrState::Enabled : HdrState::Disabled; + case hdr_option_e::disabled: + break; + } + + return std::nullopt; + } + + /** + * @brief Indicates which remapping fields and config structure shall be used. + */ + enum class remapping_type_e { + mixed, ///! Both reseolution and refresh rate may be remapped + resolution_only, ///! Only resolution will be remapped + refresh_rate_only ///! Only refresh rate will be remapped + }; + + /** + * @brief Determine the ramapping type from the user config. + * @param video_config User's video related configuration. + * @returns Enum value if remapping can be performed, null optional if remapping shall be skipped. + */ + std::optional determine_remapping_type(const config::video_t &video_config) { + using dd_t = config::video_t::dd_t; + const bool auto_resolution {video_config.dd.resolution_option == dd_t::resolution_option_e::automatic}; + const bool auto_refresh_rate {video_config.dd.refresh_rate_option == dd_t::refresh_rate_option_e::automatic}; + + if (auto_resolution && auto_refresh_rate) { + return remapping_type_e::mixed; + } + + if (auto_resolution) { + return remapping_type_e::resolution_only; + } + + if (auto_refresh_rate) { + return remapping_type_e::refresh_rate_only; + } + + return std::nullopt; + } + + /** + * @brief Contains remapping data parsed from the string values. + */ + struct parsed_remapping_entry_t { + std::optional requested_resolution; + std::optional requested_fps; + std::optional final_resolution; + std::optional final_refresh_rate; + }; + + /** + * @brief Check if resolution is to be mapped based on remmaping type. + * @param type Remapping type to check. + * @returns True if resolution is to be mapped, false otherwise. + */ + bool is_resolution_mapped(const remapping_type_e type) { + return type == remapping_type_e::resolution_only || type == remapping_type_e::mixed; + } + + /** + * @brief Check if FPS is to be mapped based on remmaping type. + * @param type Remapping type to check. + * @returns True if FPS is to be mapped, false otherwise. + */ + bool is_fps_mapped(const remapping_type_e type) { + return type == remapping_type_e::refresh_rate_only || type == remapping_type_e::mixed; + } + + /** + * @brief Parse the remapping entry from the config into an internal structure. + * @param entry Entry to parse. + * @param type Specify which entry fields should be parsed. + * @returns Parsed structure or null optional if a necessary field could not be parsed. + */ + std::optional parse_remapping_entry(const config::video_t::dd_t::mode_remapping_entry_t &entry, const remapping_type_e type) { + parsed_remapping_entry_t result {}; + + if (is_resolution_mapped(type) && (!parse_resolution_string(entry.requested_resolution, result.requested_resolution) || + !parse_resolution_string(entry.final_resolution, result.final_resolution))) { + return std::nullopt; + } + + if (is_fps_mapped(type) && (!parse_refresh_rate_string(entry.requested_fps, result.requested_fps, false) || + !parse_refresh_rate_string(entry.final_refresh_rate, result.final_refresh_rate))) { + return std::nullopt; + } + + return result; + } + + /** + * @brief Remap the the requested display mode based on the config. + * @param video_config User's video related configuration. + * @param session Session information. + * @param config A reference to a config object that will be modified on success. + * @returns True if the remapping was performed or skipped, false if remapping has failed due to invalid config. + * + * @examples + * const std::shared_ptr launch_session; + * const config::video_t &video_config { config::video }; + * + * SingleDisplayConfiguration config; + * const bool success = remap_display_mode_if_needed(video_config, *launch_session, config); + * @examples_end + */ + bool remap_display_mode_if_needed(const config::video_t &video_config, const rtsp_stream::launch_session_t &session, SingleDisplayConfiguration &config) { + const auto remapping_type {determine_remapping_type(video_config)}; + if (!remapping_type) { + return true; + } + + const auto &remapping_list {[&]() { + using enum remapping_type_e; + + switch (*remapping_type) { + case resolution_only: + return video_config.dd.mode_remapping.resolution_only; + case refresh_rate_only: + return video_config.dd.mode_remapping.refresh_rate_only; + case mixed: + default: + return video_config.dd.mode_remapping.mixed; + } + }()}; + + if (remapping_list.empty()) { + BOOST_LOG(debug) << "No values are available for display mode remapping."; + return true; + } + BOOST_LOG(debug) << "Trying to remap display modes..."; + + const auto entry_to_string {[type = *remapping_type](const config::video_t::dd_t::mode_remapping_entry_t &entry) { + const bool mapping_resolution {is_resolution_mapped(type)}; + const bool mapping_fps {is_fps_mapped(type)}; + + // clang-format off + return (mapping_resolution ? " - requested resolution: "s + entry.requested_resolution + "\n" : "") + + (mapping_fps ? " - requested FPS: "s + entry.requested_fps + "\n" : "") + + (mapping_resolution ? " - final resolution: "s + entry.final_resolution + "\n" : "") + + (mapping_fps ? " - final refresh rate: "s + entry.final_refresh_rate : ""); + // clang-format on + }}; + + for (const auto &entry : remapping_list) { + const auto parsed_entry {parse_remapping_entry(entry, *remapping_type)}; + if (!parsed_entry) { + BOOST_LOG(error) << "Failed to parse remapping entry from:\n" + << entry_to_string(entry); + return false; + } + + if (!parsed_entry->final_resolution && !parsed_entry->final_refresh_rate) { + BOOST_LOG(error) << "At least one final value must be set for remapping display modes! Entry:\n" + << entry_to_string(entry); + return false; + } + + if (!session.enable_sops && (parsed_entry->requested_resolution || parsed_entry->final_resolution)) { + BOOST_LOG(warning) << R"(Skipping remapping entry, because the "Optimize game settings" is not set in the client! Entry:\n)" + << entry_to_string(entry); + continue; + } + + // Note: at this point config should already have parsed resolution set. + if (parsed_entry->requested_resolution && parsed_entry->requested_resolution != config.m_resolution) { + BOOST_LOG(verbose) << "Skipping remapping because requested resolutions do not match! Entry:\n" + << entry_to_string(entry); + continue; + } + + // Note: at this point config should already have parsed refresh rate set. + if (parsed_entry->requested_fps && parsed_entry->requested_fps != config.m_refresh_rate) { + BOOST_LOG(verbose) << "Skipping remapping because requested FPS do not match! Entry:\n" + << entry_to_string(entry); + continue; + } + + BOOST_LOG(info) << "Remapping requested display mode. Entry:\n" + << entry_to_string(entry); + if (parsed_entry->final_resolution) { + config.m_resolution = parsed_entry->final_resolution; + } + if (parsed_entry->final_refresh_rate) { + config.m_refresh_rate = parsed_entry->final_refresh_rate; + } + break; + } + + return true; + } + + /** + * @brief Construct a settings manager interface to manage display device settings. + * @param persistence_filepath File location for saving persistent state. + * @param video_config User's video related configuration. + * @return An interface or nullptr if the OS does not support the interface. + */ + std::unique_ptr make_settings_manager([[maybe_unused]] const std::filesystem::path &persistence_filepath, [[maybe_unused]] const config::video_t &video_config) { +#ifdef _WIN32 + return std::make_unique( + std::make_shared(std::make_shared()), + std::make_shared(), + std::make_unique( + std::make_shared(persistence_filepath) + ), + WinWorkarounds { + .m_hdr_blank_delay = video_config.dd.wa.hdr_toggle_delay != std::chrono::milliseconds::zero() ? std::make_optional(video_config.dd.wa.hdr_toggle_delay) : std::nullopt + } + ); +#else + return nullptr; +#endif + } + + /** + * @brief Defines the "revert config" algorithms. + */ + enum class revert_option_e { + try_once, ///< Try reverting once and then abort. + try_indefinitely, ///< Keep trying to revert indefinitely. + try_indefinitely_with_delay ///< Keep trying to revert indefinitely, but delay the first try by some amount of time. + }; + + /** + * @brief Reverts the configuration based on the provided option. + * @note This is function does not lock mutex. + */ + void revert_configuration_unlocked(const revert_option_e option) { + if (!DD_DATA.sm_instance) { + // Platform is not supported, nothing to do. + return; + } + + // Note: by default the executor function is immediately executed in the calling thread. With delay, we want to avoid that. + SchedulerOptions scheduler_option {.m_sleep_durations = {DEFAULT_RETRY_INTERVAL}}; + if (option == revert_option_e::try_indefinitely_with_delay && DD_DATA.config_revert_delay > std::chrono::milliseconds::zero()) { + scheduler_option.m_sleep_durations = {DD_DATA.config_revert_delay, DEFAULT_RETRY_INTERVAL}; + scheduler_option.m_execution = SchedulerOptions::Execution::ScheduledOnly; + } + + DD_DATA.sm_instance->schedule([try_once = (option == revert_option_e::try_once), tried_out_devices = std::set {}](auto &settings_iface, auto &stop_token) mutable { + if (try_once) { + std::ignore = settings_iface.revertSettings(); + stop_token.requestStop(); + return; + } + + auto available_devices {[&settings_iface]() { + const auto devices {settings_iface.enumAvailableDevices()}; + std::set parsed_devices; + + std::transform( + std::begin(devices), + std::end(devices), + std::inserter(parsed_devices, std::end(parsed_devices)), + [](const auto &device) { + return device.m_device_id + " - " + device.m_friendly_name; + } + ); + + return parsed_devices; + }()}; + if (available_devices == tried_out_devices) { + BOOST_LOG(debug) << "Skipping reverting configuration, because no newly added/removed devices were detected since last check. Currently available devices:\n" + << toJson(available_devices); + return; + } + + using enum SettingsManagerInterface::RevertResult; + if (const auto result {settings_iface.revertSettings()}; result == Ok) { + stop_token.requestStop(); + return; + } else if (result == ApiTemporarilyUnavailable) { + // Do nothing and retry next time + return; + } + + // If we have failed to revert settings then we will try to do it next time only if a device was added/removed + BOOST_LOG(warning) << "Failed to revert display device configuration (will retry once devices are added or removed). Enabling all of the available devices:\n" + << toJson(available_devices); + tried_out_devices.swap(available_devices); + }, + scheduler_option); + } + } // namespace + + std::unique_ptr init(const std::filesystem::path &persistence_filepath, const config::video_t &video_config) { + std::lock_guard lock {DD_DATA.mutex}; + // We can support re-init without any issues, however we should make sure to clean up first! + revert_configuration_unlocked(revert_option_e::try_once); + DD_DATA.config_revert_delay = video_config.dd.config_revert_delay; + DD_DATA.sm_instance = nullptr; + + // If we fail to create settings manager, this means platform is not supported, and + // we will need to provided error-free pass-trough in other methods + if (auto settings_manager {make_settings_manager(persistence_filepath, video_config)}) { + DD_DATA.sm_instance = std::make_unique>(std::move(settings_manager)); + + const auto available_devices {DD_DATA.sm_instance->execute([](auto &settings_iface) { + return settings_iface.enumAvailableDevices(); + })}; + BOOST_LOG(info) << "Currently available display devices:\n" + << toJson(available_devices); + + // In case we have failed to revert configuration before shutting down, we should + // do it now. + revert_configuration_unlocked(revert_option_e::try_indefinitely); + } + + class deinit_t: public platf::deinit_t { + public: + ~deinit_t() override { + std::lock_guard lock {DD_DATA.mutex}; + try { + // This may throw if used incorrectly. At the moment this will not happen, however + // in case some unforeseen changes are made that could raise an exception, + // we definitely don't want this to happen in destructor. Especially in the + // deinit_t where the outcome does not really matter. + revert_configuration_unlocked(revert_option_e::try_once); + } catch (std::exception &err) { + BOOST_LOG(fatal) << err.what(); + } + + DD_DATA.sm_instance = nullptr; + } + }; + + return std::make_unique(); + } + + std::string map_output_name(const std::string &output_name) { + std::lock_guard lock {DD_DATA.mutex}; + if (!DD_DATA.sm_instance) { + // Fallback to giving back the output name if the platform is not supported. + return output_name; + } + + return DD_DATA.sm_instance->execute([&output_name](auto &settings_iface) { + return settings_iface.getDisplayName(output_name); + }); + } + + void configure_display(const config::video_t &video_config, const rtsp_stream::launch_session_t &session) { + const auto result {parse_configuration(video_config, session)}; + if (const auto *parsed_config {std::get_if(&result)}; parsed_config) { + configure_display(*parsed_config); + return; + } + + if (const auto *disabled {std::get_if(&result)}; disabled) { + revert_configuration(); + return; + } + + // Error already logged for failed_to_parse_tag_t case, and we also don't + // want to revert active configuration in case we have any + } + + void configure_display(const SingleDisplayConfiguration &config) { + std::lock_guard lock {DD_DATA.mutex}; + if (!DD_DATA.sm_instance) { + // Platform is not supported, nothing to do. + return; + } + + DD_DATA.sm_instance->schedule([config](auto &settings_iface, auto &stop_token) { + // We only want to keep retrying in case of a transient errors. + // In other cases, when we either fail or succeed we just want to stop... + if (settings_iface.applySettings(config) != SettingsManagerInterface::ApplyResult::ApiTemporarilyUnavailable) { + stop_token.requestStop(); + } + }, + {.m_sleep_durations = {DEFAULT_RETRY_INTERVAL}}); + } + + void revert_configuration() { + std::lock_guard lock {DD_DATA.mutex}; + revert_configuration_unlocked(revert_option_e::try_indefinitely_with_delay); + } + + bool reset_persistence() { + std::lock_guard lock {DD_DATA.mutex}; + if (!DD_DATA.sm_instance) { + // Platform is not supported, assume success. + return true; + } + + return DD_DATA.sm_instance->execute([](auto &settings_iface, auto &stop_token) { + // Whatever the outcome is we want to stop interfering with the user, + // so any schedulers need to be stopped. + stop_token.requestStop(); + return settings_iface.resetPersistence(); + }); + } + + EnumeratedDeviceList enumerate_devices() { + std::lock_guard lock {DD_DATA.mutex}; + if (!DD_DATA.sm_instance) { + // Platform is not supported. + return {}; + } + + return DD_DATA.sm_instance->execute([](auto &settings_iface) { + return settings_iface.enumAvailableDevices(); + }); + } + + std::variant parse_configuration(const config::video_t &video_config, const rtsp_stream::launch_session_t &session) { + const auto device_prep {parse_device_prep_option(video_config)}; + if (!device_prep) { + return configuration_disabled_tag_t {}; + } + + SingleDisplayConfiguration config; + config.m_device_id = video_config.output_name; + config.m_device_prep = *device_prep; + config.m_hdr_state = parse_hdr_option(video_config, session); + + if (!parse_resolution_option(video_config, session, config)) { + // Error already logged + return failed_to_parse_tag_t {}; + } + + if (!parse_refresh_rate_option(video_config, session, config)) { + // Error already logged + return failed_to_parse_tag_t {}; + } + + if (!remap_display_mode_if_needed(video_config, session, config)) { + // Error already logged + return failed_to_parse_tag_t {}; + } + + return config; + } +} // namespace display_device diff --git a/src/display_device.h b/src/display_device.h new file mode 100644 index 00000000000..d49698bdb52 --- /dev/null +++ b/src/display_device.h @@ -0,0 +1,162 @@ +/** + * @file src/display_device.h + * @brief Declarations for display device handling. + */ +#pragma once + +// standard includes +#include +#include + +// lib includes +#include + +// forward declarations +namespace platf { + class deinit_t; +} + +namespace config { + struct video_t; +} + +namespace rtsp_stream { + struct launch_session_t; +} + +namespace display_device { + /** + * @brief Initialize the implementation and perform the initial state recovery (if needed). + * @param persistence_filepath File location for reading/saving persistent state. + * @param video_config User's video related configuration. + * @returns A deinit_t instance that performs cleanup when destroyed. + * + * @examples + * const config::video_t &video_config { config::video }; + * const auto init_guard { init("/my/persitence/file.state", video_config) }; + * @examples_end + */ + [[nodiscard]] std::unique_ptr init(const std::filesystem::path &persistence_filepath, const config::video_t &video_config); + + /** + * @brief Map the output name to a specific display. + * @param output_name The user-configurable output name. + * @returns Mapped display name or empty string if the output name could not be mapped. + * + * @examples + * const auto mapped_name_config { map_output_name(config::video.output_name) }; + * const auto mapped_name_custom { map_output_name("{some-device-id}") }; + * @examples_end + */ + [[nodiscard]] std::string map_output_name(const std::string &output_name); + + /** + * @brief Configure the display device based on the user configuration and the session information. + * @note This is a convenience method for calling similar method of a different signature. + * + * @param video_config User's video related configuration. + * @param session Session information. + * + * @examples + * const std::shared_ptr launch_session; + * const config::video_t &video_config { config::video }; + * + * configure_display(video_config, *launch_session); + * @examples_end + */ + void configure_display(const config::video_t &video_config, const rtsp_stream::launch_session_t &session); + + /** + * @brief Configure the display device using the provided configuration. + * + * In some cases configuring display can fail due to transient issues and + * we will keep trying every 5 seconds, even if the stream has already started as there was + * no possibility to apply settings before the stream start. + * + * Therefore, there is no return value as we still want to continue with the stream, so that + * the users can do something about it once they are connected. Otherwise, we might + * prevent users from logging in at all if we keep failing to apply configuration. + * + * @param config Configuration for the display. + * + * @examples + * const SingleDisplayConfiguration valid_config { }; + * configure_display(valid_config); + * @examples_end + */ + void configure_display(const SingleDisplayConfiguration &config); + + /** + * @brief Revert the display configuration and restore the previous state. + * + * In case the state could not be restored, by default it will be retried again in 5 seconds + * (repeating indefinitely until success or until persistence is reset). + * + * @examples + * revert_configuration(); + * @examples_end + */ + void revert_configuration(); + + /** + * @brief Reset the persistence and currently held initial display state. + * + * This is normally used to get out of the "broken" state where the algorithm wants + * to restore the initial display state, but it is no longer possible. + * + * This could happen if the display is no longer available or the hardware was changed + * and the device ids no longer match. + * + * The user then accepts that Sunshine is not able to restore the state and "agrees" to + * do it manually. + * + * @return True if persistence was reset, false otherwise. + * @note Whether the function succeeds or fails, any of the scheduled "retries" from + * other methods will be stopped to not interfere with the user actions. + * + * @examples + * const auto result = reset_persistence(); + * @examples_end + */ + [[nodiscard]] bool reset_persistence(); + + /** + * @brief Enumerate the available devices. + * @return A list of devices. + * + * @examples + * const auto devices = enumerate_devices(); + * @examples_end + */ + [[nodiscard]] EnumeratedDeviceList enumerate_devices(); + + /** + * @brief A tag structure indicating that configuration parsing has failed. + */ + struct failed_to_parse_tag_t {}; + + /** + * @brief A tag structure indicating that configuration is disabled. + */ + struct configuration_disabled_tag_t {}; + + /** + * @brief Parse the user configuration and the session information. + * @param video_config User's video related configuration. + * @param session Session information. + * @return Parsed single display configuration or + * a tag indicating that the parsing has failed or + * a tag indicating that the user does not want to perform any configuration. + * + * @examples + * const std::shared_ptr launch_session; + * const config::video_t &video_config { config::video }; + * + * const auto config { parse_configuration(video_config, *launch_session) }; + * if (const auto *parsed_config { std::get_if(&result) }; parsed_config) { + * configure_display(*config); + * } + * @examples_end + */ + [[nodiscard]] std::variant parse_configuration(const config::video_t &video_config, const rtsp_stream::launch_session_t &session); +} // namespace display_device diff --git a/src/entry_handler.cpp b/src/entry_handler.cpp index 2f755e9a279..ed978dcd06a 100644 --- a/src/entry_handler.cpp +++ b/src/entry_handler.cpp @@ -26,21 +26,18 @@ extern "C" { using namespace std::literals; -void -launch_ui() { +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) { +void launch_ui_with_path(std::string path) { std::string url = "https://localhost:" + std::to_string(net::map_port(confighttp::PORT_HTTPS)) + path; platf::open_url(url); } namespace args { - int - creds(const char *name, int argc, char *argv[]) { + int creds(const char *name, int argc, char *argv[]) { if (argc < 2 || argv[0] == "help"sv || argv[1] == "help"sv) { help(name); } @@ -50,21 +47,18 @@ namespace args { return 0; } - int - help(const char *name) { + int help(const char *name) { logging::print_help(name); return 0; } - int - version() { + int version() { // version was already logged at startup return 0; } #ifdef _WIN32 - int - restore_nvprefs_undo() { + int restore_nvprefs_undo() { if (nvprefs_instance.load()) { nvprefs_instance.restore_from_and_delete_undo_file_if_exists(); nvprefs_instance.unload(); @@ -78,8 +72,7 @@ namespace lifetime { char **argv; std::atomic_int desired_exit_code; - void - exit_sunshine(int exit_code, bool async) { + void exit_sunshine(int exit_code, bool async) { // Store the exit code of the first exit_sunshine() call int zero = 0; desired_exit_code.compare_exchange_strong(zero, exit_code); @@ -94,8 +87,7 @@ namespace lifetime { } } - void - debug_trap() { + void debug_trap() { #ifdef _WIN32 DebugBreak(); #else @@ -103,22 +95,19 @@ namespace lifetime { #endif } - char ** - get_argv() { + char **get_argv() { return argv; } } // namespace lifetime -void -log_publisher_data() { +void log_publisher_data() { BOOST_LOG(info) << "Package Publisher: "sv << SUNSHINE_PUBLISHER_NAME; BOOST_LOG(info) << "Publisher Website: "sv << SUNSHINE_PUBLISHER_WEBSITE; BOOST_LOG(info) << "Get support: "sv << SUNSHINE_PUBLISHER_ISSUE_URL; } #ifdef _WIN32 -bool -is_gamestream_enabled() { +bool is_gamestream_enabled() { DWORD enabled; DWORD size = sizeof(enabled); return RegGetValueW( @@ -128,7 +117,8 @@ is_gamestream_enabled() { RRF_RT_REG_DWORD, nullptr, &enabled, - &size) == ERROR_SUCCESS && + &size + ) == ERROR_SUCCESS && enabled != 0; } @@ -168,8 +158,7 @@ namespace service_ctrl { /** * @brief Asynchronously starts the Sunshine service. */ - bool - start_service() { + bool start_service() { if (!service_handle) { return false; } @@ -189,8 +178,7 @@ namespace service_ctrl { * @brief Query the service status. * @param status The SERVICE_STATUS struct to populate. */ - bool - query_service_status(SERVICE_STATUS &status) { + bool query_service_status(SERVICE_STATUS &status) { if (!service_handle) { return false; } @@ -209,9 +197,8 @@ namespace service_ctrl { SC_HANDLE service_handle = NULL; }; - bool - is_service_running() { - service_controller sc { SERVICE_QUERY_STATUS }; + bool is_service_running() { + service_controller sc {SERVICE_QUERY_STATUS}; SERVICE_STATUS status; if (!sc.query_service_status(status)) { @@ -221,9 +208,8 @@ namespace service_ctrl { return status.dwCurrentState == SERVICE_RUNNING; } - bool - start_service() { - service_controller sc { SERVICE_QUERY_STATUS | SERVICE_START }; + bool start_service() { + service_controller sc {SERVICE_QUERY_STATUS | SERVICE_START}; std::cout << "Starting Sunshine..."sv; @@ -247,8 +233,7 @@ namespace service_ctrl { return true; } - bool - wait_for_ui_ready() { + bool wait_for_ui_ready() { std::cout << "Waiting for Web UI to be ready..."; // Wait up to 30 seconds for the web UI to start diff --git a/src/entry_handler.h b/src/entry_handler.h index a2c9735ac8d..a83fed292cf 100644 --- a/src/entry_handler.h +++ b/src/entry_handler.h @@ -18,8 +18,7 @@ * launch_ui(); * @examples_end */ -void -launch_ui(); +void launch_ui(); /** * @brief Launch the Web UI at a specific endpoint. @@ -27,8 +26,7 @@ launch_ui(); * launch_ui_with_path("/pin"); * @examples_end */ -void -launch_ui_with_path(std::string path); +void launch_ui_with_path(std::string path); /** * @brief Functions for handling command line arguments. @@ -43,8 +41,7 @@ namespace args { * creds("sunshine", 2, {"new_username", "new_password"}); * @examples_end */ - int - creds(const char *name, int argc, char *argv[]); + int creds(const char *name, int argc, char *argv[]); /** * @brief Print help to stdout, then exit. @@ -53,8 +50,7 @@ namespace args { * help("sunshine"); * @examples_end */ - int - help(const char *name); + int help(const char *name); /** * @brief Print the version to stdout, then exit. @@ -62,8 +58,7 @@ namespace args { * version(); * @examples_end */ - int - version(); + int version(); #ifdef _WIN32 /** @@ -75,8 +70,7 @@ namespace args { * restore_nvprefs_undo(); * @examples_end */ - int - restore_nvprefs_undo(); + int restore_nvprefs_undo(); #endif } // namespace args @@ -92,35 +86,30 @@ namespace lifetime { * @param exit_code The exit code to return from main(). * @param async Specifies whether our termination will be non-blocking. */ - void - exit_sunshine(int exit_code, bool async); + void exit_sunshine(int exit_code, bool async); /** * @brief Breaks into the debugger or terminates Sunshine if no debugger is attached. */ - void - debug_trap(); + void debug_trap(); /** * @brief Get the argv array passed to main(). */ - char ** - get_argv(); + char **get_argv(); } // namespace lifetime /** * @brief Log the publisher metadata provided from CMake. */ -void -log_publisher_data(); +void log_publisher_data(); #ifdef _WIN32 /** * @brief Check if NVIDIA's GameStream software is running. * @return `true` if GameStream is enabled, `false` otherwise. */ -bool -is_gamestream_enabled(); +bool is_gamestream_enabled(); /** * @brief Namespace for controlling the Sunshine service model on Windows. @@ -132,8 +121,7 @@ namespace service_ctrl { * is_service_running(); * @examples_end */ - bool - is_service_running(); + bool is_service_running(); /** * @brief Start the service and wait for startup to complete. @@ -141,8 +129,7 @@ namespace service_ctrl { * start_service(); * @examples_end */ - bool - start_service(); + bool start_service(); /** * @brief Wait for the UI to be ready after Sunshine startup. @@ -150,7 +137,6 @@ namespace service_ctrl { * wait_for_ui_ready(); * @examples_end */ - bool - wait_for_ui_ready(); + bool wait_for_ui_ready(); } // namespace service_ctrl #endif diff --git a/src/file_handler.cpp b/src/file_handler.cpp index b5c9638a1b8..f7b4ee7253d 100644 --- a/src/file_handler.cpp +++ b/src/file_handler.cpp @@ -12,8 +12,7 @@ #include "logging.h" namespace file_handler { - std::string - get_parent_directory(const std::string &path) { + std::string get_parent_directory(const std::string &path) { // remove any trailing path separators std::string trimmed_path = path; while (!trimmed_path.empty() && trimmed_path.back() == '/') { @@ -24,8 +23,7 @@ namespace file_handler { return p.parent_path().string(); } - bool - make_directory(const std::string &path) { + bool make_directory(const std::string &path) { // first, check if the directory already exists if (std::filesystem::exists(path)) { return true; @@ -34,19 +32,17 @@ namespace file_handler { return std::filesystem::create_directories(path); } - std::string - read_file(const char *path) { + std::string read_file(const char *path) { if (!std::filesystem::exists(path)) { BOOST_LOG(debug) << "Missing file: " << path; return {}; } std::ifstream in(path); - return std::string { (std::istreambuf_iterator(in)), std::istreambuf_iterator() }; + return std::string {(std::istreambuf_iterator(in)), std::istreambuf_iterator()}; } - int - write_file(const char *path, const std::string_view &contents) { + int write_file(const char *path, const std::string_view &contents) { std::ofstream out(path); if (!out.is_open()) { diff --git a/src/file_handler.h b/src/file_handler.h index d669f17b8e5..5029fb08991 100644 --- a/src/file_handler.h +++ b/src/file_handler.h @@ -4,6 +4,7 @@ */ #pragma once +// standard includes #include /** @@ -18,8 +19,7 @@ namespace file_handler { * std::string parent_dir = get_parent_directory("path/to/file"); * @examples_end */ - std::string - get_parent_directory(const std::string &path); + std::string get_parent_directory(const std::string &path); /** * @brief Make a directory. @@ -29,8 +29,7 @@ namespace file_handler { * bool dir_created = make_directory("path/to/directory"); * @examples_end */ - bool - make_directory(const std::string &path); + bool make_directory(const std::string &path); /** * @brief Read a file to string. @@ -40,8 +39,7 @@ namespace file_handler { * std::string contents = read_file("path/to/file"); * @examples_end */ - std::string - read_file(const char *path); + std::string read_file(const char *path); /** * @brief Writes a file. @@ -52,6 +50,5 @@ namespace file_handler { * int write_status = write_file("path/to/file", "file contents"); * @examples_end */ - int - write_file(const char *path, const std::string_view &contents); + int write_file(const char *path, const std::string_view &contents); } // namespace file_handler diff --git a/src/globals.cpp b/src/globals.cpp index fa9aa1d7a44..60daf398aa7 100644 --- a/src/globals.cpp +++ b/src/globals.cpp @@ -2,6 +2,7 @@ * @file globals.cpp * @brief Definitions for globally accessible variables and functions. */ +// local includes #include "globals.h" safe::mail_t mail::man; diff --git a/src/globals.h b/src/globals.h index 79c49eec793..1617b7c6f51 100644 --- a/src/globals.h +++ b/src/globals.h @@ -4,6 +4,7 @@ */ #pragma once +// local includes #include "entry_handler.h" #include "thread_pool.h" @@ -31,9 +32,9 @@ extern nvprefs::nvprefs_interface nvprefs_instance; * @brief Handles process-wide communication. */ namespace mail { -#define MAIL(x) \ +#define MAIL(x) \ constexpr auto x = std::string_view { \ - #x \ + #x \ } /** diff --git a/src/httpcommon.cpp b/src/httpcommon.cpp index 419ca6dd142..2f09268510a 100644 --- a/src/httpcommon.cpp +++ b/src/httpcommon.cpp @@ -4,22 +4,21 @@ */ #define BOOST_BIND_GLOBAL_PLACEHOLDERS -#include "process.h" - +// standard includes #include #include +// lib includes +#include +#include #include #include #include - -#include - +#include #include #include -#include -#include +// local includes #include "config.h" #include "crypto.h" #include "file_handler.h" @@ -28,6 +27,7 @@ #include "network.h" #include "nvhttp.h" #include "platform/common.h" +#include "process.h" #include "rtsp.h" #include "utility.h" #include "uuid.h" @@ -37,16 +37,13 @@ namespace http { namespace fs = std::filesystem; namespace pt = boost::property_tree; - int - reload_user_creds(const std::string &file); - bool - user_creds_exist(const std::string &file); + int reload_user_creds(const std::string &file); + bool user_creds_exist(const std::string &file); std::string unique_id; net::net_e origin_web_ui_allowed; - int - init() { + int init() { bool clean_slate = config::sunshine.flags[config::flag::FRESH_STATE]; origin_web_ui_allowed = net::from_enum_string(config::nvhttp.origin_web_ui_allowed); @@ -57,29 +54,25 @@ namespace http { config::nvhttp.pkey = (dir / ("pkey-"s + unique_id)).string(); } - if (!fs::exists(config::nvhttp.pkey) || !fs::exists(config::nvhttp.cert)) { - if (create_creds(config::nvhttp.pkey, config::nvhttp.cert)) { - return -1; - } - } - if (user_creds_exist(config::sunshine.credentials_file)) { - if (reload_user_creds(config::sunshine.credentials_file)) return -1; + if ((!fs::exists(config::nvhttp.pkey) || !fs::exists(config::nvhttp.cert)) && + create_creds(config::nvhttp.pkey, config::nvhttp.cert)) { + return -1; } - else { + if (!user_creds_exist(config::sunshine.credentials_file)) { BOOST_LOG(info) << "Open the Web UI to set your new username and password and getting started"; + } else if (reload_user_creds(config::sunshine.credentials_file)) { + return -1; } return 0; } - int - save_user_creds(const std::string &file, const std::string &username, const std::string &password, bool run_our_mouth) { + int save_user_creds(const std::string &file, const std::string &username, const std::string &password, bool run_our_mouth) { pt::ptree outputTree; if (fs::exists(file)) { try { pt::read_json(file, outputTree); - } - catch (std::exception &e) { + } catch (std::exception &e) { BOOST_LOG(error) << "Couldn't read user credentials: "sv << e.what(); return -1; } @@ -91,8 +84,7 @@ namespace http { outputTree.put("password", util::hex(crypto::hash(password + salt)).to_string()); try { pt::write_json(file, outputTree); - } - catch (std::exception &e) { + } catch (std::exception &e) { BOOST_LOG(error) << "error writing to the credentials file, perhaps try this again as an administrator? Details: "sv << e.what(); return -1; } @@ -101,8 +93,7 @@ namespace http { return 0; } - bool - user_creds_exist(const std::string &file) { + bool user_creds_exist(const std::string &file) { if (!fs::exists(file)) { return false; } @@ -113,32 +104,28 @@ namespace http { return inputTree.find("username") != inputTree.not_found() && inputTree.find("password") != inputTree.not_found() && inputTree.find("salt") != inputTree.not_found(); - } - catch (std::exception &e) { + } catch (std::exception &e) { BOOST_LOG(error) << "validating user credentials: "sv << e.what(); } return false; } - int - reload_user_creds(const std::string &file) { + int reload_user_creds(const std::string &file) { pt::ptree inputTree; try { pt::read_json(file, inputTree); config::sunshine.username = inputTree.get("username"); config::sunshine.password = inputTree.get("password"); config::sunshine.salt = inputTree.get("salt"); - } - catch (std::exception &e) { + } catch (std::exception &e) { BOOST_LOG(error) << "loading user credentials: "sv << e.what(); return -1; } return 0; } - int - create_creds(const std::string &pkey, const std::string &cert) { + int create_creds(const std::string &pkey, const std::string &cert) { fs::path pkey_path = pkey; fs::path cert_path = cert; @@ -172,18 +159,14 @@ namespace http { return -1; } - fs::permissions(pkey_path, - fs::perms::owner_read | fs::perms::owner_write, - fs::perm_options::replace, err_code); + fs::permissions(pkey_path, fs::perms::owner_read | fs::perms::owner_write, fs::perm_options::replace, err_code); if (err_code) { BOOST_LOG(error) << "Couldn't change permissions of ["sv << config::nvhttp.pkey << "] :"sv << err_code.message(); return -1; } - fs::permissions(cert_path, - fs::perms::owner_read | fs::perms::group_read | fs::perms::others_read | fs::perms::owner_write, - fs::perm_options::replace, err_code); + fs::permissions(cert_path, fs::perms::owner_read | fs::perms::group_read | fs::perms::others_read | fs::perms::owner_write, fs::perm_options::replace, err_code); if (err_code) { BOOST_LOG(error) << "Couldn't change permissions of ["sv << config::nvhttp.cert << "] :"sv << err_code.message(); @@ -193,21 +176,15 @@ namespace http { return 0; } - bool - download_file(const std::string &url, const std::string &file) { - CURL *curl = curl_easy_init(); - if (curl) { - // sonar complains about weak ssl and tls versions - // ideally, the setopts should go after the early returns; however sonar cannot detect the fix - curl_easy_setopt(curl, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_2); - } - else { + bool download_file(const std::string &url, const std::string &file, long ssl_version) { + // sonar complains about weak ssl and tls versions; however sonar cannot detect the fix + CURL *curl = curl_easy_init(); // NOSONAR + if (!curl) { BOOST_LOG(error) << "Couldn't create CURL instance"; return false; } - std::string file_dir = file_handler::get_parent_directory(file); - if (!file_handler::make_directory(file_dir)) { + if (std::string file_dir = file_handler::get_parent_directory(file); !file_handler::make_directory(file_dir)) { BOOST_LOG(error) << "Couldn't create directory ["sv << file_dir << ']'; curl_easy_cleanup(curl); return false; @@ -220,6 +197,7 @@ namespace http { return false; } + curl_easy_setopt(curl, CURLOPT_SSLVERSION, ssl_version); // NOSONAR curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, fwrite); curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp); @@ -234,20 +212,16 @@ namespace http { return result == CURLE_OK; } - std::string - url_escape(const std::string &url) { - CURL *curl = curl_easy_init(); - char *string = curl_easy_escape(curl, url.c_str(), url.length()); + std::string url_escape(const std::string &url) { + char *string = curl_easy_escape(nullptr, url.c_str(), static_cast(url.length())); std::string result(string); curl_free(string); - curl_easy_cleanup(curl); return result; } - std::string - url_get_host(const std::string &url) { + std::string url_get_host(const std::string &url) { CURLU *curlu = curl_url(); - curl_url_set(curlu, CURLUPART_URL, url.c_str(), url.length()); + curl_url_set(curlu, CURLUPART_URL, url.c_str(), static_cast(url.length())); char *host; if (curl_url_get(curlu, CURLUPART_HOST, &host, 0) != CURLUE_OK) { curl_url_cleanup(curlu); @@ -258,5 +232,4 @@ namespace http { curl_url_cleanup(curlu); return result; } - } // namespace http diff --git a/src/httpcommon.h b/src/httpcommon.h index 324ada03d23..8b9799943b4 100644 --- a/src/httpcommon.h +++ b/src/httpcommon.h @@ -4,30 +4,28 @@ */ #pragma once +// lib includes +#include + +// local includes #include "network.h" #include "thread_safe.h" namespace http { - int - init(); - int - create_creds(const std::string &pkey, const std::string &cert); - int - save_user_creds( + int init(); + int create_creds(const std::string &pkey, const std::string &cert); + int save_user_creds( const std::string &file, const std::string &username, const std::string &password, - bool run_our_mouth = false); + bool run_our_mouth = false + ); - int - reload_user_creds(const std::string &file); - bool - download_file(const std::string &url, const std::string &file); - std::string - url_escape(const std::string &url); - std::string - url_get_host(const std::string &url); + int reload_user_creds(const std::string &file); + bool download_file(const std::string &url, const std::string &file, long ssl_version = CURL_SSLVERSION_TLSv1_2); + std::string url_escape(const std::string &url); + std::string url_get_host(const std::string &url); extern std::string unique_id; extern net::net_e origin_web_ui_allowed; diff --git a/src/input.cpp b/src/input.cpp index bf275467d87..768ddece213 100644 --- a/src/input.cpp +++ b/src/input.cpp @@ -2,13 +2,13 @@ * @file src/input.cpp * @brief Definitions for gamepad, keyboard, and mouse input handling. */ -// define uint32_t for #include extern "C" { #include #include } +// standard includes #include #include #include @@ -16,6 +16,10 @@ extern "C" { #include #include +// lib includes +#include + +// local includes #include "config.h" #include "globals.h" #include "input.h" @@ -24,14 +28,13 @@ extern "C" { #include "thread_pool.h" #include "utility.h" -#include - // Win32 WHEEL_DELTA constant #ifndef WHEEL_DELTA #define WHEEL_DELTA 120 #endif using namespace std::literals; + namespace input { constexpr auto MAX_GAMEPADS = std::min((std::size_t) platf::MAX_GAMEPADS, sizeof(std::int16_t) * 8); @@ -54,9 +57,8 @@ namespace input { UP ///< Button is up }; - template - int - alloc_id(std::bitset &gamepad_mask) { + template + int alloc_id(std::bitset &gamepad_mask) { for (int x = 0; x < gamepad_mask.size(); ++x) { if (!gamepad_mask[x]) { gamepad_mask[x] = true; @@ -67,23 +69,22 @@ namespace input { return -1; } - template - void - free_id(std::bitset &gamepad_mask, int id) { + template + void free_id(std::bitset &gamepad_mask, int id) { gamepad_mask[id] = false; } typedef uint32_t key_press_id_t; - key_press_id_t - make_kpid(uint16_t vk, uint8_t flags) { + + key_press_id_t make_kpid(uint16_t vk, uint8_t flags) { return (key_press_id_t) vk << 8 | flags; } - uint16_t - vk_from_kpid(key_press_id_t kpid) { + + uint16_t vk_from_kpid(key_press_id_t kpid) { return kpid >> 8; } - uint8_t - flags_from_kpid(key_press_id_t kpid) { + + uint8_t flags_from_kpid(key_press_id_t kpid) { return kpid & 0xFF; } @@ -92,8 +93,7 @@ namespace input { * @param f Netfloat value. * @return The native endianness float value. */ - float - from_netfloat(netfloat f) { + float from_netfloat(netfloat f) { return boost::endian::endian_load(f); } @@ -104,8 +104,7 @@ namespace input { * @param max The maximum value for clamping. * @return Clamped native endianess float value. */ - float - from_clamped_netfloat(netfloat f, float min, float max) { + float from_clamped_netfloat(netfloat f, float min, float max) { return std::clamp(from_netfloat(f), min, max); } @@ -116,16 +115,21 @@ namespace input { static platf::input_t platf_input; static std::bitset gamepadMask {}; - void - free_gamepad(platf::input_t &platf_input, int id) { + void free_gamepad(platf::input_t &platf_input, int id) { platf::gamepad_update(platf_input, id, platf::gamepad_state_t {}); platf::free_gamepad(platf_input, id); free_id(gamepadMask, id); } + struct gamepad_t { gamepad_t(): - gamepad_state {}, back_timeout_id {}, id { -1 }, back_button_state { button_state_e::NONE } {} + gamepad_state {}, + back_timeout_id {}, + id {-1}, + back_button_state {button_state_e::NONE} { + } + ~gamepad_t() { if (id >= 0) { task_pool.push([id = this->id]() { @@ -158,16 +162,18 @@ namespace input { input_t( safe::mail_raw_t::event_t touch_port_event, - platf::feedback_queue_t feedback_queue): + platf::feedback_queue_t feedback_queue + ): shortcutFlags {}, gamepads(MAX_GAMEPADS), - client_context { platf::allocate_client_input_context(platf_input) }, - touch_port_event { std::move(touch_port_event) }, - feedback_queue { std::move(feedback_queue) }, + client_context {platf::allocate_client_input_context(platf_input)}, + touch_port_event {std::move(touch_port_event)}, + feedback_queue {std::move(feedback_queue)}, mouse_left_button_timeout {}, - touch_port { { 0, 0, 0, 0 }, 0, 0, 1.0f }, + touch_port {{0, 0, 0, 0}, 0, 0, 1.0f}, accumulated_vscroll_delta {}, - accumulated_hscroll_delta {} {} + accumulated_hscroll_delta {} { + } // Keep track of alt+ctrl+shift key combo int shortcutFlags; @@ -194,8 +200,7 @@ namespace input { * @param keyCode The VKEY code * @return 0 if no shortcut applied, > 0 if shortcut applied. */ - inline int - apply_shortcut(short keyCode) { + inline int apply_shortcut(short keyCode) { constexpr auto VK_F1 = 0x70; constexpr auto VK_F13 = 0x7C; @@ -215,8 +220,7 @@ namespace input { return 0; } - void - print(PNV_REL_MOUSE_MOVE_PACKET packet) { + void print(PNV_REL_MOUSE_MOVE_PACKET packet) { BOOST_LOG(debug) << "--begin relative mouse move packet--"sv << std::endl << "deltaX ["sv << util::endian::big(packet->deltaX) << ']' << std::endl @@ -224,8 +228,7 @@ namespace input { << "--end relative mouse move packet--"sv; } - void - print(PNV_ABS_MOUSE_MOVE_PACKET packet) { + void print(PNV_ABS_MOUSE_MOVE_PACKET packet) { BOOST_LOG(debug) << "--begin absolute mouse move packet--"sv << std::endl << "x ["sv << util::endian::big(packet->x) << ']' << std::endl @@ -235,8 +238,7 @@ namespace input { << "--end absolute mouse move packet--"sv; } - void - print(PNV_MOUSE_BUTTON_PACKET packet) { + void print(PNV_MOUSE_BUTTON_PACKET packet) { BOOST_LOG(debug) << "--begin mouse button packet--"sv << std::endl << "action ["sv << util::hex(packet->header.magic).to_string_view() << ']' << std::endl @@ -244,24 +246,21 @@ namespace input { << "--end mouse button packet--"sv; } - void - print(PNV_SCROLL_PACKET packet) { + void print(PNV_SCROLL_PACKET packet) { BOOST_LOG(debug) << "--begin mouse scroll packet--"sv << std::endl << "scrollAmt1 ["sv << util::endian::big(packet->scrollAmt1) << ']' << std::endl << "--end mouse scroll packet--"sv; } - void - print(PSS_HSCROLL_PACKET packet) { + void print(PSS_HSCROLL_PACKET packet) { BOOST_LOG(debug) << "--begin mouse hscroll packet--"sv << std::endl << "scrollAmount ["sv << util::endian::big(packet->scrollAmount) << ']' << std::endl << "--end mouse hscroll packet--"sv; } - void - print(PNV_KEYBOARD_PACKET packet) { + void print(PNV_KEYBOARD_PACKET packet) { BOOST_LOG(debug) << "--begin keyboard packet--"sv << std::endl << "keyAction ["sv << util::hex(packet->header.magic).to_string_view() << ']' << std::endl @@ -271,8 +270,7 @@ namespace input { << "--end keyboard packet--"sv; } - void - print(PNV_UNICODE_PACKET packet) { + void print(PNV_UNICODE_PACKET packet) { std::string text(packet->text, util::endian::big(packet->header.size) - sizeof(packet->header.magic)); BOOST_LOG(debug) << "--begin unicode packet--"sv << std::endl @@ -280,8 +278,7 @@ namespace input { << "--end unicode packet--"sv; } - void - print(PNV_MULTI_CONTROLLER_PACKET packet) { + void print(PNV_MULTI_CONTROLLER_PACKET packet) { // Moonlight spams controller packet even when not necessary BOOST_LOG(verbose) << "--begin controller packet--"sv << std::endl @@ -301,8 +298,7 @@ namespace input { * @brief Prints a touch packet. * @param packet The touch packet. */ - void - print(PSS_TOUCH_PACKET packet) { + void print(PSS_TOUCH_PACKET packet) { BOOST_LOG(debug) << "--begin touch packet--"sv << std::endl << "eventType ["sv << util::hex(packet->eventType).to_string_view() << ']' << std::endl @@ -320,8 +316,7 @@ namespace input { * @brief Prints a pen packet. * @param packet The pen packet. */ - void - print(PSS_PEN_PACKET packet) { + void print(PSS_PEN_PACKET packet) { BOOST_LOG(debug) << "--begin pen packet--"sv << std::endl << "eventType ["sv << util::hex(packet->eventType).to_string_view() << ']' << std::endl @@ -341,8 +336,7 @@ namespace input { * @brief Prints a controller arrival packet. * @param packet The controller arrival packet. */ - void - print(PSS_CONTROLLER_ARRIVAL_PACKET packet) { + void print(PSS_CONTROLLER_ARRIVAL_PACKET packet) { BOOST_LOG(debug) << "--begin controller arrival packet--"sv << std::endl << "controllerNumber ["sv << (uint32_t) packet->controllerNumber << ']' << std::endl @@ -356,8 +350,7 @@ namespace input { * @brief Prints a controller touch packet. * @param packet The controller touch packet. */ - void - print(PSS_CONTROLLER_TOUCH_PACKET packet) { + void print(PSS_CONTROLLER_TOUCH_PACKET packet) { BOOST_LOG(debug) << "--begin controller touch packet--"sv << std::endl << "controllerNumber ["sv << (uint32_t) packet->controllerNumber << ']' << std::endl @@ -373,8 +366,7 @@ namespace input { * @brief Prints a controller motion packet. * @param packet The controller motion packet. */ - void - print(PSS_CONTROLLER_MOTION_PACKET packet) { + void print(PSS_CONTROLLER_MOTION_PACKET packet) { BOOST_LOG(verbose) << "--begin controller motion packet--"sv << std::endl << "controllerNumber ["sv << util::hex(packet->controllerNumber).to_string_view() << ']' << std::endl @@ -389,8 +381,7 @@ namespace input { * @brief Prints a controller battery packet. * @param packet The controller battery packet. */ - void - print(PSS_CONTROLLER_BATTERY_PACKET packet) { + void print(PSS_CONTROLLER_BATTERY_PACKET packet) { BOOST_LOG(verbose) << "--begin controller battery packet--"sv << std::endl << "controllerNumber ["sv << util::hex(packet->controllerNumber).to_string_view() << ']' << std::endl @@ -399,8 +390,7 @@ namespace input { << "--end controller battery packet--"sv; } - void - print(void *payload) { + void print(void *payload) { auto header = (PNV_INPUT_HEADER) payload; switch (util::endian::little(header->magic)) { @@ -451,8 +441,7 @@ namespace input { } } - void - passthrough(std::shared_ptr &input, PNV_REL_MOUSE_MOVE_PACKET packet) { + void passthrough(std::shared_ptr &input, PNV_REL_MOUSE_MOVE_PACKET packet) { if (!config::input.mouse) { return; } @@ -468,8 +457,7 @@ namespace input { * @param size The size of the client's surface containing the value. * @return The host-relative coordinate pair if a touchport is available. */ - std::optional> - client_to_touchport(std::shared_ptr &input, const std::pair &val, const std::pair &size) { + std::optional> client_to_touchport(std::shared_ptr &input, const std::pair &val, const std::pair &size) { auto &touch_port_event = input->touch_port_event; auto &touch_port = input->touch_port; if (touch_port_event->peek()) { @@ -492,7 +480,7 @@ namespace input { x = std::clamp(x, offsetX, (size.first * scalarX) - offsetX); y = std::clamp(y, offsetY, (size.second * scalarY) - offsetY); - return std::pair { (x - offsetX) * touch_port.scalar_inv, (y - offsetY) * touch_port.scalar_inv }; + return std::pair {(x - offsetX) * touch_port.scalar_inv, (y - offsetY) * touch_port.scalar_inv}; } /** @@ -502,8 +490,7 @@ namespace input { * @param scalar The scalar cartesian coordinate pair. * @return The scaled radial coordinate. */ - float - multiply_polar_by_cartesian_scalar(float r, float angle, const std::pair &scalar) { + float multiply_polar_by_cartesian_scalar(float r, float angle, const std::pair &scalar) { // Convert polar to cartesian coordinates float x = r * std::cos(angle); float y = r * std::sin(angle); @@ -516,8 +503,7 @@ namespace input { return std::sqrt(std::pow(x, 2) + std::pow(y, 2)); } - std::pair - scale_client_contact_area(const std::pair &val, uint16_t rotation, const std::pair &scalar) { + std::pair scale_client_contact_area(const std::pair &val, uint16_t rotation, const std::pair &scalar) { // If the rotation is unknown, we'll just scale both axes equally by using // a 45-degree angle for our scaling calculations float angle = rotation == LI_ROT_UNKNOWN ? (M_PI / 4) : (rotation * (M_PI / 180)); @@ -527,11 +513,10 @@ namespace input { float minor = val.second != 0.0f ? val.second : val.first; // The minor axis is perpendicular to major axis so the angle must be rotated by 90 degrees - return { multiply_polar_by_cartesian_scalar(major, angle, scalar), multiply_polar_by_cartesian_scalar(minor, angle + (M_PI / 2), scalar) }; + return {multiply_polar_by_cartesian_scalar(major, angle, scalar), multiply_polar_by_cartesian_scalar(minor, angle + (M_PI / 2), scalar)}; } - void - passthrough(std::shared_ptr &input, PNV_ABS_MOUSE_MOVE_PACKET packet) { + void passthrough(std::shared_ptr &input, PNV_ABS_MOUSE_MOVE_PACKET packet) { if (!config::input.mouse) { return; } @@ -554,22 +539,23 @@ namespace input { auto width = (float) util::endian::big(packet->width); auto height = (float) util::endian::big(packet->height); - auto tpcoords = client_to_touchport(input, { x, y }, { width, height }); + auto tpcoords = client_to_touchport(input, {x, y}, {width, height}); if (!tpcoords) { return; } auto &touch_port = input->touch_port; platf::touch_port_t abs_port { - touch_port.offset_x, touch_port.offset_y, - touch_port.env_width, touch_port.env_height + touch_port.offset_x, + touch_port.offset_y, + touch_port.env_width, + touch_port.env_height }; platf::abs_mouse(platf_input, abs_port, tpcoords->first, tpcoords->second); } - void - passthrough(std::shared_ptr &input, PNV_MOUSE_BUTTON_PACKET packet) { + void passthrough(std::shared_ptr &input, PNV_MOUSE_BUTTON_PACKET packet) { if (!config::input.mouse) { return; } @@ -617,7 +603,8 @@ namespace input { } if ( button == BUTTON_RIGHT && !release && - input->mouse_left_button_timeout > DISABLE_LEFT_BUTTON_DELAY) { + input->mouse_left_button_timeout > DISABLE_LEFT_BUTTON_DELAY + ) { platf::button_mouse(platf_input, BUTTON_RIGHT, false); platf::button_mouse(platf_input, BUTTON_RIGHT, true); @@ -629,8 +616,7 @@ namespace input { platf::button_mouse(platf_input, button, release); } - short - map_keycode(short keycode) { + short map_keycode(short keycode) { auto it = config::input.keybindings.find(keycode); if (it != std::end(config::input.keybindings)) { return it->second; @@ -642,16 +628,14 @@ namespace input { /** * @brief Update flags for keyboard shortcut combo's */ - inline void - update_shortcutFlags(int *flags, short keyCode, bool release) { + inline void update_shortcutFlags(int *flags, short keyCode, bool release) { switch (keyCode) { case VKEY_SHIFT: case VKEY_LSHIFT: case VKEY_RSHIFT: if (release) { *flags &= ~input_t::SHIFT; - } - else { + } else { *flags |= input_t::SHIFT; } break; @@ -660,8 +644,7 @@ namespace input { case VKEY_RCONTROL: if (release) { *flags &= ~input_t::CTRL; - } - else { + } else { *flags |= input_t::CTRL; } break; @@ -670,16 +653,14 @@ namespace input { case VKEY_RMENU: if (release) { *flags &= ~input_t::ALT; - } - else { + } else { *flags |= input_t::ALT; } break; } } - bool - is_modifier(uint16_t keyCode) { + bool is_modifier(uint16_t keyCode) { switch (keyCode) { case VKEY_SHIFT: case VKEY_LSHIFT: @@ -696,8 +677,7 @@ namespace input { } } - void - send_key_and_modifiers(uint16_t key_code, bool release, uint8_t flags, uint8_t synthetic_modifiers) { + void send_key_and_modifiers(uint16_t key_code, bool release, uint8_t flags, uint8_t synthetic_modifiers) { if (!release) { // Press any synthetic modifiers required for this key if (synthetic_modifiers & MODIFIER_SHIFT) { @@ -727,8 +707,7 @@ namespace input { } } - void - repeat_key(uint16_t key_code, uint8_t flags, uint8_t synthetic_modifiers) { + void repeat_key(uint16_t key_code, uint8_t flags, uint8_t synthetic_modifiers) { // If key no longer pressed, stop repeating if (!key_press[make_kpid(key_code, flags)]) { key_press_repeat_id = nullptr; @@ -740,8 +719,7 @@ namespace input { key_press_repeat_id = task_pool.pushDelayed(repeat_key, config::input.key_repeat_period, key_code, flags, synthetic_modifiers).task_id; } - void - passthrough(std::shared_ptr &input, PNV_KEYBOARD_PACKET packet) { + void passthrough(std::shared_ptr &input, PNV_KEYBOARD_PACKET packet) { if (!config::input.keyboard) { return; } @@ -780,13 +758,11 @@ namespace input { if (config::input.key_repeat_delay.count() > 0) { key_press_repeat_id = task_pool.pushDelayed(repeat_key, config::input.key_repeat_delay, keyCode, packet->flags, synthetic_modifiers).task_id; } - } - else { + } else { // Already released return; } - } - else if (!release) { + } else if (!release) { // Already pressed down key return; } @@ -803,16 +779,14 @@ namespace input { * @param input The input context pointer. * @param packet The scroll packet. */ - void - passthrough(std::shared_ptr &input, PNV_SCROLL_PACKET packet) { + void passthrough(std::shared_ptr &input, PNV_SCROLL_PACKET packet) { if (!config::input.mouse) { return; } if (config::input.high_resolution_scrolling) { platf::scroll(platf_input, util::endian::big(packet->scrollAmt1)); - } - else { + } else { input->accumulated_vscroll_delta += util::endian::big(packet->scrollAmt1); auto full_ticks = input->accumulated_vscroll_delta / WHEEL_DELTA; if (full_ticks) { @@ -828,16 +802,14 @@ namespace input { * @param input The input context pointer. * @param packet The scroll packet. */ - void - passthrough(std::shared_ptr &input, PSS_HSCROLL_PACKET packet) { + void passthrough(std::shared_ptr &input, PSS_HSCROLL_PACKET packet) { if (!config::input.mouse) { return; } if (config::input.high_resolution_scrolling) { platf::hscroll(platf_input, util::endian::big(packet->scrollAmount)); - } - else { + } else { input->accumulated_hscroll_delta += util::endian::big(packet->scrollAmount); auto full_ticks = input->accumulated_hscroll_delta / WHEEL_DELTA; if (full_ticks) { @@ -848,8 +820,7 @@ namespace input { } } - void - passthrough(PNV_UNICODE_PACKET packet) { + void passthrough(PNV_UNICODE_PACKET packet) { if (!config::input.keyboard) { return; } @@ -863,8 +834,7 @@ namespace input { * @param input The input context pointer. * @param packet The controller arrival packet. */ - void - passthrough(std::shared_ptr &input, PSS_CONTROLLER_ARRIVAL_PACKET packet) { + void passthrough(std::shared_ptr &input, PSS_CONTROLLER_ARRIVAL_PACKET packet) { if (!config::input.controller) { return; } @@ -891,7 +861,7 @@ namespace input { } // Allocate a new gamepad - if (platf::alloc_gamepad(platf_input, { id, packet->controllerNumber }, arrival, input->feedback_queue)) { + if (platf::alloc_gamepad(platf_input, {id, packet->controllerNumber}, arrival, input->feedback_queue)) { free_id(gamepadMask, id); return; } @@ -904,25 +874,23 @@ namespace input { * @param input The input context pointer. * @param packet The touch packet. */ - void - passthrough(std::shared_ptr &input, PSS_TOUCH_PACKET packet) { + void passthrough(std::shared_ptr &input, PSS_TOUCH_PACKET packet) { if (!config::input.mouse) { return; } // Convert the client normalized coordinates to touchport coordinates - auto coords = client_to_touchport(input, - { from_clamped_netfloat(packet->x, 0.0f, 1.0f) * 65535.f, - from_clamped_netfloat(packet->y, 0.0f, 1.0f) * 65535.f }, - { 65535.f, 65535.f }); + auto coords = client_to_touchport(input, {from_clamped_netfloat(packet->x, 0.0f, 1.0f) * 65535.f, from_clamped_netfloat(packet->y, 0.0f, 1.0f) * 65535.f}, {65535.f, 65535.f}); if (!coords) { return; } auto &touch_port = input->touch_port; platf::touch_port_t abs_port { - touch_port.offset_x, touch_port.offset_y, - touch_port.env_width, touch_port.env_height + touch_port.offset_x, + touch_port.offset_y, + touch_port.env_width, + touch_port.env_height }; // Renormalize the coordinates @@ -937,10 +905,11 @@ namespace input { // Normalize the contact area based on the touchport auto contact_area = scale_client_contact_area( - { from_clamped_netfloat(packet->contactAreaMajor, 0.0f, 1.0f) * 65535.f, - from_clamped_netfloat(packet->contactAreaMinor, 0.0f, 1.0f) * 65535.f }, + {from_clamped_netfloat(packet->contactAreaMajor, 0.0f, 1.0f) * 65535.f, + from_clamped_netfloat(packet->contactAreaMinor, 0.0f, 1.0f) * 65535.f}, rotation, - { abs_port.width / 65535.f, abs_port.height / 65535.f }); + {abs_port.width / 65535.f, abs_port.height / 65535.f} + ); platf::touch_input_t touch { packet->eventType, @@ -961,25 +930,23 @@ namespace input { * @param input The input context pointer. * @param packet The pen packet. */ - void - passthrough(std::shared_ptr &input, PSS_PEN_PACKET packet) { + void passthrough(std::shared_ptr &input, PSS_PEN_PACKET packet) { if (!config::input.mouse) { return; } // Convert the client normalized coordinates to touchport coordinates - auto coords = client_to_touchport(input, - { from_clamped_netfloat(packet->x, 0.0f, 1.0f) * 65535.f, - from_clamped_netfloat(packet->y, 0.0f, 1.0f) * 65535.f }, - { 65535.f, 65535.f }); + auto coords = client_to_touchport(input, {from_clamped_netfloat(packet->x, 0.0f, 1.0f) * 65535.f, from_clamped_netfloat(packet->y, 0.0f, 1.0f) * 65535.f}, {65535.f, 65535.f}); if (!coords) { return; } auto &touch_port = input->touch_port; platf::touch_port_t abs_port { - touch_port.offset_x, touch_port.offset_y, - touch_port.env_width, touch_port.env_height + touch_port.offset_x, + touch_port.offset_y, + touch_port.env_width, + touch_port.env_height }; // Renormalize the coordinates @@ -994,10 +961,11 @@ namespace input { // Normalize the contact area based on the touchport auto contact_area = scale_client_contact_area( - { from_clamped_netfloat(packet->contactAreaMajor, 0.0f, 1.0f) * 65535.f, - from_clamped_netfloat(packet->contactAreaMinor, 0.0f, 1.0f) * 65535.f }, + {from_clamped_netfloat(packet->contactAreaMajor, 0.0f, 1.0f) * 65535.f, + from_clamped_netfloat(packet->contactAreaMinor, 0.0f, 1.0f) * 65535.f}, rotation, - { abs_port.width / 65535.f, abs_port.height / 65535.f }); + {abs_port.width / 65535.f, abs_port.height / 65535.f} + ); platf::pen_input_t pen { packet->eventType, @@ -1020,8 +988,7 @@ namespace input { * @param input The input context pointer. * @param packet The controller touch packet. */ - void - passthrough(std::shared_ptr &input, PSS_CONTROLLER_TOUCH_PACKET packet) { + void passthrough(std::shared_ptr &input, PSS_CONTROLLER_TOUCH_PACKET packet) { if (!config::input.controller) { return; } @@ -1038,7 +1005,7 @@ namespace input { } platf::gamepad_touch_t touch { - { gamepad.id, packet->controllerNumber }, + {gamepad.id, packet->controllerNumber}, packet->eventType, util::endian::little(packet->pointerId), from_clamped_netfloat(packet->x, 0.0f, 1.0f), @@ -1054,8 +1021,7 @@ namespace input { * @param input The input context pointer. * @param packet The controller motion packet. */ - void - passthrough(std::shared_ptr &input, PSS_CONTROLLER_MOTION_PACKET packet) { + void passthrough(std::shared_ptr &input, PSS_CONTROLLER_MOTION_PACKET packet) { if (!config::input.controller) { return; } @@ -1072,7 +1038,7 @@ namespace input { } platf::gamepad_motion_t motion { - { gamepad.id, packet->controllerNumber }, + {gamepad.id, packet->controllerNumber}, packet->motionType, from_netfloat(packet->x), from_netfloat(packet->y), @@ -1087,8 +1053,7 @@ namespace input { * @param input The input context pointer. * @param packet The controller battery packet. */ - void - passthrough(std::shared_ptr &input, PSS_CONTROLLER_BATTERY_PACKET packet) { + void passthrough(std::shared_ptr &input, PSS_CONTROLLER_BATTERY_PACKET packet) { if (!config::input.controller) { return; } @@ -1105,7 +1070,7 @@ namespace input { } platf::gamepad_battery_t battery { - { gamepad.id, packet->controllerNumber }, + {gamepad.id, packet->controllerNumber}, packet->batteryState, packet->batteryPercentage }; @@ -1113,8 +1078,7 @@ namespace input { platf::gamepad_battery(platf_input, battery); } - void - passthrough(std::shared_ptr &input, PNV_MULTI_CONTROLLER_PACKET packet) { + void passthrough(std::shared_ptr &input, PNV_MULTI_CONTROLLER_PACKET packet) { if (!config::input.controller) { return; } @@ -1135,14 +1099,13 @@ namespace input { return; } - if (platf::alloc_gamepad(platf_input, { id, (uint8_t) packet->controllerNumber }, {}, input->feedback_queue)) { + if (platf::alloc_gamepad(platf_input, {id, (uint8_t) packet->controllerNumber}, {}, input->feedback_queue)) { free_id(gamepadMask, id); return; } gamepad.id = id; - } - else if (!(packet->activeGamepadMask & (1 << packet->controllerNumber)) && gamepad.id >= 0) { + } else if (!(packet->activeGamepadMask & (1 << packet->controllerNumber)) && gamepad.id >= 0) { // If this is the final event for a gamepad being removed, free the gamepad and return. free_gamepad(platf_input, gamepad.id); gamepad.id = -1; @@ -1219,8 +1182,7 @@ namespace input { gamepad.back_timeout_id = task_pool.pushDelayed(std::move(f), config::input.back_button_timeout).task_id; } - } - else if (gamepad.back_timeout_id) { + } else if (gamepad.back_timeout_id) { task_pool.cancel(gamepad.back_timeout_id); gamepad.back_timeout_id = nullptr; } @@ -1243,8 +1205,7 @@ namespace input { * @param src A later packet to attempt to batch. * @return The status of the batching operation. */ - batch_result_e - batch(PNV_REL_MOUSE_MOVE_PACKET dest, PNV_REL_MOUSE_MOVE_PACKET src) { + batch_result_e batch(PNV_REL_MOUSE_MOVE_PACKET dest, PNV_REL_MOUSE_MOVE_PACKET src) { short deltaX, deltaY; // Batching is safe as long as the result doesn't overflow a 16-bit integer @@ -1267,8 +1228,7 @@ namespace input { * @param src A later packet to attempt to batch. * @return The status of the batching operation. */ - batch_result_e - batch(PNV_ABS_MOUSE_MOVE_PACKET dest, PNV_ABS_MOUSE_MOVE_PACKET src) { + batch_result_e batch(PNV_ABS_MOUSE_MOVE_PACKET dest, PNV_ABS_MOUSE_MOVE_PACKET src) { // Batching must only happen if the reference width and height don't change if (dest->width != src->width || dest->height != src->height) { return batch_result_e::terminate_batch; @@ -1285,8 +1245,7 @@ namespace input { * @param src A later packet to attempt to batch. * @return The status of the batching operation. */ - batch_result_e - batch(PNV_SCROLL_PACKET dest, PNV_SCROLL_PACKET src) { + batch_result_e batch(PNV_SCROLL_PACKET dest, PNV_SCROLL_PACKET src) { short scrollAmt; // Batching is safe as long as the result doesn't overflow a 16-bit integer @@ -1306,8 +1265,7 @@ namespace input { * @param src A later packet to attempt to batch. * @return The status of the batching operation. */ - batch_result_e - batch(PSS_HSCROLL_PACKET dest, PSS_HSCROLL_PACKET src) { + batch_result_e batch(PSS_HSCROLL_PACKET dest, PSS_HSCROLL_PACKET src) { short scrollAmt; // Batching is safe as long as the result doesn't overflow a 16-bit integer @@ -1326,8 +1284,7 @@ namespace input { * @param src A later packet to attempt to batch. * @return The status of the batching operation. */ - batch_result_e - batch(PNV_MULTI_CONTROLLER_PACKET dest, PNV_MULTI_CONTROLLER_PACKET src) { + batch_result_e batch(PNV_MULTI_CONTROLLER_PACKET dest, PNV_MULTI_CONTROLLER_PACKET src) { // Do not allow batching if the active controllers change if (dest->activeGamepadMask != src->activeGamepadMask) { return batch_result_e::terminate_batch; @@ -1355,8 +1312,7 @@ namespace input { * @param src A later packet to attempt to batch. * @return The status of the batching operation. */ - batch_result_e - batch(PSS_TOUCH_PACKET dest, PSS_TOUCH_PACKET src) { + batch_result_e batch(PSS_TOUCH_PACKET dest, PSS_TOUCH_PACKET src) { // Only batch hover or move events if (dest->eventType != LI_TOUCH_EVENT_MOVE && dest->eventType != LI_TOUCH_EVENT_HOVER) { @@ -1390,8 +1346,7 @@ namespace input { * @param src A later packet to attempt to batch. * @return The status of the batching operation. */ - batch_result_e - batch(PSS_PEN_PACKET dest, PSS_PEN_PACKET src) { + batch_result_e batch(PSS_PEN_PACKET dest, PSS_PEN_PACKET src) { // Only batch hover or move events if (dest->eventType != LI_TOUCH_EVENT_MOVE && dest->eventType != LI_TOUCH_EVENT_HOVER) { @@ -1424,8 +1379,7 @@ namespace input { * @param src A later packet to attempt to batch. * @return The status of the batching operation. */ - batch_result_e - batch(PSS_CONTROLLER_TOUCH_PACKET dest, PSS_CONTROLLER_TOUCH_PACKET src) { + batch_result_e batch(PSS_CONTROLLER_TOUCH_PACKET dest, PSS_CONTROLLER_TOUCH_PACKET src) { // Only batch hover or move events if (dest->eventType != LI_TOUCH_EVENT_MOVE && dest->eventType != LI_TOUCH_EVENT_HOVER) { @@ -1465,8 +1419,7 @@ namespace input { * @param src A later packet to attempt to batch. * @return The status of the batching operation. */ - batch_result_e - batch(PSS_CONTROLLER_MOTION_PACKET dest, PSS_CONTROLLER_MOTION_PACKET src) { + batch_result_e batch(PSS_CONTROLLER_MOTION_PACKET dest, PSS_CONTROLLER_MOTION_PACKET src) { // We can only batch entries for the same controller, but allow batching attempts to continue // in case we have more packets for this controller later in the queue. if (dest->controllerNumber != src->controllerNumber) { @@ -1489,8 +1442,7 @@ namespace input { * @param src A later packet to attempt to batch. * @return The status of the batching operation. */ - batch_result_e - batch(PNV_INPUT_HEADER dest, PNV_INPUT_HEADER src) { + batch_result_e batch(PNV_INPUT_HEADER dest, PNV_INPUT_HEADER src) { // We can only batch if the packet types are the same if (dest->magic != src->magic) { return batch_result_e::terminate_batch; @@ -1526,8 +1478,7 @@ namespace input { * @brief Called on a thread pool thread to process an input message. * @param input The input context pointer. */ - void - passthrough_next_message(std::shared_ptr input) { + void passthrough_next_message(std::shared_ptr input) { // 'entry' backs the 'payload' pointer, so they must remain in scope together std::vector entry; PNV_INPUT_HEADER payload; @@ -1558,12 +1509,10 @@ namespace input { if (batch_result == batch_result_e::terminate_batch) { // Stop batching break; - } - else if (batch_result == batch_result_e::batched) { + } else if (batch_result == batch_result_e::batched) { // Erase this entry since it was batched i = input->input_queue.erase(i); - } - else { + } else { // We couldn't batch this entry, but try to batch later entries. i++; } @@ -1627,8 +1576,7 @@ namespace input { * @param input The input context pointer. * @param input_data The input message. */ - void - passthrough(std::shared_ptr &input, std::vector &&input_data) { + void passthrough(std::shared_ptr &input, std::vector &&input_data) { { std::lock_guard lg(input->input_queue_lock); input->input_queue.push_back(std::move(input_data)); @@ -1636,8 +1584,7 @@ namespace input { task_pool.push(passthrough_next_message, input); } - void - reset(std::shared_ptr &input) { + void reset(std::shared_ptr &input) { task_pool.cancel(key_press_repeat_id); task_pool.cancel(input->mouse_left_button_timeout); @@ -1668,15 +1615,13 @@ namespace input { } }; - [[nodiscard]] std::unique_ptr - init() { + [[nodiscard]] std::unique_ptr init() { platf_input = platf::input(); return std::make_unique(); } - bool - probe_gamepads() { + bool probe_gamepads() { auto input = static_cast(platf_input.get()); const auto gamepads = platf::supported_gamepads(input); for (auto &gamepad : gamepads) { @@ -1687,18 +1632,18 @@ namespace input { return true; } - std::shared_ptr - alloc(safe::mail_t mail) { + std::shared_ptr alloc(safe::mail_t mail) { auto input = std::make_shared( mail->event(mail::touch_port), - mail->queue(mail::gamepad_feedback)); + mail->queue(mail::gamepad_feedback) + ); // Workaround to ensure new frames will be captured when a client connects task_pool.pushDelayed([]() { platf::move_mouse(platf_input, 1, 1); platf::move_mouse(platf_input, -1, -1); }, - 100ms); + 100ms); return input; } diff --git a/src/input.h b/src/input.h index a20c9c305b3..96b2f457f28 100644 --- a/src/input.h +++ b/src/input.h @@ -4,29 +4,25 @@ */ #pragma once +// standard includes #include +// local includes #include "platform/common.h" #include "thread_safe.h" namespace input { struct input_t; - void - print(void *input); - void - reset(std::shared_ptr &input); - void - passthrough(std::shared_ptr &input, std::vector &&input_data); + void print(void *input); + void reset(std::shared_ptr &input); + void passthrough(std::shared_ptr &input, std::vector &&input_data); - [[nodiscard]] std::unique_ptr - init(); + [[nodiscard]] std::unique_ptr init(); - bool - probe_gamepads(); + bool probe_gamepads(); - std::shared_ptr - alloc(safe::mail_t mail); + std::shared_ptr alloc(safe::mail_t mail); struct touch_port_t: public platf::touch_port_t { int env_width, env_height; @@ -36,8 +32,7 @@ namespace input { float scalar_inv; - explicit - operator bool() const { + explicit operator bool() const { return width != 0 && height != 0 && env_width != 0 && env_height != 0; } }; @@ -49,6 +44,5 @@ namespace input { * @param scalar The scalar cartesian coordinate pair. * @return The major and minor axis pair. */ - std::pair - scale_client_contact_area(const std::pair &val, uint16_t rotation, const std::pair &scalar); + std::pair scale_client_contact_area(const std::pair &val, uint16_t rotation, const std::pair &scalar); } // namespace input diff --git a/src/logging.cpp b/src/logging.cpp index 6d1085630d9..1913eef162e 100644 --- a/src/logging.cpp +++ b/src/logging.cpp @@ -15,6 +15,7 @@ #include #include #include +#include // local includes #include "logging.h" @@ -46,15 +47,13 @@ namespace logging { deinit(); } - void - deinit() { + void deinit() { log_flush(); bl::core::get()->remove_sink(sink); sink.reset(); } - void - formatter(const boost::log::record_view &view, boost::log::formatting_ostream &os) { + void formatter(const boost::log::record_view &view, boost::log::formatting_ostream &os) { constexpr const char *message = "Message"; constexpr const char *severity = "Severity"; @@ -89,7 +88,8 @@ namespace logging { auto now = std::chrono::system_clock::now(); auto ms = std::chrono::duration_cast( - now - std::chrono::time_point_cast(now)); + now - std::chrono::time_point_cast(now) + ); auto t = std::chrono::system_clock::to_time_t(now); auto lt = *std::localtime(&t); @@ -98,19 +98,19 @@ namespace logging { << log_type << view.attribute_values()[message].extract(); } - [[nodiscard]] std::unique_ptr - init(int min_log_level, const std::string &log_file) { + [[nodiscard]] std::unique_ptr init(int min_log_level, const std::string &log_file) { if (sink) { // Deinitialize the logging system before reinitializing it. This can probably only ever be hit in tests. deinit(); } setup_av_logging(min_log_level); + setup_libdisplaydevice_logging(min_log_level); sink = boost::make_shared(); #ifndef SUNSHINE_TESTS - boost::shared_ptr stream { &std::cout, boost::null_deleter() }; + boost::shared_ptr stream {&std::cout, boost::null_deleter()}; sink->locked_backend()->add_stream(stream); #endif sink->locked_backend()->add_stream(boost::make_shared(log_file)); @@ -125,12 +125,10 @@ namespace logging { return std::make_unique(); } - void - setup_av_logging(int min_log_level) { + void setup_av_logging(int min_log_level) { if (min_log_level >= 1) { av_log_set_level(AV_LOG_QUIET); - } - else { + } else { av_log_set_level(AV_LOG_DEBUG); } av_log_set_callback([](void *ptr, int level, const char *fmt, va_list vl) { @@ -142,32 +140,56 @@ namespace logging { // We print AV_LOG_FATAL at the error level. FFmpeg prints things as fatal that // are expected in some cases, such as lack of codec support or similar things. BOOST_LOG(error) << buffer; - } - else if (level <= AV_LOG_WARNING) { + } else if (level <= AV_LOG_WARNING) { BOOST_LOG(warning) << buffer; - } - else if (level <= AV_LOG_INFO) { + } else if (level <= AV_LOG_INFO) { BOOST_LOG(info) << buffer; - } - else if (level <= AV_LOG_VERBOSE) { + } else if (level <= AV_LOG_VERBOSE) { // AV_LOG_VERBOSE is less verbose than AV_LOG_DEBUG BOOST_LOG(debug) << buffer; - } - else { + } else { BOOST_LOG(verbose) << buffer; } }); } - void - log_flush() { + void setup_libdisplaydevice_logging(int min_log_level) { + constexpr int min_level {static_cast(display_device::Logger::LogLevel::verbose)}; + constexpr int max_level {static_cast(display_device::Logger::LogLevel::fatal)}; + const auto log_level {static_cast(std::min(std::max(min_level, min_log_level), max_level))}; + + display_device::Logger::get().setLogLevel(log_level); + display_device::Logger::get().setCustomCallback([](const display_device::Logger::LogLevel level, const std::string &message) { + switch (level) { + case display_device::Logger::LogLevel::verbose: + BOOST_LOG(verbose) << message; + break; + case display_device::Logger::LogLevel::debug: + BOOST_LOG(debug) << message; + break; + case display_device::Logger::LogLevel::info: + BOOST_LOG(info) << message; + break; + case display_device::Logger::LogLevel::warning: + BOOST_LOG(warning) << message; + break; + case display_device::Logger::LogLevel::error: + BOOST_LOG(error) << message; + break; + case display_device::Logger::LogLevel::fatal: + BOOST_LOG(fatal) << message; + break; + } + }); + } + + void log_flush() { if (sink) { sink->flush(); } } - void - print_help(const char *name) { + void print_help(const char *name) { std::cout << "Usage: "sv << name << " [options] [/path/to/configuration_file] [--cmd]"sv << std::endl << " Any configurable option can be overwritten with: \"name=value\""sv << std::endl @@ -187,13 +209,11 @@ namespace logging { << std::endl; } - std::string - bracket(const std::string &input) { + std::string bracket(const std::string &input) { return "["s + input + "]"s; } - std::wstring - bracket(const std::wstring &input) { + std::wstring bracket(const std::wstring &input) { return L"["s + input + L"]"s; } diff --git a/src/logging.h b/src/logging.h index ee580e5db87..02e01467936 100644 --- a/src/logging.h +++ b/src/logging.h @@ -41,11 +41,9 @@ namespace logging { * deinit(); * @examples_end */ - void - deinit(); + void deinit(); - void - formatter(const boost::log::record_view &view, boost::log::formatting_ostream &os); + void formatter(const boost::log::record_view &view, boost::log::formatting_ostream &os); /** * @brief Initialize the logging system. @@ -56,15 +54,19 @@ namespace logging { * log_init(2, "sunshine.log"); * @examples_end */ - [[nodiscard]] std::unique_ptr - init(int min_log_level, const std::string &log_file); + [[nodiscard]] std::unique_ptr init(int min_log_level, const std::string &log_file); /** * @brief Setup AV logging. * @param min_log_level The log level. */ - void - setup_av_logging(int min_log_level); + void setup_av_logging(int min_log_level); + + /** + * @brief Setup logging for libdisplaydevice. + * @param min_log_level The log level. + */ + void setup_libdisplaydevice_logging(int min_log_level); /** * @brief Flush the log. @@ -72,8 +74,7 @@ namespace logging { * log_flush(); * @examples_end */ - void - log_flush(); + void log_flush(); /** * @brief Print help to stdout. @@ -82,8 +83,7 @@ namespace logging { * print_help("sunshine"); * @examples_end */ - void - print_help(const char *name); + void print_help(const char *name); /** * @brief A helper class for tracking and logging numerical values across a period of time @@ -98,28 +98,24 @@ namespace logging { * // [2024:01:01:12:00:00]: Debug: Test time value (min/max/avg): 1ms/3ms/2.00ms * @examples_end */ - template + template class min_max_avg_periodic_logger { public: - min_max_avg_periodic_logger(boost::log::sources::severity_logger &severity, - std::string_view message, - std::string_view units, - std::chrono::seconds interval_in_seconds = std::chrono::seconds(20)): + min_max_avg_periodic_logger(boost::log::sources::severity_logger &severity, std::string_view message, std::string_view units, std::chrono::seconds interval_in_seconds = std::chrono::seconds(20)): severity(severity), message(message), units(units), interval(interval_in_seconds), - enabled(config::sunshine.min_log_level <= severity.default_severity()) {} + enabled(config::sunshine.min_log_level <= severity.default_severity()) { + } - void - collect_and_log(const T &value) { + void collect_and_log(const T &value) { if (enabled) { auto print_info = [&](const T &min_value, const T &max_value, double avg_value) { auto f = stat_trackers::two_digits_after_decimal(); if constexpr (std::is_floating_point_v) { BOOST_LOG(severity.get()) << message << " (min/max/avg): " << f % min_value << units << "/" << f % max_value << units << "/" << f % avg_value << units; - } - else { + } else { BOOST_LOG(severity.get()) << message << " (min/max/avg): " << min_value << units << "/" << max_value << units << "/" << f % avg_value << units; } }; @@ -127,18 +123,19 @@ namespace logging { } } - void - collect_and_log(std::function func) { - if (enabled) collect_and_log(func()); + void collect_and_log(std::function func) { + if (enabled) { + collect_and_log(func()); + } } - void - reset() { - if (enabled) tracker.reset(); + void reset() { + if (enabled) { + tracker.reset(); + } } - bool - is_enabled() const { + bool is_enabled() const { return enabled; } @@ -168,40 +165,41 @@ namespace logging { */ class time_delta_periodic_logger { public: - time_delta_periodic_logger(boost::log::sources::severity_logger &severity, - std::string_view message, - std::chrono::seconds interval_in_seconds = std::chrono::seconds(20)): - logger(severity, message, "ms", interval_in_seconds) {} - - void - first_point(const std::chrono::steady_clock::time_point &point) { - if (logger.is_enabled()) point1 = point; + time_delta_periodic_logger(boost::log::sources::severity_logger &severity, std::string_view message, std::chrono::seconds interval_in_seconds = std::chrono::seconds(20)): + logger(severity, message, "ms", interval_in_seconds) { } - void - first_point_now() { - if (logger.is_enabled()) first_point(std::chrono::steady_clock::now()); + void first_point(const std::chrono::steady_clock::time_point &point) { + if (logger.is_enabled()) { + point1 = point; + } + } + + void first_point_now() { + if (logger.is_enabled()) { + first_point(std::chrono::steady_clock::now()); + } } - void - second_point_and_log(const std::chrono::steady_clock::time_point &point) { + void second_point_and_log(const std::chrono::steady_clock::time_point &point) { if (logger.is_enabled()) { logger.collect_and_log(std::chrono::duration(point - point1).count()); } } - void - second_point_now_and_log() { - if (logger.is_enabled()) second_point_and_log(std::chrono::steady_clock::now()); + void second_point_now_and_log() { + if (logger.is_enabled()) { + second_point_and_log(std::chrono::steady_clock::now()); + } } - void - reset() { - if (logger.is_enabled()) logger.reset(); + void reset() { + if (logger.is_enabled()) { + logger.reset(); + } } - bool - is_enabled() const { + bool is_enabled() const { return logger.is_enabled(); } @@ -215,15 +213,13 @@ namespace logging { * @param input Input string. * @return Enclosed string. */ - std::string - bracket(const std::string &input); + std::string bracket(const std::string &input); /** * @brief Enclose string in square brackets. * @param input Input string. * @return Enclosed string. */ - std::wstring - bracket(const std::wstring &input); + std::wstring bracket(const std::wstring &input); } // namespace logging diff --git a/src/main.cpp b/src/main.cpp index c4ded3d5ec4..b91dedce722 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -10,6 +10,7 @@ // local includes #include "confighttp.h" +#include "display_device.h" #include "entry_handler.h" #include "globals.h" #include "httpcommon.h" @@ -29,31 +30,37 @@ extern "C" { using namespace std::literals; std::map> signal_handlers; -void -on_signal_forwarder(int sig) { + +void on_signal_forwarder(int sig) { signal_handlers.at(sig)(); } -template -void -on_signal(int sig, FN &&fn) { +template +void on_signal(int sig, FN &&fn) { signal_handlers.emplace(sig, std::forward(fn)); std::signal(sig, on_signal_forwarder); } std::map> cmd_to_func { - { "creds"sv, [](const char *name, int argc, char **argv) { return args::creds(name, argc, argv); } }, - { "help"sv, [](const char *name, int argc, char **argv) { return args::help(name); } }, - { "version"sv, [](const char *name, int argc, char **argv) { return args::version(); } }, + {"creds"sv, [](const char *name, int argc, char **argv) { + return args::creds(name, argc, argv); + }}, + {"help"sv, [](const char *name, int argc, char **argv) { + return args::help(name); + }}, + {"version"sv, [](const char *name, int argc, char **argv) { + return args::version(); + }}, #ifdef _WIN32 - { "restore-nvprefs-undo"sv, [](const char *name, int argc, char **argv) { return args::restore_nvprefs_undo(); } }, + {"restore-nvprefs-undo"sv, [](const char *name, int argc, char **argv) { + return args::restore_nvprefs_undo(); + }}, #endif }; #ifdef _WIN32 -LRESULT CALLBACK -SessionMonitorWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { +LRESULT CALLBACK SessionMonitorWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { switch (uMsg) { case WM_CLOSE: DestroyWindow(hwnd); @@ -61,19 +68,19 @@ SessionMonitorWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { case WM_DESTROY: PostQuitMessage(0); return 0; - case WM_ENDSESSION: { - // Terminate ourselves with a blocking exit call - std::cout << "Received WM_ENDSESSION"sv << std::endl; - lifetime::exit_sunshine(0, false); - return 0; - } + case WM_ENDSESSION: + { + // Terminate ourselves with a blocking exit call + std::cout << "Received WM_ENDSESSION"sv << std::endl; + lifetime::exit_sunshine(0, false); + return 0; + } default: return DefWindowProc(hwnd, uMsg, wParam, lParam); } } -WINAPI BOOL -ConsoleCtrlHandler(DWORD type) { +WINAPI BOOL ConsoleCtrlHandler(DWORD type) { if (type == CTRL_CLOSE_EVENT) { BOOST_LOG(info) << "Console closed handler called"; lifetime::exit_sunshine(0, false); @@ -82,8 +89,7 @@ ConsoleCtrlHandler(DWORD type) { } #endif -int -main(int argc, char *argv[]) { +int main(int argc, char *argv[]) { lifetime::argv = argv; task_pool_util::TaskPool::task_id_t force_shutdown = nullptr; @@ -100,6 +106,7 @@ main(int argc, char *argv[]) { mail::man = std::make_shared(); + // parse config file if (config::parse(argc, argv)) { return 0; } @@ -117,6 +124,12 @@ main(int argc, char *argv[]) { // Log publisher metadata log_publisher_data(); + // Log modified_config_settings + for (auto &[name, val] : config::modified_config_settings) { + BOOST_LOG(info) << "config: '"sv << name << "' = "sv << val; + } + config::modified_config_settings.clear(); + if (!config::sunshine.cmd.name.empty()) { auto fn = cmd_to_func.find(config::sunshine.cmd.name); if (fn == std::end(cmd_to_func)) { @@ -133,6 +146,14 @@ main(int argc, char *argv[]) { return fn->second(argv[0], config::sunshine.cmd.argc, config::sunshine.cmd.argv); } + // Adding guard here first as it also performs recovery after crash, + // otherwise people could theoretically end up without display output. + // It also should be destroyed before forced shutdown to expedite the cleanup. + auto display_device_deinit_guard = display_device::init(platf::appdata() / "display_device.state", config::video); + if (!display_device_deinit_guard) { + BOOST_LOG(error) << "Display device session failed to initialize"sv; + } + #ifdef WIN32 // Modify relevant NVIDIA control panel settings if the system has corresponding gpu if (nvprefs_instance.load()) { @@ -179,7 +200,8 @@ main(int argc, char *argv[]) { nullptr, nullptr, nullptr, - nullptr); + nullptr + ); session_monitor_hwnd_promise.set_value(wnd); @@ -207,12 +229,10 @@ main(int argc, char *argv[]) { if (session_monitor_join_thread_future.wait_for(1s) == std::future_status::ready) { session_monitor_thread.join(); return; - } - else { + } else { BOOST_LOG(warning) << "session_monitor_join_thread_future reached timeout"; } - } - else { + } else { BOOST_LOG(warning) << "session_monitor_hwnd_future reached timeout"; } @@ -230,7 +250,7 @@ main(int argc, char *argv[]) { // Create signal handler after logging has been initialized auto shutdown_event = mail::man->event(mail::shutdown); - on_signal(SIGINT, [&force_shutdown, shutdown_event]() { + on_signal(SIGINT, [&force_shutdown, &display_device_deinit_guard, shutdown_event]() { BOOST_LOG(info) << "Interrupt handler called"sv; auto task = []() { @@ -241,9 +261,10 @@ main(int argc, char *argv[]) { force_shutdown = task_pool.pushDelayed(task, 10s).task_id; shutdown_event->raise(true); + display_device_deinit_guard = nullptr; }); - on_signal(SIGTERM, [&force_shutdown, shutdown_event]() { + on_signal(SIGTERM, [&force_shutdown, &display_device_deinit_guard, shutdown_event]() { BOOST_LOG(info) << "Terminate handler called"sv; auto task = []() { @@ -254,6 +275,7 @@ main(int argc, char *argv[]) { force_shutdown = task_pool.pushDelayed(task, 10s).task_id; shutdown_event->raise(true); + display_device_deinit_guard = nullptr; }); #ifdef _WIN32 @@ -313,8 +335,8 @@ main(int argc, char *argv[]) { return lifetime::desired_exit_code; } - std::thread httpThread { nvhttp::start }; - std::thread configThread { confighttp::start }; + std::thread httpThread {nvhttp::start}; + std::thread configThread {confighttp::start}; #ifdef _WIN32 // If we're using the default port and GameStream is enabled, warn the user diff --git a/src/main.h b/src/main.h index d867a6563e0..f1a6e15c4bd 100644 --- a/src/main.h +++ b/src/main.h @@ -12,5 +12,4 @@ * main(1, const char* args[] = {"sunshine", nullptr}); * @examples_end */ -int -main(int argc, char *argv[]); +int main(int argc, char *argv[]); diff --git a/src/move_by_copy.h b/src/move_by_copy.h index 964dcfb002c..d1c0181d38f 100644 --- a/src/move_by_copy.h +++ b/src/move_by_copy.h @@ -4,6 +4,7 @@ */ #pragma once +// standard includes #include /** @@ -14,7 +15,7 @@ namespace move_by_copy_util { * When a copy is made, it moves the object * This allows you to move an object when a move can't be done. */ - template + template class MoveByCopy { public: typedef T move_type; @@ -24,7 +25,8 @@ namespace move_by_copy_util { public: explicit MoveByCopy(move_type &&to_move): - _to_move(std::move(to_move)) {} + _to_move(std::move(to_move)) { + } MoveByCopy(MoveByCopy &&other) = default; @@ -32,11 +34,9 @@ namespace move_by_copy_util { *this = other; } - MoveByCopy & - operator=(MoveByCopy &&other) = default; + MoveByCopy &operator=(MoveByCopy &&other) = default; - MoveByCopy & - operator=(const MoveByCopy &other) { + MoveByCopy &operator=(const MoveByCopy &other) { this->_to_move = std::move(const_cast(other)._to_move); return *this; @@ -47,16 +47,14 @@ namespace move_by_copy_util { } }; - template - MoveByCopy - cmove(T &movable) { + template + MoveByCopy cmove(T &movable) { return MoveByCopy(std::move(movable)); } // Do NOT use this unless you are absolutely certain the object to be moved is no longer used by the caller - template - MoveByCopy - const_cmove(const T &movable) { + template + MoveByCopy const_cmove(const T &movable) { return MoveByCopy(std::move(const_cast(movable))); } } // namespace move_by_copy_util diff --git a/src/network.cpp b/src/network.cpp index 5b34df46aef..fedee2b8333 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -2,12 +2,15 @@ * @file src/network.cpp * @brief Definitions for networking related functions. */ -#include "network.h" +// standard includes +#include +#include + +// local includes #include "config.h" #include "logging.h" +#include "network.h" #include "utility.h" -#include -#include using namespace std::literals; @@ -33,8 +36,7 @@ namespace net { ip::make_network_v6("fe80::/64"sv), }; - net_e - from_enum_string(const std::string_view &view) { + net_e from_enum_string(const std::string_view &view) { if (view == "wan") { return WAN; } @@ -45,8 +47,7 @@ namespace net { return PC; } - net_e - from_address(const std::string_view &view) { + net_e from_address(const std::string_view &view) { auto addr = normalize_address(ip::make_address(view)); if (addr.is_v6()) { @@ -61,8 +62,7 @@ namespace net { return LAN; } } - } - else { + } else { for (auto &range : pc_ips_v4) { if (range.hosts().find(addr.to_v4()) != range.hosts().end()) { return PC; @@ -79,8 +79,7 @@ namespace net { return WAN; } - std::string_view - to_enum_string(net_e net) { + std::string_view to_enum_string(net_e net) { switch (net) { case PC: return "pc"sv; @@ -94,8 +93,7 @@ namespace net { return "wan"sv; } - af_e - af_from_enum_string(const std::string_view &view) { + af_e af_from_enum_string(const std::string_view &view) { if (view == "ipv4") { return IPV4; } @@ -107,8 +105,7 @@ namespace net { return BOTH; } - std::string_view - af_to_any_address_string(af_e af) { + std::string_view af_to_any_address_string(af_e af) { switch (af) { case IPV4: return "0.0.0.0"sv; @@ -120,8 +117,7 @@ namespace net { return "::"sv; } - boost::asio::ip::address - normalize_address(boost::asio::ip::address address) { + boost::asio::ip::address normalize_address(boost::asio::ip::address address) { // Convert IPv6-mapped IPv4 addresses into regular IPv4 addresses if (address.is_v6()) { auto v6 = address.to_v6(); @@ -133,37 +129,31 @@ namespace net { return address; } - std::string - addr_to_normalized_string(boost::asio::ip::address address) { + std::string addr_to_normalized_string(boost::asio::ip::address address) { return normalize_address(address).to_string(); } - std::string - addr_to_url_escaped_string(boost::asio::ip::address address) { + std::string addr_to_url_escaped_string(boost::asio::ip::address address) { address = normalize_address(address); if (address.is_v6()) { std::stringstream ss; ss << '[' << address.to_string() << ']'; return ss.str(); - } - else { + } else { return address.to_string(); } } - int - encryption_mode_for_address(boost::asio::ip::address address) { + int encryption_mode_for_address(boost::asio::ip::address address) { auto nettype = net::from_address(address.to_string()); if (nettype == net::net_e::PC || nettype == net::net_e::LAN) { return config::stream.lan_encryption_mode; - } - else { + } else { return config::stream.wan_encryption_mode; } } - host_t - host_create(af_e af, ENetAddress &addr, std::uint16_t port) { + host_t host_create(af_e af, ENetAddress &addr, std::uint16_t port) { static std::once_flag enet_init_flag; std::call_once(enet_init_flag, []() { enet_initialize(); @@ -174,7 +164,7 @@ namespace net { enet_address_set_port(&addr, port); // Maximum of 128 clients, which should be enough for anyone - auto host = host_t { enet_host_create(af == IPV4 ? AF_INET : AF_INET6, &addr, 128, 0, 0, 0) }; + auto host = host_t {enet_host_create(af == IPV4 ? AF_INET : AF_INET6, &addr, 128, 0, 0, 0)}; // Enable opportunistic QoS tagging (automatically disables if the network appears to drop tagged packets) enet_socket_set_option(host->socket, ENET_SOCKOPT_QOS, 1); @@ -182,8 +172,7 @@ namespace net { return host; } - void - free_host(ENetHost *host) { + void free_host(ENetHost *host) { std::for_each(host->peers, host->peers + host->peerCount, [](ENetPeer &peer_ref) { ENetPeer *peer = &peer_ref; @@ -195,10 +184,9 @@ namespace net { enet_host_destroy(host); } - std::uint16_t - map_port(int port) { + std::uint16_t map_port(int port) { // calculate the port from the config port - auto mapped_port = (std::uint16_t)((int) config::sunshine.port + port); + auto mapped_port = (std::uint16_t) ((int) config::sunshine.port + port); // Ensure port is in the range of 1024-65535 if (mapped_port < 1024 || mapped_port > 65535) { @@ -213,10 +201,9 @@ namespace net { * @param hostname The hostname to use for instance name generation. * @return Hostname-based instance name or "Sunshine" if hostname is invalid. */ - std::string - mdns_instance_name(const std::string_view &hostname) { + std::string mdns_instance_name(const std::string_view &hostname) { // Start with the unmodified hostname - std::string instancename { hostname.data(), hostname.size() }; + std::string instancename {hostname.data(), hostname.size()}; // Truncate to 63 characters per RFC 6763 section 7.2. if (instancename.size() > 63) { @@ -227,8 +214,7 @@ namespace net { // Replace any spaces with dashes if (instancename[i] == ' ') { instancename[i] = '-'; - } - else if (!std::isalnum(instancename[i]) && instancename[i] != '-') { + } else if (!std::isalnum(instancename[i]) && instancename[i] != '-') { // Stop at the first invalid character instancename.resize(i); break; diff --git a/src/network.h b/src/network.h index 98de905b405..99aa0189a36 100644 --- a/src/network.h +++ b/src/network.h @@ -4,18 +4,19 @@ */ #pragma once +// standard includes #include #include +// lib includes #include - #include +// local includes #include "utility.h" namespace net { - void - free_host(ENetHost *host); + void free_host(ENetHost *host); /** * @brief Map a specified port based on the base port. @@ -26,8 +27,7 @@ namespace net { * @examples_end * @todo Ensure port is not already in use by another application. */ - std::uint16_t - map_port(int port); + std::uint16_t map_port(int port); using host_t = util::safe_ptr; using peer_t = ENetPeer *; @@ -44,32 +44,26 @@ namespace net { BOTH ///< IPv4 and IPv6 }; - net_e - from_enum_string(const std::string_view &view); - std::string_view - to_enum_string(net_e net); + net_e from_enum_string(const std::string_view &view); + std::string_view to_enum_string(net_e net); - net_e - from_address(const std::string_view &view); + net_e from_address(const std::string_view &view); - host_t - host_create(af_e af, ENetAddress &addr, std::uint16_t port); + host_t host_create(af_e af, ENetAddress &addr, std::uint16_t port); /** * @brief Get the address family enum value from a string. * @param view The config option value. * @return The address family enum value. */ - af_e - af_from_enum_string(const std::string_view &view); + af_e af_from_enum_string(const std::string_view &view); /** * @brief Get the wildcard binding address for a given address family. * @param af Address family. * @return Normalized address. */ - std::string_view - af_to_any_address_string(af_e af); + std::string_view af_to_any_address_string(af_e af); /** * @brief Convert an address to a normalized form. @@ -77,8 +71,7 @@ namespace net { * @param address The address to normalize. * @return Normalized address. */ - boost::asio::ip::address - normalize_address(boost::asio::ip::address address); + boost::asio::ip::address normalize_address(boost::asio::ip::address address); /** * @brief Get the given address in normalized string form. @@ -86,8 +79,7 @@ namespace net { * @param address The address to normalize. * @return Normalized address in string form. */ - std::string - addr_to_normalized_string(boost::asio::ip::address address); + std::string addr_to_normalized_string(boost::asio::ip::address address); /** * @brief Get the given address in a normalized form for the host portion of a URL. @@ -95,22 +87,19 @@ namespace net { * @param address The address to normalize and escape. * @return Normalized address in URL-escaped string. */ - std::string - addr_to_url_escaped_string(boost::asio::ip::address address); + std::string addr_to_url_escaped_string(boost::asio::ip::address address); /** * @brief Get the encryption mode for the given remote endpoint address. * @param address The address used to look up the desired encryption mode. * @return The WAN or LAN encryption mode, based on the provided address. */ - int - encryption_mode_for_address(boost::asio::ip::address address); + int encryption_mode_for_address(boost::asio::ip::address address); /** * @brief Returns a string for use as the instance name for mDNS. * @param hostname The hostname to use for instance name generation. * @return Hostname-based instance name or "Sunshine" if hostname is invalid. */ - std::string - mdns_instance_name(const std::string_view &hostname); + std::string mdns_instance_name(const std::string_view &hostname); } // namespace net diff --git a/src/nvenc/nvenc_base.cpp b/src/nvenc/nvenc_base.cpp index b69d6f26bd6..bcd12ca1e77 100644 --- a/src/nvenc/nvenc_base.cpp +++ b/src/nvenc/nvenc_base.cpp @@ -1,640 +1,678 @@ -/** - * @file src/nvenc/nvenc_base.cpp - * @brief Definitions for abstract platform-agnostic base of standalone NVENC encoder. - */ -#include "nvenc_base.h" - -#include "src/config.h" -#include "src/logging.h" -#include "src/utility.h" - -#define MAKE_NVENC_VER(major, minor) ((major) | ((minor) << 24)) - -// Make sure we check backwards compatibility when bumping the Video Codec SDK version -// Things to look out for: -// - NV_ENC_*_VER definitions where the value inside NVENCAPI_STRUCT_VERSION() was increased -// - Incompatible struct changes in nvEncodeAPI.h (fields removed, semantics changed, etc.) -// - Test both old and new drivers with all supported codecs -#if NVENCAPI_VERSION != MAKE_NVENC_VER(12U, 0U) - #error Check and update NVENC code for backwards compatibility! -#endif - -namespace { - - GUID - quality_preset_guid_from_number(unsigned number) { - if (number > 7) number = 7; - - switch (number) { - case 1: - default: - return NV_ENC_PRESET_P1_GUID; - - case 2: - return NV_ENC_PRESET_P2_GUID; - - case 3: - return NV_ENC_PRESET_P3_GUID; - - case 4: - return NV_ENC_PRESET_P4_GUID; - - case 5: - return NV_ENC_PRESET_P5_GUID; - - case 6: - return NV_ENC_PRESET_P6_GUID; - - case 7: - return NV_ENC_PRESET_P7_GUID; - } - }; - - bool - equal_guids(const GUID &guid1, const GUID &guid2) { - return std::memcmp(&guid1, &guid2, sizeof(GUID)) == 0; - } - - auto - quality_preset_string_from_guid(const GUID &guid) { - if (equal_guids(guid, NV_ENC_PRESET_P1_GUID)) { - return "P1"; - } - if (equal_guids(guid, NV_ENC_PRESET_P2_GUID)) { - return "P2"; - } - if (equal_guids(guid, NV_ENC_PRESET_P3_GUID)) { - return "P3"; - } - if (equal_guids(guid, NV_ENC_PRESET_P4_GUID)) { - return "P4"; - } - if (equal_guids(guid, NV_ENC_PRESET_P5_GUID)) { - return "P5"; - } - if (equal_guids(guid, NV_ENC_PRESET_P6_GUID)) { - return "P6"; - } - if (equal_guids(guid, NV_ENC_PRESET_P7_GUID)) { - return "P7"; - } - return "Unknown"; - } - -} // namespace - -namespace nvenc { - - nvenc_base::nvenc_base(NV_ENC_DEVICE_TYPE device_type): - device_type(device_type) { - } - - nvenc_base::~nvenc_base() { - // Use destroy_encoder() instead - } - - bool - nvenc_base::create_encoder(const nvenc_config &config, const video::config_t &client_config, const nvenc_colorspace_t &colorspace, NV_ENC_BUFFER_FORMAT buffer_format) { - // Pick the minimum NvEncode API version required to support the specified codec - // to maximize driver compatibility. AV1 was introduced in SDK v12.0. - minimum_api_version = (client_config.videoFormat <= 1) ? MAKE_NVENC_VER(11U, 0U) : MAKE_NVENC_VER(12U, 0U); - - if (!nvenc && !init_library()) return false; - - if (encoder) destroy_encoder(); - auto fail_guard = util::fail_guard([this] { destroy_encoder(); }); - - encoder_params.width = client_config.width; - encoder_params.height = client_config.height; - encoder_params.buffer_format = buffer_format; - encoder_params.rfi = true; - - NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS session_params = { min_struct_version(NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS_VER) }; - session_params.device = device; - session_params.deviceType = device_type; - session_params.apiVersion = minimum_api_version; - if (nvenc_failed(nvenc->nvEncOpenEncodeSessionEx(&session_params, &encoder))) { - BOOST_LOG(error) << "NvEnc: NvEncOpenEncodeSessionEx() failed: " << last_nvenc_error_string; - return false; - } - - uint32_t encode_guid_count = 0; - if (nvenc_failed(nvenc->nvEncGetEncodeGUIDCount(encoder, &encode_guid_count))) { - BOOST_LOG(error) << "NvEnc: NvEncGetEncodeGUIDCount() failed: " << last_nvenc_error_string; - return false; - }; - - std::vector encode_guids(encode_guid_count); - if (nvenc_failed(nvenc->nvEncGetEncodeGUIDs(encoder, encode_guids.data(), encode_guids.size(), &encode_guid_count))) { - BOOST_LOG(error) << "NvEnc: NvEncGetEncodeGUIDs() failed: " << last_nvenc_error_string; - return false; - } - - NV_ENC_INITIALIZE_PARAMS init_params = { min_struct_version(NV_ENC_INITIALIZE_PARAMS_VER) }; - - switch (client_config.videoFormat) { - case 0: - // H.264 - init_params.encodeGUID = NV_ENC_CODEC_H264_GUID; - break; - - case 1: - // HEVC - init_params.encodeGUID = NV_ENC_CODEC_HEVC_GUID; - break; - - case 2: - // AV1 - init_params.encodeGUID = NV_ENC_CODEC_AV1_GUID; - break; - - default: - BOOST_LOG(error) << "NvEnc: unknown video format " << client_config.videoFormat; - return false; - } - - { - auto search_predicate = [&](const GUID &guid) { - return equal_guids(init_params.encodeGUID, guid); - }; - if (std::find_if(encode_guids.begin(), encode_guids.end(), search_predicate) == encode_guids.end()) { - BOOST_LOG(error) << "NvEnc: encoding format is not supported by the gpu"; - return false; - } - } - - auto get_encoder_cap = [&](NV_ENC_CAPS cap) { - NV_ENC_CAPS_PARAM param = { min_struct_version(NV_ENC_CAPS_PARAM_VER), cap }; - int value = 0; - nvenc->nvEncGetEncodeCaps(encoder, init_params.encodeGUID, ¶m, &value); - return value; - }; - - auto buffer_is_10bit = [&]() { - return buffer_format == NV_ENC_BUFFER_FORMAT_YUV420_10BIT || buffer_format == NV_ENC_BUFFER_FORMAT_YUV444_10BIT; - }; - - auto buffer_is_yuv444 = [&]() { - return buffer_format == NV_ENC_BUFFER_FORMAT_AYUV || buffer_format == NV_ENC_BUFFER_FORMAT_YUV444_10BIT; - }; - - { - auto supported_width = get_encoder_cap(NV_ENC_CAPS_WIDTH_MAX); - auto supported_height = get_encoder_cap(NV_ENC_CAPS_HEIGHT_MAX); - if (encoder_params.width > supported_width || encoder_params.height > supported_height) { - BOOST_LOG(error) << "NvEnc: gpu max encode resolution " << supported_width << "x" << supported_height << ", requested " << encoder_params.width << "x" << encoder_params.height; - return false; - } - } - - if (buffer_is_10bit() && !get_encoder_cap(NV_ENC_CAPS_SUPPORT_10BIT_ENCODE)) { - BOOST_LOG(error) << "NvEnc: gpu doesn't support 10-bit encode"; - return false; - } - - if (buffer_is_yuv444() && !get_encoder_cap(NV_ENC_CAPS_SUPPORT_YUV444_ENCODE)) { - BOOST_LOG(error) << "NvEnc: gpu doesn't support YUV444 encode"; - return false; - } - - if (async_event_handle && !get_encoder_cap(NV_ENC_CAPS_ASYNC_ENCODE_SUPPORT)) { - BOOST_LOG(warning) << "NvEnc: gpu doesn't support async encode"; - async_event_handle = nullptr; - } - - encoder_params.rfi = get_encoder_cap(NV_ENC_CAPS_SUPPORT_REF_PIC_INVALIDATION); - - init_params.presetGUID = quality_preset_guid_from_number(config.quality_preset); - init_params.tuningInfo = NV_ENC_TUNING_INFO_ULTRA_LOW_LATENCY; - init_params.enablePTD = 1; - init_params.enableEncodeAsync = async_event_handle ? 1 : 0; - init_params.enableWeightedPrediction = config.weighted_prediction && get_encoder_cap(NV_ENC_CAPS_SUPPORT_WEIGHTED_PREDICTION); - - init_params.encodeWidth = encoder_params.width; - init_params.darWidth = encoder_params.width; - init_params.encodeHeight = encoder_params.height; - init_params.darHeight = encoder_params.height; - init_params.frameRateNum = client_config.framerate; - init_params.frameRateDen = 1; - - NV_ENC_PRESET_CONFIG preset_config = { min_struct_version(NV_ENC_PRESET_CONFIG_VER), { min_struct_version(NV_ENC_CONFIG_VER, 7, 8) } }; - if (nvenc_failed(nvenc->nvEncGetEncodePresetConfigEx(encoder, init_params.encodeGUID, init_params.presetGUID, init_params.tuningInfo, &preset_config))) { - BOOST_LOG(error) << "NvEnc: NvEncGetEncodePresetConfigEx() failed: " << last_nvenc_error_string; - return false; - } - - NV_ENC_CONFIG enc_config = preset_config.presetCfg; - enc_config.profileGUID = NV_ENC_CODEC_PROFILE_AUTOSELECT_GUID; - enc_config.gopLength = NVENC_INFINITE_GOPLENGTH; - enc_config.frameIntervalP = 1; - enc_config.rcParams.rateControlMode = NV_ENC_PARAMS_RC_CBR; - enc_config.rcParams.zeroReorderDelay = 1; - enc_config.rcParams.enableLookahead = 0; - enc_config.rcParams.lowDelayKeyFrameScale = 1; - enc_config.rcParams.multiPass = config.two_pass == nvenc_two_pass::quarter_resolution ? NV_ENC_TWO_PASS_QUARTER_RESOLUTION : - config.two_pass == nvenc_two_pass::full_resolution ? NV_ENC_TWO_PASS_FULL_RESOLUTION : - NV_ENC_MULTI_PASS_DISABLED; - - enc_config.rcParams.enableAQ = config.adaptive_quantization; - enc_config.rcParams.averageBitRate = client_config.bitrate * 1000; - - if (get_encoder_cap(NV_ENC_CAPS_SUPPORT_CUSTOM_VBV_BUF_SIZE)) { - enc_config.rcParams.vbvBufferSize = client_config.bitrate * 1000 / client_config.framerate; - if (config.vbv_percentage_increase > 0) { - enc_config.rcParams.vbvBufferSize += enc_config.rcParams.vbvBufferSize * config.vbv_percentage_increase / 100; - } - } - - auto set_h264_hevc_common_format_config = [&](auto &format_config) { - format_config.repeatSPSPPS = 1; - format_config.idrPeriod = NVENC_INFINITE_GOPLENGTH; - format_config.sliceMode = 3; - format_config.sliceModeData = client_config.slicesPerFrame; - if (buffer_is_yuv444()) { - format_config.chromaFormatIDC = 3; - } - format_config.enableFillerDataInsertion = config.insert_filler_data; - }; - - auto set_ref_frames = [&](uint32_t &ref_frames_option, NV_ENC_NUM_REF_FRAMES &L0_option, uint32_t ref_frames_default) { - if (client_config.numRefFrames > 0) { - ref_frames_option = client_config.numRefFrames; - } - else { - ref_frames_option = ref_frames_default; - } - if (ref_frames_option > 0 && !get_encoder_cap(NV_ENC_CAPS_SUPPORT_MULTIPLE_REF_FRAMES)) { - ref_frames_option = 1; - encoder_params.rfi = false; - } - encoder_params.ref_frames_in_dpb = ref_frames_option; - // This limits ref frames any frame can use to 1, but allows larger buffer size for fallback if some frames are invalidated through rfi - L0_option = NV_ENC_NUM_REF_FRAMES_1; - }; - - auto set_minqp_if_enabled = [&](int value) { - if (config.enable_min_qp) { - enc_config.rcParams.enableMinQP = 1; - enc_config.rcParams.minQP.qpInterP = value; - enc_config.rcParams.minQP.qpIntra = value; - } - }; - - auto fill_h264_hevc_vui = [&](auto &vui_config) { - vui_config.videoSignalTypePresentFlag = 1; - vui_config.videoFormat = NV_ENC_VUI_VIDEO_FORMAT_UNSPECIFIED; - vui_config.videoFullRangeFlag = colorspace.full_range; - vui_config.colourDescriptionPresentFlag = 1; - vui_config.colourPrimaries = colorspace.primaries; - vui_config.transferCharacteristics = colorspace.tranfer_function; - vui_config.colourMatrix = colorspace.matrix; - vui_config.chromaSampleLocationFlag = buffer_is_yuv444() ? 0 : 1; - vui_config.chromaSampleLocationTop = 0; - vui_config.chromaSampleLocationBot = 0; - }; - - switch (client_config.videoFormat) { - case 0: { - // H.264 - enc_config.profileGUID = buffer_is_yuv444() ? NV_ENC_H264_PROFILE_HIGH_444_GUID : NV_ENC_H264_PROFILE_HIGH_GUID; - auto &format_config = enc_config.encodeCodecConfig.h264Config; - set_h264_hevc_common_format_config(format_config); - if (config.h264_cavlc || !get_encoder_cap(NV_ENC_CAPS_SUPPORT_CABAC)) { - format_config.entropyCodingMode = NV_ENC_H264_ENTROPY_CODING_MODE_CAVLC; - } - else { - format_config.entropyCodingMode = NV_ENC_H264_ENTROPY_CODING_MODE_CABAC; - } - set_ref_frames(format_config.maxNumRefFrames, format_config.numRefL0, 5); - set_minqp_if_enabled(config.min_qp_h264); - fill_h264_hevc_vui(format_config.h264VUIParameters); - break; - } - - case 1: { - // HEVC - auto &format_config = enc_config.encodeCodecConfig.hevcConfig; - set_h264_hevc_common_format_config(format_config); - if (buffer_is_10bit()) { - format_config.pixelBitDepthMinus8 = 2; - } - set_ref_frames(format_config.maxNumRefFramesInDPB, format_config.numRefL0, 5); - set_minqp_if_enabled(config.min_qp_hevc); - fill_h264_hevc_vui(format_config.hevcVUIParameters); - break; - } - - case 2: { - // AV1 - auto &format_config = enc_config.encodeCodecConfig.av1Config; - format_config.repeatSeqHdr = 1; - format_config.idrPeriod = NVENC_INFINITE_GOPLENGTH; - if (buffer_is_yuv444()) { - format_config.chromaFormatIDC = 3; - } - format_config.enableBitstreamPadding = config.insert_filler_data; - if (buffer_is_10bit()) { - format_config.inputPixelBitDepthMinus8 = 2; - format_config.pixelBitDepthMinus8 = 2; - } - format_config.colorPrimaries = colorspace.primaries; - format_config.transferCharacteristics = colorspace.tranfer_function; - format_config.matrixCoefficients = colorspace.matrix; - format_config.colorRange = colorspace.full_range; - format_config.chromaSamplePosition = buffer_is_yuv444() ? 0 : 1; - set_ref_frames(format_config.maxNumRefFramesInDPB, format_config.numFwdRefs, 8); - set_minqp_if_enabled(config.min_qp_av1); - - if (client_config.slicesPerFrame > 1) { - // NVENC only supports slice counts that are powers of two, so we'll pick powers of two - // with bias to rows due to hopefully more similar macroblocks with a row vs a column. - format_config.numTileRows = std::pow(2, std::ceil(std::log2(client_config.slicesPerFrame) / 2)); - format_config.numTileColumns = std::pow(2, std::floor(std::log2(client_config.slicesPerFrame) / 2)); - } - break; - } - } - - init_params.encodeConfig = &enc_config; - - if (nvenc_failed(nvenc->nvEncInitializeEncoder(encoder, &init_params))) { - BOOST_LOG(error) << "NvEnc: NvEncInitializeEncoder() failed: " << last_nvenc_error_string; - return false; - } - - if (async_event_handle) { - NV_ENC_EVENT_PARAMS event_params = { min_struct_version(NV_ENC_EVENT_PARAMS_VER) }; - event_params.completionEvent = async_event_handle; - if (nvenc_failed(nvenc->nvEncRegisterAsyncEvent(encoder, &event_params))) { - BOOST_LOG(error) << "NvEnc: NvEncRegisterAsyncEvent() failed: " << last_nvenc_error_string; - return false; - } - } - - NV_ENC_CREATE_BITSTREAM_BUFFER create_bitstream_buffer = { min_struct_version(NV_ENC_CREATE_BITSTREAM_BUFFER_VER) }; - if (nvenc_failed(nvenc->nvEncCreateBitstreamBuffer(encoder, &create_bitstream_buffer))) { - BOOST_LOG(error) << "NvEnc: NvEncCreateBitstreamBuffer() failed: " << last_nvenc_error_string; - return false; - } - output_bitstream = create_bitstream_buffer.bitstreamBuffer; - - if (!create_and_register_input_buffer()) { - return false; - } - - { - auto f = stat_trackers::two_digits_after_decimal(); - BOOST_LOG(debug) << "NvEnc: requested encoded frame size " << f % (client_config.bitrate / 8. / client_config.framerate) << " kB"; - } - - { - auto video_format_string = client_config.videoFormat == 0 ? "H.264 " : - client_config.videoFormat == 1 ? "HEVC " : - client_config.videoFormat == 2 ? "AV1 " : - " "; - std::string extra; - if (init_params.enableEncodeAsync) extra += " async"; - if (buffer_is_yuv444()) extra += " yuv444"; - if (buffer_is_10bit()) extra += " 10-bit"; - if (enc_config.rcParams.multiPass != NV_ENC_MULTI_PASS_DISABLED) 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); - if (encoder_params.rfi) extra += " rfi"; - if (init_params.enableWeightedPrediction) extra += " weighted-prediction"; - if (enc_config.rcParams.enableAQ) extra += " spatial-aq"; - if (enc_config.rcParams.enableMinQP) extra += " qpmin=" + std::to_string(enc_config.rcParams.minQP.qpInterP); - if (config.insert_filler_data) extra += " filler-data"; - - BOOST_LOG(info) << "NvEnc: created encoder " << video_format_string << quality_preset_string_from_guid(init_params.presetGUID) << extra; - } - - encoder_state = {}; - fail_guard.disable(); - return true; - } - - void - nvenc_base::destroy_encoder() { - if (output_bitstream) { - if (nvenc_failed(nvenc->nvEncDestroyBitstreamBuffer(encoder, output_bitstream))) { - BOOST_LOG(error) << "NvEnc: NvEncDestroyBitstreamBuffer() failed: " << last_nvenc_error_string; - } - output_bitstream = nullptr; - } - if (encoder && async_event_handle) { - NV_ENC_EVENT_PARAMS event_params = { min_struct_version(NV_ENC_EVENT_PARAMS_VER) }; - event_params.completionEvent = async_event_handle; - if (nvenc_failed(nvenc->nvEncUnregisterAsyncEvent(encoder, &event_params))) { - BOOST_LOG(error) << "NvEnc: NvEncUnregisterAsyncEvent() failed: " << last_nvenc_error_string; - } - } - if (registered_input_buffer) { - if (nvenc_failed(nvenc->nvEncUnregisterResource(encoder, registered_input_buffer))) { - BOOST_LOG(error) << "NvEnc: NvEncUnregisterResource() failed: " << last_nvenc_error_string; - } - registered_input_buffer = nullptr; - } - if (encoder) { - if (nvenc_failed(nvenc->nvEncDestroyEncoder(encoder))) { - BOOST_LOG(error) << "NvEnc: NvEncDestroyEncoder() failed: " << last_nvenc_error_string; - } - encoder = nullptr; - } - - encoder_state = {}; - encoder_params = {}; - } - - nvenc_encoded_frame - nvenc_base::encode_frame(uint64_t frame_index, bool force_idr) { - if (!encoder) { - return {}; - } - - assert(registered_input_buffer); - assert(output_bitstream); - - if (!synchronize_input_buffer()) { - BOOST_LOG(error) << "NvEnc: failed to synchronize input buffer"; - return {}; - } - - NV_ENC_MAP_INPUT_RESOURCE mapped_input_buffer = { min_struct_version(NV_ENC_MAP_INPUT_RESOURCE_VER) }; - mapped_input_buffer.registeredResource = registered_input_buffer; - - if (nvenc_failed(nvenc->nvEncMapInputResource(encoder, &mapped_input_buffer))) { - BOOST_LOG(error) << "NvEnc: NvEncMapInputResource() failed: " << last_nvenc_error_string; - return {}; - } - auto unmap_guard = util::fail_guard([&] { - if (nvenc_failed(nvenc->nvEncUnmapInputResource(encoder, mapped_input_buffer.mappedResource))) { - BOOST_LOG(error) << "NvEnc: NvEncUnmapInputResource() failed: " << last_nvenc_error_string; - } - }); - - NV_ENC_PIC_PARAMS pic_params = { min_struct_version(NV_ENC_PIC_PARAMS_VER, 4, 6) }; - pic_params.inputWidth = encoder_params.width; - pic_params.inputHeight = encoder_params.height; - pic_params.encodePicFlags = force_idr ? NV_ENC_PIC_FLAG_FORCEIDR : 0; - pic_params.inputTimeStamp = frame_index; - pic_params.pictureStruct = NV_ENC_PIC_STRUCT_FRAME; - pic_params.inputBuffer = mapped_input_buffer.mappedResource; - pic_params.bufferFmt = mapped_input_buffer.mappedBufferFmt; - pic_params.outputBitstream = output_bitstream; - pic_params.completionEvent = async_event_handle; - - if (nvenc_failed(nvenc->nvEncEncodePicture(encoder, &pic_params))) { - BOOST_LOG(error) << "NvEnc: NvEncEncodePicture() failed: " << last_nvenc_error_string; - return {}; - } - - NV_ENC_LOCK_BITSTREAM lock_bitstream = { min_struct_version(NV_ENC_LOCK_BITSTREAM_VER, 1, 2) }; - lock_bitstream.outputBitstream = output_bitstream; - lock_bitstream.doNotWait = 0; - - if (async_event_handle && !wait_for_async_event(100)) { - BOOST_LOG(error) << "NvEnc: frame " << frame_index << " encode wait timeout"; - return {}; - } - - if (nvenc_failed(nvenc->nvEncLockBitstream(encoder, &lock_bitstream))) { - BOOST_LOG(error) << "NvEnc: NvEncLockBitstream() failed: " << last_nvenc_error_string; - return {}; - } - - auto data_pointer = (uint8_t *) lock_bitstream.bitstreamBufferPtr; - nvenc_encoded_frame encoded_frame { - { data_pointer, data_pointer + lock_bitstream.bitstreamSizeInBytes }, - lock_bitstream.outputTimeStamp, - lock_bitstream.pictureType == NV_ENC_PIC_TYPE_IDR, - encoder_state.rfi_needs_confirmation, - }; - - if (encoder_state.rfi_needs_confirmation) { - // Invalidation request has been fulfilled, and video network packet will be marked as such - encoder_state.rfi_needs_confirmation = false; - } - - encoder_state.last_encoded_frame_index = frame_index; - - if (encoded_frame.idr) { - BOOST_LOG(debug) << "NvEnc: idr frame " << encoded_frame.frame_index; - } - - if (nvenc_failed(nvenc->nvEncUnlockBitstream(encoder, lock_bitstream.outputBitstream))) { - BOOST_LOG(error) << "NvEnc: NvEncUnlockBitstream() failed: " << last_nvenc_error_string; - } - - encoder_state.frame_size_logger.collect_and_log(encoded_frame.data.size() / 1000.); - - return encoded_frame; - } - - bool - nvenc_base::invalidate_ref_frames(uint64_t first_frame, uint64_t last_frame) { - if (!encoder || !encoder_params.rfi) return false; - - if (first_frame >= encoder_state.last_rfi_range.first && - last_frame <= encoder_state.last_rfi_range.second) { - BOOST_LOG(debug) << "NvEnc: rfi request " << first_frame << "-" << last_frame << " already done"; - return true; - } - - encoder_state.rfi_needs_confirmation = true; - - if (last_frame < first_frame) { - BOOST_LOG(error) << "NvEnc: invaid rfi request " << first_frame << "-" << last_frame << ", generating IDR"; - return false; - } - - BOOST_LOG(debug) << "NvEnc: rfi request " << first_frame << "-" << last_frame << " expanding to last encoded frame " << encoder_state.last_encoded_frame_index; - last_frame = encoder_state.last_encoded_frame_index; - - encoder_state.last_rfi_range = { first_frame, last_frame }; - - if (last_frame - first_frame + 1 >= encoder_params.ref_frames_in_dpb) { - BOOST_LOG(debug) << "NvEnc: rfi request too large, generating IDR"; - return false; - } - - for (auto i = first_frame; i <= last_frame; i++) { - if (nvenc_failed(nvenc->nvEncInvalidateRefFrames(encoder, i))) { - BOOST_LOG(error) << "NvEnc: NvEncInvalidateRefFrames() " << i << " failed: " << last_nvenc_error_string; - return false; - } - } - - return true; - } - - bool - nvenc_base::nvenc_failed(NVENCSTATUS status) { - auto status_string = [](NVENCSTATUS status) -> std::string { - switch (status) { -#define nvenc_status_case(x) \ - case x: \ - return #x; - nvenc_status_case(NV_ENC_SUCCESS); - nvenc_status_case(NV_ENC_ERR_NO_ENCODE_DEVICE); - nvenc_status_case(NV_ENC_ERR_UNSUPPORTED_DEVICE); - nvenc_status_case(NV_ENC_ERR_INVALID_ENCODERDEVICE); - nvenc_status_case(NV_ENC_ERR_INVALID_DEVICE); - nvenc_status_case(NV_ENC_ERR_DEVICE_NOT_EXIST); - nvenc_status_case(NV_ENC_ERR_INVALID_PTR); - nvenc_status_case(NV_ENC_ERR_INVALID_EVENT); - nvenc_status_case(NV_ENC_ERR_INVALID_PARAM); - nvenc_status_case(NV_ENC_ERR_INVALID_CALL); - nvenc_status_case(NV_ENC_ERR_OUT_OF_MEMORY); - nvenc_status_case(NV_ENC_ERR_ENCODER_NOT_INITIALIZED); - nvenc_status_case(NV_ENC_ERR_UNSUPPORTED_PARAM); - nvenc_status_case(NV_ENC_ERR_LOCK_BUSY); - nvenc_status_case(NV_ENC_ERR_NOT_ENOUGH_BUFFER); - nvenc_status_case(NV_ENC_ERR_INVALID_VERSION); - nvenc_status_case(NV_ENC_ERR_MAP_FAILED); - nvenc_status_case(NV_ENC_ERR_NEED_MORE_INPUT); - nvenc_status_case(NV_ENC_ERR_ENCODER_BUSY); - nvenc_status_case(NV_ENC_ERR_EVENT_NOT_REGISTERD); - nvenc_status_case(NV_ENC_ERR_GENERIC); - nvenc_status_case(NV_ENC_ERR_INCOMPATIBLE_CLIENT_KEY); - nvenc_status_case(NV_ENC_ERR_UNIMPLEMENTED); - nvenc_status_case(NV_ENC_ERR_RESOURCE_REGISTER_FAILED); - nvenc_status_case(NV_ENC_ERR_RESOURCE_NOT_REGISTERED); - nvenc_status_case(NV_ENC_ERR_RESOURCE_NOT_MAPPED); - // Newer versions of sdk may add more constants, look for them at the end of NVENCSTATUS enum -#undef nvenc_status_case - default: - return std::to_string(status); - } - }; - - last_nvenc_error_string.clear(); - if (status != NV_ENC_SUCCESS) { - /* This API function gives broken strings more often than not - if (nvenc && encoder) { - last_nvenc_error_string = nvenc->nvEncGetLastErrorString(encoder); - if (!last_nvenc_error_string.empty()) last_nvenc_error_string += " "; - } - */ - last_nvenc_error_string += status_string(status); - return true; - } - - return false; - } - - uint32_t - nvenc_base::min_struct_version(uint32_t version, uint32_t v11_struct_version, uint32_t v12_struct_version) { - assert(minimum_api_version); - - // Mask off and replace the original NVENCAPI_VERSION - version &= ~NVENCAPI_VERSION; - version |= minimum_api_version; - - // If there's a struct version override, apply that too - if (v11_struct_version || v12_struct_version) { - version &= ~(0xFFu << 16); - version |= (((minimum_api_version & 0xFF) >= 12) ? v12_struct_version : v11_struct_version) << 16; - } - - return version; - } -} // namespace nvenc +/** + * @file src/nvenc/nvenc_base.cpp + * @brief Definitions for abstract platform-agnostic base of standalone NVENC encoder. + */ +// this include +#include "nvenc_base.h" + +// local includes +#include "src/config.h" +#include "src/logging.h" +#include "src/utility.h" + +#define MAKE_NVENC_VER(major, minor) ((major) | ((minor) << 24)) + +// Make sure we check backwards compatibility when bumping the Video Codec SDK version +// Things to look out for: +// - NV_ENC_*_VER definitions where the value inside NVENCAPI_STRUCT_VERSION() was increased +// - Incompatible struct changes in nvEncodeAPI.h (fields removed, semantics changed, etc.) +// - Test both old and new drivers with all supported codecs +#if NVENCAPI_VERSION != MAKE_NVENC_VER(12U, 0U) + #error Check and update NVENC code for backwards compatibility! +#endif + +namespace { + + GUID quality_preset_guid_from_number(unsigned number) { + if (number > 7) { + number = 7; + } + + switch (number) { + case 1: + default: + return NV_ENC_PRESET_P1_GUID; + + case 2: + return NV_ENC_PRESET_P2_GUID; + + case 3: + return NV_ENC_PRESET_P3_GUID; + + case 4: + return NV_ENC_PRESET_P4_GUID; + + case 5: + return NV_ENC_PRESET_P5_GUID; + + case 6: + return NV_ENC_PRESET_P6_GUID; + + case 7: + return NV_ENC_PRESET_P7_GUID; + } + }; + + bool equal_guids(const GUID &guid1, const GUID &guid2) { + return std::memcmp(&guid1, &guid2, sizeof(GUID)) == 0; + } + + auto quality_preset_string_from_guid(const GUID &guid) { + if (equal_guids(guid, NV_ENC_PRESET_P1_GUID)) { + return "P1"; + } + if (equal_guids(guid, NV_ENC_PRESET_P2_GUID)) { + return "P2"; + } + if (equal_guids(guid, NV_ENC_PRESET_P3_GUID)) { + return "P3"; + } + if (equal_guids(guid, NV_ENC_PRESET_P4_GUID)) { + return "P4"; + } + if (equal_guids(guid, NV_ENC_PRESET_P5_GUID)) { + return "P5"; + } + if (equal_guids(guid, NV_ENC_PRESET_P6_GUID)) { + return "P6"; + } + if (equal_guids(guid, NV_ENC_PRESET_P7_GUID)) { + return "P7"; + } + return "Unknown"; + } + +} // namespace + +namespace nvenc { + + nvenc_base::nvenc_base(NV_ENC_DEVICE_TYPE device_type): + device_type(device_type) { + } + + nvenc_base::~nvenc_base() { + // Use destroy_encoder() instead + } + + bool nvenc_base::create_encoder(const nvenc_config &config, const video::config_t &client_config, const nvenc_colorspace_t &colorspace, NV_ENC_BUFFER_FORMAT buffer_format) { + // Pick the minimum NvEncode API version required to support the specified codec + // to maximize driver compatibility. AV1 was introduced in SDK v12.0. + minimum_api_version = (client_config.videoFormat <= 1) ? MAKE_NVENC_VER(11U, 0U) : MAKE_NVENC_VER(12U, 0U); + + if (!nvenc && !init_library()) { + return false; + } + + if (encoder) { + destroy_encoder(); + } + auto fail_guard = util::fail_guard([this] { + destroy_encoder(); + }); + + encoder_params.width = client_config.width; + encoder_params.height = client_config.height; + encoder_params.buffer_format = buffer_format; + encoder_params.rfi = true; + + NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS session_params = {min_struct_version(NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS_VER)}; + session_params.device = device; + session_params.deviceType = device_type; + session_params.apiVersion = minimum_api_version; + if (nvenc_failed(nvenc->nvEncOpenEncodeSessionEx(&session_params, &encoder))) { + BOOST_LOG(error) << "NvEnc: NvEncOpenEncodeSessionEx() failed: " << last_nvenc_error_string; + return false; + } + + uint32_t encode_guid_count = 0; + if (nvenc_failed(nvenc->nvEncGetEncodeGUIDCount(encoder, &encode_guid_count))) { + BOOST_LOG(error) << "NvEnc: NvEncGetEncodeGUIDCount() failed: " << last_nvenc_error_string; + return false; + }; + + std::vector encode_guids(encode_guid_count); + if (nvenc_failed(nvenc->nvEncGetEncodeGUIDs(encoder, encode_guids.data(), encode_guids.size(), &encode_guid_count))) { + BOOST_LOG(error) << "NvEnc: NvEncGetEncodeGUIDs() failed: " << last_nvenc_error_string; + return false; + } + + NV_ENC_INITIALIZE_PARAMS init_params = {min_struct_version(NV_ENC_INITIALIZE_PARAMS_VER)}; + + switch (client_config.videoFormat) { + case 0: + // H.264 + init_params.encodeGUID = NV_ENC_CODEC_H264_GUID; + break; + + case 1: + // HEVC + init_params.encodeGUID = NV_ENC_CODEC_HEVC_GUID; + break; + + case 2: + // AV1 + init_params.encodeGUID = NV_ENC_CODEC_AV1_GUID; + break; + + default: + BOOST_LOG(error) << "NvEnc: unknown video format " << client_config.videoFormat; + return false; + } + + { + auto search_predicate = [&](const GUID &guid) { + return equal_guids(init_params.encodeGUID, guid); + }; + if (std::find_if(encode_guids.begin(), encode_guids.end(), search_predicate) == encode_guids.end()) { + BOOST_LOG(error) << "NvEnc: encoding format is not supported by the gpu"; + return false; + } + } + + auto get_encoder_cap = [&](NV_ENC_CAPS cap) { + NV_ENC_CAPS_PARAM param = {min_struct_version(NV_ENC_CAPS_PARAM_VER), cap}; + int value = 0; + nvenc->nvEncGetEncodeCaps(encoder, init_params.encodeGUID, ¶m, &value); + return value; + }; + + auto buffer_is_10bit = [&]() { + return buffer_format == NV_ENC_BUFFER_FORMAT_YUV420_10BIT || buffer_format == NV_ENC_BUFFER_FORMAT_YUV444_10BIT; + }; + + auto buffer_is_yuv444 = [&]() { + return buffer_format == NV_ENC_BUFFER_FORMAT_AYUV || buffer_format == NV_ENC_BUFFER_FORMAT_YUV444_10BIT; + }; + + { + auto supported_width = get_encoder_cap(NV_ENC_CAPS_WIDTH_MAX); + auto supported_height = get_encoder_cap(NV_ENC_CAPS_HEIGHT_MAX); + if (encoder_params.width > supported_width || encoder_params.height > supported_height) { + BOOST_LOG(error) << "NvEnc: gpu max encode resolution " << supported_width << "x" << supported_height << ", requested " << encoder_params.width << "x" << encoder_params.height; + return false; + } + } + + if (buffer_is_10bit() && !get_encoder_cap(NV_ENC_CAPS_SUPPORT_10BIT_ENCODE)) { + BOOST_LOG(error) << "NvEnc: gpu doesn't support 10-bit encode"; + return false; + } + + if (buffer_is_yuv444() && !get_encoder_cap(NV_ENC_CAPS_SUPPORT_YUV444_ENCODE)) { + BOOST_LOG(error) << "NvEnc: gpu doesn't support YUV444 encode"; + return false; + } + + if (async_event_handle && !get_encoder_cap(NV_ENC_CAPS_ASYNC_ENCODE_SUPPORT)) { + BOOST_LOG(warning) << "NvEnc: gpu doesn't support async encode"; + async_event_handle = nullptr; + } + + encoder_params.rfi = get_encoder_cap(NV_ENC_CAPS_SUPPORT_REF_PIC_INVALIDATION); + + init_params.presetGUID = quality_preset_guid_from_number(config.quality_preset); + init_params.tuningInfo = NV_ENC_TUNING_INFO_ULTRA_LOW_LATENCY; + init_params.enablePTD = 1; + init_params.enableEncodeAsync = async_event_handle ? 1 : 0; + init_params.enableWeightedPrediction = config.weighted_prediction && get_encoder_cap(NV_ENC_CAPS_SUPPORT_WEIGHTED_PREDICTION); + + init_params.encodeWidth = encoder_params.width; + init_params.darWidth = encoder_params.width; + init_params.encodeHeight = encoder_params.height; + init_params.darHeight = encoder_params.height; + init_params.frameRateNum = client_config.framerate; + init_params.frameRateDen = 1; + + NV_ENC_PRESET_CONFIG preset_config = {min_struct_version(NV_ENC_PRESET_CONFIG_VER), {min_struct_version(NV_ENC_CONFIG_VER, 7, 8)}}; + if (nvenc_failed(nvenc->nvEncGetEncodePresetConfigEx(encoder, init_params.encodeGUID, init_params.presetGUID, init_params.tuningInfo, &preset_config))) { + BOOST_LOG(error) << "NvEnc: NvEncGetEncodePresetConfigEx() failed: " << last_nvenc_error_string; + return false; + } + + NV_ENC_CONFIG enc_config = preset_config.presetCfg; + enc_config.profileGUID = NV_ENC_CODEC_PROFILE_AUTOSELECT_GUID; + enc_config.gopLength = NVENC_INFINITE_GOPLENGTH; + enc_config.frameIntervalP = 1; + enc_config.rcParams.rateControlMode = NV_ENC_PARAMS_RC_CBR; + enc_config.rcParams.zeroReorderDelay = 1; + enc_config.rcParams.enableLookahead = 0; + enc_config.rcParams.lowDelayKeyFrameScale = 1; + enc_config.rcParams.multiPass = config.two_pass == nvenc_two_pass::quarter_resolution ? NV_ENC_TWO_PASS_QUARTER_RESOLUTION : + config.two_pass == nvenc_two_pass::full_resolution ? NV_ENC_TWO_PASS_FULL_RESOLUTION : + NV_ENC_MULTI_PASS_DISABLED; + + enc_config.rcParams.enableAQ = config.adaptive_quantization; + enc_config.rcParams.averageBitRate = client_config.bitrate * 1000; + + if (get_encoder_cap(NV_ENC_CAPS_SUPPORT_CUSTOM_VBV_BUF_SIZE)) { + enc_config.rcParams.vbvBufferSize = client_config.bitrate * 1000 / client_config.framerate; + if (config.vbv_percentage_increase > 0) { + enc_config.rcParams.vbvBufferSize += enc_config.rcParams.vbvBufferSize * config.vbv_percentage_increase / 100; + } + } + + auto set_h264_hevc_common_format_config = [&](auto &format_config) { + format_config.repeatSPSPPS = 1; + format_config.idrPeriod = NVENC_INFINITE_GOPLENGTH; + format_config.sliceMode = 3; + format_config.sliceModeData = client_config.slicesPerFrame; + if (buffer_is_yuv444()) { + format_config.chromaFormatIDC = 3; + } + format_config.enableFillerDataInsertion = config.insert_filler_data; + }; + + auto set_ref_frames = [&](uint32_t &ref_frames_option, NV_ENC_NUM_REF_FRAMES &L0_option, uint32_t ref_frames_default) { + if (client_config.numRefFrames > 0) { + ref_frames_option = client_config.numRefFrames; + } else { + ref_frames_option = ref_frames_default; + } + if (ref_frames_option > 0 && !get_encoder_cap(NV_ENC_CAPS_SUPPORT_MULTIPLE_REF_FRAMES)) { + ref_frames_option = 1; + encoder_params.rfi = false; + } + encoder_params.ref_frames_in_dpb = ref_frames_option; + // This limits ref frames any frame can use to 1, but allows larger buffer size for fallback if some frames are invalidated through rfi + L0_option = NV_ENC_NUM_REF_FRAMES_1; + }; + + auto set_minqp_if_enabled = [&](int value) { + if (config.enable_min_qp) { + enc_config.rcParams.enableMinQP = 1; + enc_config.rcParams.minQP.qpInterP = value; + enc_config.rcParams.minQP.qpIntra = value; + } + }; + + auto fill_h264_hevc_vui = [&](auto &vui_config) { + vui_config.videoSignalTypePresentFlag = 1; + vui_config.videoFormat = NV_ENC_VUI_VIDEO_FORMAT_UNSPECIFIED; + vui_config.videoFullRangeFlag = colorspace.full_range; + vui_config.colourDescriptionPresentFlag = 1; + vui_config.colourPrimaries = colorspace.primaries; + vui_config.transferCharacteristics = colorspace.tranfer_function; + vui_config.colourMatrix = colorspace.matrix; + vui_config.chromaSampleLocationFlag = buffer_is_yuv444() ? 0 : 1; + vui_config.chromaSampleLocationTop = 0; + vui_config.chromaSampleLocationBot = 0; + }; + + switch (client_config.videoFormat) { + case 0: + { + // H.264 + enc_config.profileGUID = buffer_is_yuv444() ? NV_ENC_H264_PROFILE_HIGH_444_GUID : NV_ENC_H264_PROFILE_HIGH_GUID; + auto &format_config = enc_config.encodeCodecConfig.h264Config; + set_h264_hevc_common_format_config(format_config); + if (config.h264_cavlc || !get_encoder_cap(NV_ENC_CAPS_SUPPORT_CABAC)) { + format_config.entropyCodingMode = NV_ENC_H264_ENTROPY_CODING_MODE_CAVLC; + } else { + format_config.entropyCodingMode = NV_ENC_H264_ENTROPY_CODING_MODE_CABAC; + } + set_ref_frames(format_config.maxNumRefFrames, format_config.numRefL0, 5); + set_minqp_if_enabled(config.min_qp_h264); + fill_h264_hevc_vui(format_config.h264VUIParameters); + break; + } + + case 1: + { + // HEVC + auto &format_config = enc_config.encodeCodecConfig.hevcConfig; + set_h264_hevc_common_format_config(format_config); + if (buffer_is_10bit()) { + format_config.pixelBitDepthMinus8 = 2; + } + set_ref_frames(format_config.maxNumRefFramesInDPB, format_config.numRefL0, 5); + set_minqp_if_enabled(config.min_qp_hevc); + fill_h264_hevc_vui(format_config.hevcVUIParameters); + if (client_config.enableIntraRefresh == 1) { + if (get_encoder_cap(NV_ENC_CAPS_SUPPORT_INTRA_REFRESH)) { + format_config.enableIntraRefresh = 1; + format_config.intraRefreshPeriod = 300; + format_config.intraRefreshCnt = 299; + if (get_encoder_cap(NV_ENC_CAPS_SINGLE_SLICE_INTRA_REFRESH)) { + format_config.singleSliceIntraRefresh = 1; + } else { + BOOST_LOG(warning) << "NvEnc: Single Slice Intra Refresh not supported"; + } + } else { + BOOST_LOG(error) << "NvEnc: Client asked for intra-refresh but the encoder does not support intra-refresh"; + } + } + break; + } + + case 2: + { + // AV1 + auto &format_config = enc_config.encodeCodecConfig.av1Config; + format_config.repeatSeqHdr = 1; + format_config.idrPeriod = NVENC_INFINITE_GOPLENGTH; + if (buffer_is_yuv444()) { + format_config.chromaFormatIDC = 3; + } + format_config.enableBitstreamPadding = config.insert_filler_data; + if (buffer_is_10bit()) { + format_config.inputPixelBitDepthMinus8 = 2; + format_config.pixelBitDepthMinus8 = 2; + } + format_config.colorPrimaries = colorspace.primaries; + format_config.transferCharacteristics = colorspace.tranfer_function; + format_config.matrixCoefficients = colorspace.matrix; + format_config.colorRange = colorspace.full_range; + format_config.chromaSamplePosition = buffer_is_yuv444() ? 0 : 1; + set_ref_frames(format_config.maxNumRefFramesInDPB, format_config.numFwdRefs, 8); + set_minqp_if_enabled(config.min_qp_av1); + + if (client_config.slicesPerFrame > 1) { + // NVENC only supports slice counts that are powers of two, so we'll pick powers of two + // with bias to rows due to hopefully more similar macroblocks with a row vs a column. + format_config.numTileRows = std::pow(2, std::ceil(std::log2(client_config.slicesPerFrame) / 2)); + format_config.numTileColumns = std::pow(2, std::floor(std::log2(client_config.slicesPerFrame) / 2)); + } + break; + } + } + + init_params.encodeConfig = &enc_config; + + if (nvenc_failed(nvenc->nvEncInitializeEncoder(encoder, &init_params))) { + BOOST_LOG(error) << "NvEnc: NvEncInitializeEncoder() failed: " << last_nvenc_error_string; + return false; + } + + if (async_event_handle) { + NV_ENC_EVENT_PARAMS event_params = {min_struct_version(NV_ENC_EVENT_PARAMS_VER)}; + event_params.completionEvent = async_event_handle; + if (nvenc_failed(nvenc->nvEncRegisterAsyncEvent(encoder, &event_params))) { + BOOST_LOG(error) << "NvEnc: NvEncRegisterAsyncEvent() failed: " << last_nvenc_error_string; + return false; + } + } + + NV_ENC_CREATE_BITSTREAM_BUFFER create_bitstream_buffer = {min_struct_version(NV_ENC_CREATE_BITSTREAM_BUFFER_VER)}; + if (nvenc_failed(nvenc->nvEncCreateBitstreamBuffer(encoder, &create_bitstream_buffer))) { + BOOST_LOG(error) << "NvEnc: NvEncCreateBitstreamBuffer() failed: " << last_nvenc_error_string; + return false; + } + output_bitstream = create_bitstream_buffer.bitstreamBuffer; + + if (!create_and_register_input_buffer()) { + return false; + } + + { + auto f = stat_trackers::two_digits_after_decimal(); + BOOST_LOG(debug) << "NvEnc: requested encoded frame size " << f % (client_config.bitrate / 8. / client_config.framerate) << " kB"; + } + + { + auto video_format_string = client_config.videoFormat == 0 ? "H.264 " : + client_config.videoFormat == 1 ? "HEVC " : + client_config.videoFormat == 2 ? "AV1 " : + " "; + std::string extra; + if (init_params.enableEncodeAsync) { + extra += " async"; + } + if (buffer_is_yuv444()) { + extra += " yuv444"; + } + if (buffer_is_10bit()) { + extra += " 10-bit"; + } + if (enc_config.rcParams.multiPass != NV_ENC_MULTI_PASS_DISABLED) { + 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); + } + if (encoder_params.rfi) { + extra += " rfi"; + } + if (init_params.enableWeightedPrediction) { + extra += " weighted-prediction"; + } + if (enc_config.rcParams.enableAQ) { + extra += " spatial-aq"; + } + if (enc_config.rcParams.enableMinQP) { + extra += " qpmin=" + std::to_string(enc_config.rcParams.minQP.qpInterP); + } + if (config.insert_filler_data) { + extra += " filler-data"; + } + + BOOST_LOG(info) << "NvEnc: created encoder " << video_format_string << quality_preset_string_from_guid(init_params.presetGUID) << extra; + } + + encoder_state = {}; + fail_guard.disable(); + return true; + } + + void nvenc_base::destroy_encoder() { + if (output_bitstream) { + if (nvenc_failed(nvenc->nvEncDestroyBitstreamBuffer(encoder, output_bitstream))) { + BOOST_LOG(error) << "NvEnc: NvEncDestroyBitstreamBuffer() failed: " << last_nvenc_error_string; + } + output_bitstream = nullptr; + } + if (encoder && async_event_handle) { + NV_ENC_EVENT_PARAMS event_params = {min_struct_version(NV_ENC_EVENT_PARAMS_VER)}; + event_params.completionEvent = async_event_handle; + if (nvenc_failed(nvenc->nvEncUnregisterAsyncEvent(encoder, &event_params))) { + BOOST_LOG(error) << "NvEnc: NvEncUnregisterAsyncEvent() failed: " << last_nvenc_error_string; + } + } + if (registered_input_buffer) { + if (nvenc_failed(nvenc->nvEncUnregisterResource(encoder, registered_input_buffer))) { + BOOST_LOG(error) << "NvEnc: NvEncUnregisterResource() failed: " << last_nvenc_error_string; + } + registered_input_buffer = nullptr; + } + if (encoder) { + if (nvenc_failed(nvenc->nvEncDestroyEncoder(encoder))) { + BOOST_LOG(error) << "NvEnc: NvEncDestroyEncoder() failed: " << last_nvenc_error_string; + } + encoder = nullptr; + } + + encoder_state = {}; + encoder_params = {}; + } + + nvenc_encoded_frame nvenc_base::encode_frame(uint64_t frame_index, bool force_idr) { + if (!encoder) { + return {}; + } + + assert(registered_input_buffer); + assert(output_bitstream); + + if (!synchronize_input_buffer()) { + BOOST_LOG(error) << "NvEnc: failed to synchronize input buffer"; + return {}; + } + + NV_ENC_MAP_INPUT_RESOURCE mapped_input_buffer = {min_struct_version(NV_ENC_MAP_INPUT_RESOURCE_VER)}; + mapped_input_buffer.registeredResource = registered_input_buffer; + + if (nvenc_failed(nvenc->nvEncMapInputResource(encoder, &mapped_input_buffer))) { + BOOST_LOG(error) << "NvEnc: NvEncMapInputResource() failed: " << last_nvenc_error_string; + return {}; + } + auto unmap_guard = util::fail_guard([&] { + if (nvenc_failed(nvenc->nvEncUnmapInputResource(encoder, mapped_input_buffer.mappedResource))) { + BOOST_LOG(error) << "NvEnc: NvEncUnmapInputResource() failed: " << last_nvenc_error_string; + } + }); + + NV_ENC_PIC_PARAMS pic_params = {min_struct_version(NV_ENC_PIC_PARAMS_VER, 4, 6)}; + pic_params.inputWidth = encoder_params.width; + pic_params.inputHeight = encoder_params.height; + pic_params.encodePicFlags = force_idr ? NV_ENC_PIC_FLAG_FORCEIDR : 0; + pic_params.inputTimeStamp = frame_index; + pic_params.pictureStruct = NV_ENC_PIC_STRUCT_FRAME; + pic_params.inputBuffer = mapped_input_buffer.mappedResource; + pic_params.bufferFmt = mapped_input_buffer.mappedBufferFmt; + pic_params.outputBitstream = output_bitstream; + pic_params.completionEvent = async_event_handle; + + if (nvenc_failed(nvenc->nvEncEncodePicture(encoder, &pic_params))) { + BOOST_LOG(error) << "NvEnc: NvEncEncodePicture() failed: " << last_nvenc_error_string; + return {}; + } + + NV_ENC_LOCK_BITSTREAM lock_bitstream = {min_struct_version(NV_ENC_LOCK_BITSTREAM_VER, 1, 2)}; + lock_bitstream.outputBitstream = output_bitstream; + lock_bitstream.doNotWait = async_event_handle ? 1 : 0; + + if (async_event_handle && !wait_for_async_event(100)) { + BOOST_LOG(error) << "NvEnc: frame " << frame_index << " encode wait timeout"; + return {}; + } + + if (nvenc_failed(nvenc->nvEncLockBitstream(encoder, &lock_bitstream))) { + BOOST_LOG(error) << "NvEnc: NvEncLockBitstream() failed: " << last_nvenc_error_string; + return {}; + } + + auto data_pointer = (uint8_t *) lock_bitstream.bitstreamBufferPtr; + nvenc_encoded_frame encoded_frame { + {data_pointer, data_pointer + lock_bitstream.bitstreamSizeInBytes}, + lock_bitstream.outputTimeStamp, + lock_bitstream.pictureType == NV_ENC_PIC_TYPE_IDR, + encoder_state.rfi_needs_confirmation, + }; + + if (encoder_state.rfi_needs_confirmation) { + // Invalidation request has been fulfilled, and video network packet will be marked as such + encoder_state.rfi_needs_confirmation = false; + } + + encoder_state.last_encoded_frame_index = frame_index; + + if (encoded_frame.idr) { + BOOST_LOG(debug) << "NvEnc: idr frame " << encoded_frame.frame_index; + } + + if (nvenc_failed(nvenc->nvEncUnlockBitstream(encoder, lock_bitstream.outputBitstream))) { + BOOST_LOG(error) << "NvEnc: NvEncUnlockBitstream() failed: " << last_nvenc_error_string; + } + + encoder_state.frame_size_logger.collect_and_log(encoded_frame.data.size() / 1000.); + + return encoded_frame; + } + + bool nvenc_base::invalidate_ref_frames(uint64_t first_frame, uint64_t last_frame) { + if (!encoder || !encoder_params.rfi) { + return false; + } + + if (first_frame >= encoder_state.last_rfi_range.first && + last_frame <= encoder_state.last_rfi_range.second) { + BOOST_LOG(debug) << "NvEnc: rfi request " << first_frame << "-" << last_frame << " already done"; + return true; + } + + encoder_state.rfi_needs_confirmation = true; + + if (last_frame < first_frame) { + BOOST_LOG(error) << "NvEnc: invaid rfi request " << first_frame << "-" << last_frame << ", generating IDR"; + return false; + } + + BOOST_LOG(debug) << "NvEnc: rfi request " << first_frame << "-" << last_frame << " expanding to last encoded frame " << encoder_state.last_encoded_frame_index; + last_frame = encoder_state.last_encoded_frame_index; + + encoder_state.last_rfi_range = {first_frame, last_frame}; + + if (last_frame - first_frame + 1 >= encoder_params.ref_frames_in_dpb) { + BOOST_LOG(debug) << "NvEnc: rfi request too large, generating IDR"; + return false; + } + + for (auto i = first_frame; i <= last_frame; i++) { + if (nvenc_failed(nvenc->nvEncInvalidateRefFrames(encoder, i))) { + BOOST_LOG(error) << "NvEnc: NvEncInvalidateRefFrames() " << i << " failed: " << last_nvenc_error_string; + return false; + } + } + + return true; + } + + bool nvenc_base::nvenc_failed(NVENCSTATUS status) { + auto status_string = [](NVENCSTATUS status) -> std::string { + switch (status) { +#define nvenc_status_case(x) \ + case x: \ + return #x; + nvenc_status_case(NV_ENC_SUCCESS); + nvenc_status_case(NV_ENC_ERR_NO_ENCODE_DEVICE); + nvenc_status_case(NV_ENC_ERR_UNSUPPORTED_DEVICE); + nvenc_status_case(NV_ENC_ERR_INVALID_ENCODERDEVICE); + nvenc_status_case(NV_ENC_ERR_INVALID_DEVICE); + nvenc_status_case(NV_ENC_ERR_DEVICE_NOT_EXIST); + nvenc_status_case(NV_ENC_ERR_INVALID_PTR); + nvenc_status_case(NV_ENC_ERR_INVALID_EVENT); + nvenc_status_case(NV_ENC_ERR_INVALID_PARAM); + nvenc_status_case(NV_ENC_ERR_INVALID_CALL); + nvenc_status_case(NV_ENC_ERR_OUT_OF_MEMORY); + nvenc_status_case(NV_ENC_ERR_ENCODER_NOT_INITIALIZED); + nvenc_status_case(NV_ENC_ERR_UNSUPPORTED_PARAM); + nvenc_status_case(NV_ENC_ERR_LOCK_BUSY); + nvenc_status_case(NV_ENC_ERR_NOT_ENOUGH_BUFFER); + nvenc_status_case(NV_ENC_ERR_INVALID_VERSION); + nvenc_status_case(NV_ENC_ERR_MAP_FAILED); + nvenc_status_case(NV_ENC_ERR_NEED_MORE_INPUT); + nvenc_status_case(NV_ENC_ERR_ENCODER_BUSY); + nvenc_status_case(NV_ENC_ERR_EVENT_NOT_REGISTERD); + nvenc_status_case(NV_ENC_ERR_GENERIC); + nvenc_status_case(NV_ENC_ERR_INCOMPATIBLE_CLIENT_KEY); + nvenc_status_case(NV_ENC_ERR_UNIMPLEMENTED); + nvenc_status_case(NV_ENC_ERR_RESOURCE_REGISTER_FAILED); + nvenc_status_case(NV_ENC_ERR_RESOURCE_NOT_REGISTERED); + nvenc_status_case(NV_ENC_ERR_RESOURCE_NOT_MAPPED); + // Newer versions of sdk may add more constants, look for them at the end of NVENCSTATUS enum +#undef nvenc_status_case + default: + return std::to_string(status); + } + }; + + last_nvenc_error_string.clear(); + if (status != NV_ENC_SUCCESS) { + /* This API function gives broken strings more often than not + if (nvenc && encoder) { + last_nvenc_error_string = nvenc->nvEncGetLastErrorString(encoder); + if (!last_nvenc_error_string.empty()) last_nvenc_error_string += " "; + } + */ + last_nvenc_error_string += status_string(status); + return true; + } + + return false; + } + + uint32_t nvenc_base::min_struct_version(uint32_t version, uint32_t v11_struct_version, uint32_t v12_struct_version) { + assert(minimum_api_version); + + // Mask off and replace the original NVENCAPI_VERSION + version &= ~NVENCAPI_VERSION; + version |= minimum_api_version; + + // If there's a struct version override, apply that too + if (v11_struct_version || v12_struct_version) { + version &= ~(0xFFu << 16); + version |= (((minimum_api_version & 0xFF) >= 12) ? v12_struct_version : v11_struct_version) << 16; + } + + return version; + } +} // namespace nvenc diff --git a/src/nvenc/nvenc_base.h b/src/nvenc/nvenc_base.h index c49aa4010e7..a4615a84259 100644 --- a/src/nvenc/nvenc_base.h +++ b/src/nvenc/nvenc_base.h @@ -1,161 +1,155 @@ -/** - * @file src/nvenc/nvenc_base.h - * @brief Declarations for abstract platform-agnostic base of standalone NVENC encoder. - */ -#pragma once - -#include "nvenc_colorspace.h" -#include "nvenc_config.h" -#include "nvenc_encoded_frame.h" - -#include "src/logging.h" -#include "src/video.h" - -#include - -/** - * @brief Standalone NVENC encoder - */ -namespace nvenc { - - /** - * @brief Abstract platform-agnostic base of standalone NVENC encoder. - * Derived classes perform platform-specific operations. - */ - class nvenc_base { - public: - /** - * @param device_type Underlying device type used by derived class. - */ - explicit nvenc_base(NV_ENC_DEVICE_TYPE device_type); - virtual ~nvenc_base(); - - nvenc_base(const nvenc_base &) = delete; - nvenc_base & - operator=(const nvenc_base &) = delete; - - /** - * @brief Create the encoder. - * @param config NVENC encoder configuration. - * @param client_config Stream configuration requested by the client. - * @param colorspace YUV colorspace. - * @param buffer_format Platform-agnostic input surface format. - * @return `true` on success, `false` on error - */ - bool - create_encoder(const nvenc_config &config, const video::config_t &client_config, const nvenc_colorspace_t &colorspace, NV_ENC_BUFFER_FORMAT buffer_format); - - /** - * @brief Destroy the encoder. - * Derived classes classes call it in the destructor. - */ - void - destroy_encoder(); - - /** - * @brief Encode the next frame using platform-specific input surface. - * @param frame_index Frame index that uniquely identifies the frame. - * Afterwards serves as parameter for `invalidate_ref_frames()`. - * No restrictions on the first frame index, but later frame indexes must be subsequent. - * @param force_idr Whether to encode frame as forced IDR. - * @return Encoded frame. - */ - nvenc_encoded_frame - encode_frame(uint64_t frame_index, bool force_idr); - - /** - * @brief Perform reference frame invalidation (RFI) procedure. - * @param first_frame First frame index of the invalidation range. - * @param last_frame Last frame index of the invalidation range. - * @return `true` on success, `false` on error. - * After error next frame must be encoded with `force_idr = true`. - */ - bool - invalidate_ref_frames(uint64_t first_frame, uint64_t last_frame); - - protected: - /** - * @brief Required. Used for loading NvEnc library and setting `nvenc` variable with `NvEncodeAPICreateInstance()`. - * Called during `create_encoder()` if `nvenc` variable is not initialized. - * @return `true` on success, `false` on error - */ - virtual bool - init_library() = 0; - - /** - * @brief Required. Used for creating outside-facing input surface, - * registering this surface with `nvenc->nvEncRegisterResource()` and setting `registered_input_buffer` variable. - * Called during `create_encoder()`. - * @return `true` on success, `false` on error - */ - virtual bool - create_and_register_input_buffer() = 0; - - /** - * @brief Optional. Override if you must perform additional operations on the registered input surface in the beginning of `encode_frame()`. - * Typically used for interop copy. - * @return `true` on success, `false` on error - */ - virtual bool - synchronize_input_buffer() { return true; } - - /** - * @brief Optional. Override if you want to create encoder in async mode. - * In this case must also set `async_event_handle` variable. - * @param timeout_ms Wait timeout in milliseconds - * @return `true` on success, `false` on timeout or error - */ - virtual bool - wait_for_async_event(uint32_t timeout_ms) { return false; } - - bool - nvenc_failed(NVENCSTATUS status); - - /** - * @brief This function returns the corresponding struct version for the minimum API required by the codec. - * @details Reducing the struct versions maximizes driver compatibility by avoiding needless API breaks. - * @param version The raw structure version from `NVENCAPI_STRUCT_VERSION()`. - * @param v11_struct_version Optionally specifies the struct version to use with v11 SDK major versions. - * @param v12_struct_version Optionally specifies the struct version to use with v12 SDK major versions. - * @return A suitable struct version for the active codec. - */ - uint32_t - min_struct_version(uint32_t version, uint32_t v11_struct_version = 0, uint32_t v12_struct_version = 0); - - const NV_ENC_DEVICE_TYPE device_type; - - void *encoder = nullptr; - - struct { - uint32_t width = 0; - uint32_t height = 0; - NV_ENC_BUFFER_FORMAT buffer_format = NV_ENC_BUFFER_FORMAT_UNDEFINED; - uint32_t ref_frames_in_dpb = 0; - bool rfi = false; - } encoder_params; - - std::string last_nvenc_error_string; - - // Derived classes set these variables - void *device = nullptr; ///< Platform-specific handle of encoding device. - ///< Should be set in constructor or `init_library()`. - std::shared_ptr nvenc; ///< Function pointers list produced by `NvEncodeAPICreateInstance()`. - ///< Should be set in `init_library()`. - NV_ENC_REGISTERED_PTR registered_input_buffer = nullptr; ///< Platform-specific input surface registered with `NvEncRegisterResource()`. - ///< Should be set in `create_and_register_input_buffer()`. - void *async_event_handle = nullptr; ///< (optional) Platform-specific handle of event object event. - ///< Can be set in constructor or `init_library()`, must override `wait_for_async_event()`. - - private: - NV_ENC_OUTPUT_PTR output_bitstream = nullptr; - uint32_t minimum_api_version = 0; - - struct { - uint64_t last_encoded_frame_index = 0; - bool rfi_needs_confirmation = false; - std::pair last_rfi_range; - logging::min_max_avg_periodic_logger frame_size_logger = { debug, "NvEnc: encoded frame sizes in kB", "" }; - } encoder_state; - }; - -} // namespace nvenc +/** + * @file src/nvenc/nvenc_base.h + * @brief Declarations for abstract platform-agnostic base of standalone NVENC encoder. + */ +#pragma once + +// lib includes +#include + +// local includes +#include "nvenc_colorspace.h" +#include "nvenc_config.h" +#include "nvenc_encoded_frame.h" +#include "src/logging.h" +#include "src/video.h" + +/** + * @brief Standalone NVENC encoder + */ +namespace nvenc { + + /** + * @brief Abstract platform-agnostic base of standalone NVENC encoder. + * Derived classes perform platform-specific operations. + */ + class nvenc_base { + public: + /** + * @param device_type Underlying device type used by derived class. + */ + explicit nvenc_base(NV_ENC_DEVICE_TYPE device_type); + virtual ~nvenc_base(); + + nvenc_base(const nvenc_base &) = delete; + nvenc_base &operator=(const nvenc_base &) = delete; + + /** + * @brief Create the encoder. + * @param config NVENC encoder configuration. + * @param client_config Stream configuration requested by the client. + * @param colorspace YUV colorspace. + * @param buffer_format Platform-agnostic input surface format. + * @return `true` on success, `false` on error + */ + bool create_encoder(const nvenc_config &config, const video::config_t &client_config, const nvenc_colorspace_t &colorspace, NV_ENC_BUFFER_FORMAT buffer_format); + + /** + * @brief Destroy the encoder. + * Derived classes classes call it in the destructor. + */ + void destroy_encoder(); + + /** + * @brief Encode the next frame using platform-specific input surface. + * @param frame_index Frame index that uniquely identifies the frame. + * Afterwards serves as parameter for `invalidate_ref_frames()`. + * No restrictions on the first frame index, but later frame indexes must be subsequent. + * @param force_idr Whether to encode frame as forced IDR. + * @return Encoded frame. + */ + nvenc_encoded_frame encode_frame(uint64_t frame_index, bool force_idr); + + /** + * @brief Perform reference frame invalidation (RFI) procedure. + * @param first_frame First frame index of the invalidation range. + * @param last_frame Last frame index of the invalidation range. + * @return `true` on success, `false` on error. + * After error next frame must be encoded with `force_idr = true`. + */ + bool invalidate_ref_frames(uint64_t first_frame, uint64_t last_frame); + + protected: + /** + * @brief Required. Used for loading NvEnc library and setting `nvenc` variable with `NvEncodeAPICreateInstance()`. + * Called during `create_encoder()` if `nvenc` variable is not initialized. + * @return `true` on success, `false` on error + */ + virtual bool init_library() = 0; + + /** + * @brief Required. Used for creating outside-facing input surface, + * registering this surface with `nvenc->nvEncRegisterResource()` and setting `registered_input_buffer` variable. + * Called during `create_encoder()`. + * @return `true` on success, `false` on error + */ + virtual bool create_and_register_input_buffer() = 0; + + /** + * @brief Optional. Override if you must perform additional operations on the registered input surface in the beginning of `encode_frame()`. + * Typically used for interop copy. + * @return `true` on success, `false` on error + */ + virtual bool synchronize_input_buffer() { + return true; + } + + /** + * @brief Optional. Override if you want to create encoder in async mode. + * In this case must also set `async_event_handle` variable. + * @param timeout_ms Wait timeout in milliseconds + * @return `true` on success, `false` on timeout or error + */ + virtual bool wait_for_async_event(uint32_t timeout_ms) { + return false; + } + + bool nvenc_failed(NVENCSTATUS status); + + /** + * @brief This function returns the corresponding struct version for the minimum API required by the codec. + * @details Reducing the struct versions maximizes driver compatibility by avoiding needless API breaks. + * @param version The raw structure version from `NVENCAPI_STRUCT_VERSION()`. + * @param v11_struct_version Optionally specifies the struct version to use with v11 SDK major versions. + * @param v12_struct_version Optionally specifies the struct version to use with v12 SDK major versions. + * @return A suitable struct version for the active codec. + */ + uint32_t min_struct_version(uint32_t version, uint32_t v11_struct_version = 0, uint32_t v12_struct_version = 0); + + const NV_ENC_DEVICE_TYPE device_type; + + void *encoder = nullptr; + + struct { + uint32_t width = 0; + uint32_t height = 0; + NV_ENC_BUFFER_FORMAT buffer_format = NV_ENC_BUFFER_FORMAT_UNDEFINED; + uint32_t ref_frames_in_dpb = 0; + bool rfi = false; + } encoder_params; + + std::string last_nvenc_error_string; + + // Derived classes set these variables + void *device = nullptr; ///< Platform-specific handle of encoding device. + ///< Should be set in constructor or `init_library()`. + std::shared_ptr nvenc; ///< Function pointers list produced by `NvEncodeAPICreateInstance()`. + ///< Should be set in `init_library()`. + NV_ENC_REGISTERED_PTR registered_input_buffer = nullptr; ///< Platform-specific input surface registered with `NvEncRegisterResource()`. + ///< Should be set in `create_and_register_input_buffer()`. + void *async_event_handle = nullptr; ///< (optional) Platform-specific handle of event object event. + ///< Can be set in constructor or `init_library()`, must override `wait_for_async_event()`. + + private: + NV_ENC_OUTPUT_PTR output_bitstream = nullptr; + uint32_t minimum_api_version = 0; + + struct { + uint64_t last_encoded_frame_index = 0; + bool rfi_needs_confirmation = false; + std::pair last_rfi_range; + logging::min_max_avg_periodic_logger frame_size_logger = {debug, "NvEnc: encoded frame sizes in kB", ""}; + } encoder_state; + }; + +} // namespace nvenc diff --git a/src/nvenc/nvenc_colorspace.h b/src/nvenc/nvenc_colorspace.h index c9ed519318d..a31ca426d01 100644 --- a/src/nvenc/nvenc_colorspace.h +++ b/src/nvenc/nvenc_colorspace.h @@ -1,21 +1,22 @@ -/** - * @file src/nvenc/nvenc_colorspace.h - * @brief Declarations for NVENC YUV colorspace. - */ -#pragma once - -#include - -namespace nvenc { - - /** - * @brief YUV colorspace and color range. - */ - struct nvenc_colorspace_t { - NV_ENC_VUI_COLOR_PRIMARIES primaries; - NV_ENC_VUI_TRANSFER_CHARACTERISTIC tranfer_function; - NV_ENC_VUI_MATRIX_COEFFS matrix; - bool full_range; - }; - -} // namespace nvenc +/** + * @file src/nvenc/nvenc_colorspace.h + * @brief Declarations for NVENC YUV colorspace. + */ +#pragma once + +// lib includes +#include + +namespace nvenc { + + /** + * @brief YUV colorspace and color range. + */ + struct nvenc_colorspace_t { + NV_ENC_VUI_COLOR_PRIMARIES primaries; + NV_ENC_VUI_TRANSFER_CHARACTERISTIC tranfer_function; + NV_ENC_VUI_MATRIX_COEFFS matrix; + bool full_range; + }; + +} // namespace nvenc diff --git a/src/nvenc/nvenc_config.h b/src/nvenc/nvenc_config.h index 213a0d289fa..824397e8577 100644 --- a/src/nvenc/nvenc_config.h +++ b/src/nvenc/nvenc_config.h @@ -1,53 +1,53 @@ -/** - * @file src/nvenc/nvenc_config.h - * @brief Declarations for NVENC encoder configuration. - */ -#pragma once - -namespace nvenc { - - enum class nvenc_two_pass { - disabled, ///< Single pass, the fastest and no extra vram - quarter_resolution, ///< Larger motion vectors being caught, faster and uses less extra vram - full_resolution, ///< Better overall statistics, slower and uses more extra vram - }; - - /** - * @brief NVENC encoder configuration. - */ - struct nvenc_config { - // Quality preset from 1 to 7, higher is slower - int quality_preset = 1; - - // Use optional preliminary pass for better motion vectors, bitrate distribution and stricter VBV(HRD), uses CUDA cores - nvenc_two_pass two_pass = nvenc_two_pass::quarter_resolution; - - // Percentage increase of VBV/HRD from the default single frame, allows low-latency variable bitrate - int vbv_percentage_increase = 0; - - // Improves fades compression, uses CUDA cores - bool weighted_prediction = false; - - // Allocate more bitrate to flat regions since they're visually more perceptible, uses CUDA cores - bool adaptive_quantization = false; - - // Don't use QP below certain value, limits peak image quality to save bitrate - bool enable_min_qp = false; - - // Min QP value for H.264 when enable_min_qp is selected - unsigned min_qp_h264 = 19; - - // Min QP value for HEVC when enable_min_qp is selected - unsigned min_qp_hevc = 23; - - // Min QP value for AV1 when enable_min_qp is selected - unsigned min_qp_av1 = 23; - - // Use CAVLC entropy coding in H.264 instead of CABAC, not relevant and here for historical reasons - bool h264_cavlc = false; - - // Add filler data to encoded frames to stay at target bitrate, mainly for testing - bool insert_filler_data = false; - }; - -} // namespace nvenc +/** + * @file src/nvenc/nvenc_config.h + * @brief Declarations for NVENC encoder configuration. + */ +#pragma once + +namespace nvenc { + + enum class nvenc_two_pass { + disabled, ///< Single pass, the fastest and no extra vram + quarter_resolution, ///< Larger motion vectors being caught, faster and uses less extra vram + full_resolution, ///< Better overall statistics, slower and uses more extra vram + }; + + /** + * @brief NVENC encoder configuration. + */ + struct nvenc_config { + // Quality preset from 1 to 7, higher is slower + int quality_preset = 1; + + // Use optional preliminary pass for better motion vectors, bitrate distribution and stricter VBV(HRD), uses CUDA cores + nvenc_two_pass two_pass = nvenc_two_pass::quarter_resolution; + + // Percentage increase of VBV/HRD from the default single frame, allows low-latency variable bitrate + int vbv_percentage_increase = 0; + + // Improves fades compression, uses CUDA cores + bool weighted_prediction = false; + + // Allocate more bitrate to flat regions since they're visually more perceptible, uses CUDA cores + bool adaptive_quantization = false; + + // Don't use QP below certain value, limits peak image quality to save bitrate + bool enable_min_qp = false; + + // Min QP value for H.264 when enable_min_qp is selected + unsigned min_qp_h264 = 19; + + // Min QP value for HEVC when enable_min_qp is selected + unsigned min_qp_hevc = 23; + + // Min QP value for AV1 when enable_min_qp is selected + unsigned min_qp_av1 = 23; + + // Use CAVLC entropy coding in H.264 instead of CABAC, not relevant and here for historical reasons + bool h264_cavlc = false; + + // Add filler data to encoded frames to stay at target bitrate, mainly for testing + bool insert_filler_data = false; + }; + +} // namespace nvenc diff --git a/src/nvenc/nvenc_d3d11.cpp b/src/nvenc/nvenc_d3d11.cpp index 7dd545b4b90..74670acdb5e 100644 --- a/src/nvenc/nvenc_d3d11.cpp +++ b/src/nvenc/nvenc_d3d11.cpp @@ -1,58 +1,69 @@ -/** - * @file src/nvenc/nvenc_d3d11.cpp - * @brief Definitions for abstract Direct3D11 NVENC encoder. - */ -#include "src/logging.h" - -#ifdef _WIN32 - #include "nvenc_d3d11.h" - -namespace nvenc { - - nvenc_d3d11::~nvenc_d3d11() { - if (dll) { - FreeLibrary(dll); - dll = NULL; - } - } - - bool - nvenc_d3d11::init_library() { - if (dll) return true; - - #ifdef _WIN64 - constexpr auto dll_name = "nvEncodeAPI64.dll"; - #else - constexpr auto dll_name = "nvEncodeAPI.dll"; - #endif - - if ((dll = LoadLibraryEx(dll_name, NULL, LOAD_LIBRARY_SEARCH_SYSTEM32))) { - if (auto create_instance = (decltype(NvEncodeAPICreateInstance) *) GetProcAddress(dll, "NvEncodeAPICreateInstance")) { - auto new_nvenc = std::make_unique(); - new_nvenc->version = min_struct_version(NV_ENCODE_API_FUNCTION_LIST_VER); - if (nvenc_failed(create_instance(new_nvenc.get()))) { - BOOST_LOG(error) << "NvEnc: NvEncodeAPICreateInstance() failed: " << last_nvenc_error_string; - } - else { - nvenc = std::move(new_nvenc); - return true; - } - } - else { - BOOST_LOG(error) << "NvEnc: No NvEncodeAPICreateInstance() in " << dll_name; - } - } - else { - BOOST_LOG(debug) << "NvEnc: Couldn't load NvEnc library " << dll_name; - } - - if (dll) { - FreeLibrary(dll); - dll = NULL; - } - - return false; - } - -} // namespace nvenc -#endif +/** + * @file src/nvenc/nvenc_d3d11.cpp + * @brief Definitions for abstract Direct3D11 NVENC encoder. + */ +// local includes +#include "src/logging.h" + +#ifdef _WIN32 + #include "nvenc_d3d11.h" + +namespace nvenc { + + nvenc_d3d11::nvenc_d3d11(NV_ENC_DEVICE_TYPE device_type): + nvenc_base(device_type) { + async_event_handle = CreateEvent(NULL, FALSE, FALSE, NULL); + } + + nvenc_d3d11::~nvenc_d3d11() { + if (dll) { + FreeLibrary(dll); + dll = NULL; + } + if (async_event_handle) { + CloseHandle(async_event_handle); + } + } + + bool nvenc_d3d11::init_library() { + if (dll) { + return true; + } + + #ifdef _WIN64 + constexpr auto dll_name = "nvEncodeAPI64.dll"; + #else + constexpr auto dll_name = "nvEncodeAPI.dll"; + #endif + + if ((dll = LoadLibraryEx(dll_name, NULL, LOAD_LIBRARY_SEARCH_SYSTEM32))) { + if (auto create_instance = (decltype(NvEncodeAPICreateInstance) *) GetProcAddress(dll, "NvEncodeAPICreateInstance")) { + auto new_nvenc = std::make_unique(); + new_nvenc->version = min_struct_version(NV_ENCODE_API_FUNCTION_LIST_VER); + if (nvenc_failed(create_instance(new_nvenc.get()))) { + BOOST_LOG(error) << "NvEnc: NvEncodeAPICreateInstance() failed: " << last_nvenc_error_string; + } else { + nvenc = std::move(new_nvenc); + return true; + } + } else { + BOOST_LOG(error) << "NvEnc: No NvEncodeAPICreateInstance() in " << dll_name; + } + } else { + BOOST_LOG(debug) << "NvEnc: Couldn't load NvEnc library " << dll_name; + } + + if (dll) { + FreeLibrary(dll); + dll = NULL; + } + + return false; + } + + bool nvenc_d3d11::wait_for_async_event(uint32_t timeout_ms) { + return WaitForSingleObject(async_event_handle, timeout_ms) == WAIT_OBJECT_0; + } + +} // namespace nvenc +#endif diff --git a/src/nvenc/nvenc_d3d11.h b/src/nvenc/nvenc_d3d11.h index 2d4d4fe77dc..efacb607f65 100644 --- a/src/nvenc/nvenc_d3d11.h +++ b/src/nvenc/nvenc_d3d11.h @@ -1,47 +1,46 @@ -/** - * @file src/nvenc/nvenc_d3d11.h - * @brief Declarations for abstract Direct3D11 NVENC encoder. - */ -#pragma once -#ifdef _WIN32 - - #include - #include - - #include "nvenc_base.h" - -namespace nvenc { - - _COM_SMARTPTR_TYPEDEF(ID3D11Device, IID_ID3D11Device); - _COM_SMARTPTR_TYPEDEF(ID3D11Texture2D, IID_ID3D11Texture2D); - _COM_SMARTPTR_TYPEDEF(IDXGIDevice, IID_IDXGIDevice); - _COM_SMARTPTR_TYPEDEF(IDXGIAdapter, IID_IDXGIAdapter); - - /** - * @brief Abstract Direct3D11 NVENC encoder. - * Encapsulates common code used by native and interop implementations. - */ - class nvenc_d3d11: public nvenc_base { - public: - explicit nvenc_d3d11(NV_ENC_DEVICE_TYPE device_type): - nvenc_base(device_type) {} - - ~nvenc_d3d11(); - - /** - * @brief Get input surface texture. - * @return Input surface texture. - */ - virtual ID3D11Texture2D * - get_input_texture() = 0; - - protected: - bool - init_library() override; - - private: - HMODULE dll = NULL; - }; - -} // namespace nvenc -#endif +/** + * @file src/nvenc/nvenc_d3d11.h + * @brief Declarations for abstract Direct3D11 NVENC encoder. + */ +#pragma once +#ifdef _WIN32 + + // standard includes + #include + #include + + // local includes + #include "nvenc_base.h" + +namespace nvenc { + + _COM_SMARTPTR_TYPEDEF(ID3D11Device, IID_ID3D11Device); + _COM_SMARTPTR_TYPEDEF(ID3D11Texture2D, IID_ID3D11Texture2D); + _COM_SMARTPTR_TYPEDEF(IDXGIDevice, IID_IDXGIDevice); + _COM_SMARTPTR_TYPEDEF(IDXGIAdapter, IID_IDXGIAdapter); + + /** + * @brief Abstract Direct3D11 NVENC encoder. + * Encapsulates common code used by native and interop implementations. + */ + class nvenc_d3d11: public nvenc_base { + public: + explicit nvenc_d3d11(NV_ENC_DEVICE_TYPE device_type); + ~nvenc_d3d11(); + + /** + * @brief Get input surface texture. + * @return Input surface texture. + */ + virtual ID3D11Texture2D *get_input_texture() = 0; + + protected: + bool init_library() override; + bool wait_for_async_event(uint32_t timeout_ms) override; + + private: + HMODULE dll = NULL; + }; + +} // namespace nvenc +#endif diff --git a/src/nvenc/nvenc_d3d11_native.cpp b/src/nvenc/nvenc_d3d11_native.cpp index a563b33d86e..afc665788f3 100644 --- a/src/nvenc/nvenc_d3d11_native.cpp +++ b/src/nvenc/nvenc_d3d11_native.cpp @@ -1,71 +1,74 @@ -/** - * @file src/nvenc/nvenc_d3d11_native.cpp - * @brief Definitions for native Direct3D11 NVENC encoder. - */ -#ifdef _WIN32 - #include "nvenc_d3d11_native.h" - - #include "nvenc_utils.h" - -namespace nvenc { - - nvenc_d3d11_native::nvenc_d3d11_native(ID3D11Device *d3d_device): - nvenc_d3d11(NV_ENC_DEVICE_TYPE_DIRECTX), - d3d_device(d3d_device) { - device = d3d_device; - } - - nvenc_d3d11_native::~nvenc_d3d11_native() { - if (encoder) destroy_encoder(); - } - - ID3D11Texture2D * - nvenc_d3d11_native::get_input_texture() { - return d3d_input_texture.GetInterfacePtr(); - } - - bool - nvenc_d3d11_native::create_and_register_input_buffer() { - if (encoder_params.buffer_format == NV_ENC_BUFFER_FORMAT_YUV444_10BIT) { - BOOST_LOG(error) << "NvEnc: 10-bit 4:4:4 encoding is incompatible with D3D11 surface formats, use CUDA interop"; - return false; - } - - if (!d3d_input_texture) { - D3D11_TEXTURE2D_DESC desc = {}; - desc.Width = encoder_params.width; - desc.Height = encoder_params.height; - desc.MipLevels = 1; - desc.ArraySize = 1; - desc.Format = dxgi_format_from_nvenc_format(encoder_params.buffer_format); - desc.SampleDesc.Count = 1; - desc.Usage = D3D11_USAGE_DEFAULT; - desc.BindFlags = D3D11_BIND_RENDER_TARGET; - if (d3d_device->CreateTexture2D(&desc, nullptr, &d3d_input_texture) != S_OK) { - BOOST_LOG(error) << "NvEnc: couldn't create input texture"; - return false; - } - } - - if (!registered_input_buffer) { - NV_ENC_REGISTER_RESOURCE register_resource = { min_struct_version(NV_ENC_REGISTER_RESOURCE_VER, 3, 4) }; - register_resource.resourceType = NV_ENC_INPUT_RESOURCE_TYPE_DIRECTX; - register_resource.width = encoder_params.width; - register_resource.height = encoder_params.height; - register_resource.resourceToRegister = d3d_input_texture.GetInterfacePtr(); - register_resource.bufferFormat = encoder_params.buffer_format; - register_resource.bufferUsage = NV_ENC_INPUT_IMAGE; - - if (nvenc_failed(nvenc->nvEncRegisterResource(encoder, ®ister_resource))) { - BOOST_LOG(error) << "NvEnc: NvEncRegisterResource() failed: " << last_nvenc_error_string; - return false; - } - - registered_input_buffer = register_resource.registeredResource; - } - - return true; - } - -} // namespace nvenc -#endif +/** + * @file src/nvenc/nvenc_d3d11_native.cpp + * @brief Definitions for native Direct3D11 NVENC encoder. + */ +#ifdef _WIN32 + // this include + #include "nvenc_d3d11_native.h" + + // local includes + #include "nvenc_utils.h" + +namespace nvenc { + + nvenc_d3d11_native::nvenc_d3d11_native(ID3D11Device *d3d_device): + nvenc_d3d11(NV_ENC_DEVICE_TYPE_DIRECTX), + d3d_device(d3d_device) { + device = d3d_device; + } + + nvenc_d3d11_native::~nvenc_d3d11_native() { + if (encoder) { + destroy_encoder(); + } + } + + ID3D11Texture2D * + nvenc_d3d11_native::get_input_texture() { + return d3d_input_texture.GetInterfacePtr(); + } + + bool nvenc_d3d11_native::create_and_register_input_buffer() { + if (encoder_params.buffer_format == NV_ENC_BUFFER_FORMAT_YUV444_10BIT) { + BOOST_LOG(error) << "NvEnc: 10-bit 4:4:4 encoding is incompatible with D3D11 surface formats, use CUDA interop"; + return false; + } + + if (!d3d_input_texture) { + D3D11_TEXTURE2D_DESC desc = {}; + desc.Width = encoder_params.width; + desc.Height = encoder_params.height; + desc.MipLevels = 1; + desc.ArraySize = 1; + desc.Format = dxgi_format_from_nvenc_format(encoder_params.buffer_format); + desc.SampleDesc.Count = 1; + desc.Usage = D3D11_USAGE_DEFAULT; + desc.BindFlags = D3D11_BIND_RENDER_TARGET; + if (d3d_device->CreateTexture2D(&desc, nullptr, &d3d_input_texture) != S_OK) { + BOOST_LOG(error) << "NvEnc: couldn't create input texture"; + return false; + } + } + + if (!registered_input_buffer) { + NV_ENC_REGISTER_RESOURCE register_resource = {min_struct_version(NV_ENC_REGISTER_RESOURCE_VER, 3, 4)}; + register_resource.resourceType = NV_ENC_INPUT_RESOURCE_TYPE_DIRECTX; + register_resource.width = encoder_params.width; + register_resource.height = encoder_params.height; + register_resource.resourceToRegister = d3d_input_texture.GetInterfacePtr(); + register_resource.bufferFormat = encoder_params.buffer_format; + register_resource.bufferUsage = NV_ENC_INPUT_IMAGE; + + if (nvenc_failed(nvenc->nvEncRegisterResource(encoder, ®ister_resource))) { + BOOST_LOG(error) << "NvEnc: NvEncRegisterResource() failed: " << last_nvenc_error_string; + return false; + } + + registered_input_buffer = register_resource.registeredResource; + } + + return true; + } + +} // namespace nvenc +#endif diff --git a/src/nvenc/nvenc_d3d11_native.h b/src/nvenc/nvenc_d3d11_native.h index f9d49b18631..0e6f039e302 100644 --- a/src/nvenc/nvenc_d3d11_native.h +++ b/src/nvenc/nvenc_d3d11_native.h @@ -1,38 +1,37 @@ -/** - * @file src/nvenc/nvenc_d3d11_native.h - * @brief Declarations for native Direct3D11 NVENC encoder. - */ -#pragma once -#ifdef _WIN32 - - #include - #include - - #include "nvenc_d3d11.h" - -namespace nvenc { - - /** - * @brief Native Direct3D11 NVENC encoder. - */ - class nvenc_d3d11_native final: public nvenc_d3d11 { - public: - /** - * @param d3d_device Direct3D11 device used for encoding. - */ - explicit nvenc_d3d11_native(ID3D11Device *d3d_device); - ~nvenc_d3d11_native(); - - ID3D11Texture2D * - get_input_texture() override; - - private: - bool - create_and_register_input_buffer() override; - - const ID3D11DevicePtr d3d_device; - ID3D11Texture2DPtr d3d_input_texture; - }; - -} // namespace nvenc -#endif +/** + * @file src/nvenc/nvenc_d3d11_native.h + * @brief Declarations for native Direct3D11 NVENC encoder. + */ +#pragma once +#ifdef _WIN32 + // standard includes + #include + #include + + // local includes + #include "nvenc_d3d11.h" + +namespace nvenc { + + /** + * @brief Native Direct3D11 NVENC encoder. + */ + class nvenc_d3d11_native final: public nvenc_d3d11 { + public: + /** + * @param d3d_device Direct3D11 device used for encoding. + */ + explicit nvenc_d3d11_native(ID3D11Device *d3d_device); + ~nvenc_d3d11_native(); + + ID3D11Texture2D *get_input_texture() override; + + private: + bool create_and_register_input_buffer() override; + + const ID3D11DevicePtr d3d_device; + ID3D11Texture2DPtr d3d_input_texture; + }; + +} // namespace nvenc +#endif diff --git a/src/nvenc/nvenc_d3d11_on_cuda.cpp b/src/nvenc/nvenc_d3d11_on_cuda.cpp index 37fe896329c..02436e696b3 100644 --- a/src/nvenc/nvenc_d3d11_on_cuda.cpp +++ b/src/nvenc/nvenc_d3d11_on_cuda.cpp @@ -1,267 +1,269 @@ -/** - * @file src/nvenc/nvenc_d3d11_on_cuda.cpp - * @brief Definitions for CUDA NVENC encoder with Direct3D11 input surfaces. - */ -#ifdef _WIN32 - #include "nvenc_d3d11_on_cuda.h" - - #include "nvenc_utils.h" - -namespace nvenc { - - nvenc_d3d11_on_cuda::nvenc_d3d11_on_cuda(ID3D11Device *d3d_device): - nvenc_d3d11(NV_ENC_DEVICE_TYPE_CUDA), - d3d_device(d3d_device) { - } - - nvenc_d3d11_on_cuda::~nvenc_d3d11_on_cuda() { - if (encoder) destroy_encoder(); - - if (cuda_context) { - { - auto autopop_context = push_context(); - - if (cuda_d3d_input_texture) { - if (cuda_failed(cuda_functions.cuGraphicsUnregisterResource(cuda_d3d_input_texture))) { - BOOST_LOG(error) << "NvEnc: cuGraphicsUnregisterResource() failed: error " << last_cuda_error; - } - cuda_d3d_input_texture = nullptr; - } - - if (cuda_surface) { - if (cuda_failed(cuda_functions.cuMemFree(cuda_surface))) { - BOOST_LOG(error) << "NvEnc: cuMemFree() failed: error " << last_cuda_error; - } - cuda_surface = 0; - } - } - - if (cuda_failed(cuda_functions.cuCtxDestroy(cuda_context))) { - BOOST_LOG(error) << "NvEnc: cuCtxDestroy() failed: error " << last_cuda_error; - } - cuda_context = nullptr; - } - - if (cuda_functions.dll) { - FreeLibrary(cuda_functions.dll); - cuda_functions = {}; - } - } - - ID3D11Texture2D * - nvenc_d3d11_on_cuda::get_input_texture() { - return d3d_input_texture.GetInterfacePtr(); - } - - bool - nvenc_d3d11_on_cuda::init_library() { - if (!nvenc_d3d11::init_library()) return false; - - constexpr auto dll_name = "nvcuda.dll"; - - if ((cuda_functions.dll = LoadLibraryEx(dll_name, NULL, LOAD_LIBRARY_SEARCH_SYSTEM32))) { - auto load_function = [&](T &location, auto symbol) -> bool { - location = (T) GetProcAddress(cuda_functions.dll, symbol); - return location != nullptr; - }; - if (!load_function(cuda_functions.cuInit, "cuInit") || - !load_function(cuda_functions.cuD3D11GetDevice, "cuD3D11GetDevice") || - !load_function(cuda_functions.cuCtxCreate, "cuCtxCreate_v2") || - !load_function(cuda_functions.cuCtxDestroy, "cuCtxDestroy_v2") || - !load_function(cuda_functions.cuCtxPushCurrent, "cuCtxPushCurrent_v2") || - !load_function(cuda_functions.cuCtxPopCurrent, "cuCtxPopCurrent_v2") || - !load_function(cuda_functions.cuMemAllocPitch, "cuMemAllocPitch_v2") || - !load_function(cuda_functions.cuMemFree, "cuMemFree_v2") || - !load_function(cuda_functions.cuGraphicsD3D11RegisterResource, "cuGraphicsD3D11RegisterResource") || - !load_function(cuda_functions.cuGraphicsUnregisterResource, "cuGraphicsUnregisterResource") || - !load_function(cuda_functions.cuGraphicsMapResources, "cuGraphicsMapResources") || - !load_function(cuda_functions.cuGraphicsUnmapResources, "cuGraphicsUnmapResources") || - !load_function(cuda_functions.cuGraphicsSubResourceGetMappedArray, "cuGraphicsSubResourceGetMappedArray") || - !load_function(cuda_functions.cuMemcpy2D, "cuMemcpy2D_v2")) { - BOOST_LOG(error) << "NvEnc: missing CUDA functions in " << dll_name; - FreeLibrary(cuda_functions.dll); - cuda_functions = {}; - } - } - else { - BOOST_LOG(debug) << "NvEnc: couldn't load CUDA dynamic library " << dll_name; - } - - if (cuda_functions.dll) { - IDXGIDevicePtr dxgi_device; - IDXGIAdapterPtr dxgi_adapter; - if (d3d_device && - SUCCEEDED(d3d_device->QueryInterface(IID_PPV_ARGS(&dxgi_device))) && - SUCCEEDED(dxgi_device->GetAdapter(&dxgi_adapter))) { - CUdevice cuda_device; - if (cuda_succeeded(cuda_functions.cuInit(0)) && - cuda_succeeded(cuda_functions.cuD3D11GetDevice(&cuda_device, dxgi_adapter)) && - cuda_succeeded(cuda_functions.cuCtxCreate(&cuda_context, CU_CTX_SCHED_BLOCKING_SYNC, cuda_device)) && - cuda_succeeded(cuda_functions.cuCtxPopCurrent(&cuda_context))) { - device = cuda_context; - } - else { - BOOST_LOG(error) << "NvEnc: couldn't create CUDA interop context: error " << last_cuda_error; - } - } - else { - BOOST_LOG(error) << "NvEnc: couldn't get DXGI adapter for CUDA interop"; - } - } - - return device != nullptr; - } - - bool - nvenc_d3d11_on_cuda::create_and_register_input_buffer() { - if (encoder_params.buffer_format != NV_ENC_BUFFER_FORMAT_YUV444_10BIT) { - BOOST_LOG(error) << "NvEnc: CUDA interop is expected to be used only for 10-bit 4:4:4 encoding"; - return false; - } - - if (!d3d_input_texture) { - D3D11_TEXTURE2D_DESC desc = {}; - desc.Width = encoder_params.width; - desc.Height = encoder_params.height * 3; // Planar YUV - desc.MipLevels = 1; - desc.ArraySize = 1; - desc.Format = dxgi_format_from_nvenc_format(encoder_params.buffer_format); - desc.SampleDesc.Count = 1; - desc.Usage = D3D11_USAGE_DEFAULT; - desc.BindFlags = D3D11_BIND_RENDER_TARGET; - - if (d3d_device->CreateTexture2D(&desc, nullptr, &d3d_input_texture) != S_OK) { - BOOST_LOG(error) << "NvEnc: couldn't create input texture"; - return false; - } - } - - { - auto autopop_context = push_context(); - if (!autopop_context) return false; - - if (!cuda_d3d_input_texture) { - if (cuda_failed(cuda_functions.cuGraphicsD3D11RegisterResource( - &cuda_d3d_input_texture, - d3d_input_texture, - CU_GRAPHICS_REGISTER_FLAGS_NONE))) { - BOOST_LOG(error) << "NvEnc: cuGraphicsD3D11RegisterResource() failed: error " << last_cuda_error; - return false; - } - } - - if (!cuda_surface) { - if (cuda_failed(cuda_functions.cuMemAllocPitch( - &cuda_surface, - &cuda_surface_pitch, - // Planar 16-bit YUV - encoder_params.width * 2, - encoder_params.height * 3, 16))) { - BOOST_LOG(error) << "NvEnc: cuMemAllocPitch() failed: error " << last_cuda_error; - return false; - } - } - } - - if (!registered_input_buffer) { - NV_ENC_REGISTER_RESOURCE register_resource = { min_struct_version(NV_ENC_REGISTER_RESOURCE_VER, 3, 4) }; - register_resource.resourceType = NV_ENC_INPUT_RESOURCE_TYPE_CUDADEVICEPTR; - register_resource.width = encoder_params.width; - register_resource.height = encoder_params.height; - register_resource.pitch = cuda_surface_pitch; - register_resource.resourceToRegister = (void *) cuda_surface; - register_resource.bufferFormat = encoder_params.buffer_format; - register_resource.bufferUsage = NV_ENC_INPUT_IMAGE; - - if (nvenc_failed(nvenc->nvEncRegisterResource(encoder, ®ister_resource))) { - BOOST_LOG(error) << "NvEnc: NvEncRegisterResource() failed: " << last_nvenc_error_string; - return false; - } - - registered_input_buffer = register_resource.registeredResource; - } - - return true; - } - - bool - nvenc_d3d11_on_cuda::synchronize_input_buffer() { - auto autopop_context = push_context(); - if (!autopop_context) return false; - - if (cuda_failed(cuda_functions.cuGraphicsMapResources(1, &cuda_d3d_input_texture, 0))) { - BOOST_LOG(error) << "NvEnc: cuGraphicsMapResources() failed: error " << last_cuda_error; - return false; - } - - auto unmap = [&]() -> bool { - if (cuda_failed(cuda_functions.cuGraphicsUnmapResources(1, &cuda_d3d_input_texture, 0))) { - BOOST_LOG(error) << "NvEnc: cuGraphicsUnmapResources() failed: error " << last_cuda_error; - return false; - } - return true; - }; - auto unmap_guard = util::fail_guard(unmap); - - CUarray input_texture_array; - if (cuda_failed(cuda_functions.cuGraphicsSubResourceGetMappedArray(&input_texture_array, cuda_d3d_input_texture, 0, 0))) { - BOOST_LOG(error) << "NvEnc: cuGraphicsSubResourceGetMappedArray() failed: error " << last_cuda_error; - return false; - } - - { - CUDA_MEMCPY2D copy_params = {}; - copy_params.srcMemoryType = CU_MEMORYTYPE_ARRAY; - copy_params.srcArray = input_texture_array; - copy_params.dstMemoryType = CU_MEMORYTYPE_DEVICE; - copy_params.dstDevice = cuda_surface; - copy_params.dstPitch = cuda_surface_pitch; - // Planar 16-bit YUV - copy_params.WidthInBytes = encoder_params.width * 2; - copy_params.Height = encoder_params.height * 3; - - if (cuda_failed(cuda_functions.cuMemcpy2D(©_params))) { - BOOST_LOG(error) << "NvEnc: cuMemcpy2D() failed: error " << last_cuda_error; - return false; - } - } - - unmap_guard.disable(); - return unmap(); - } - - bool - nvenc_d3d11_on_cuda::cuda_succeeded(CUresult result) { - last_cuda_error = result; - return result == CUDA_SUCCESS; - } - - bool - nvenc_d3d11_on_cuda::cuda_failed(CUresult result) { - last_cuda_error = result; - return result != CUDA_SUCCESS; - } - - nvenc_d3d11_on_cuda::autopop_context::~autopop_context() { - if (pushed_context) { - CUcontext popped_context; - if (parent.cuda_failed(parent.cuda_functions.cuCtxPopCurrent(&popped_context))) { - BOOST_LOG(error) << "NvEnc: cuCtxPopCurrent() failed: error " << parent.last_cuda_error; - } - } - } - - nvenc_d3d11_on_cuda::autopop_context - nvenc_d3d11_on_cuda::push_context() { - if (cuda_context && - cuda_succeeded(cuda_functions.cuCtxPushCurrent(cuda_context))) { - return { *this, cuda_context }; - } - else { - BOOST_LOG(error) << "NvEnc: cuCtxPushCurrent() failed: error " << last_cuda_error; - return { *this, nullptr }; - } - } - -} // namespace nvenc -#endif +/** + * @file src/nvenc/nvenc_d3d11_on_cuda.cpp + * @brief Definitions for CUDA NVENC encoder with Direct3D11 input surfaces. + */ +#ifdef _WIN32 + // this include + #include "nvenc_d3d11_on_cuda.h" + + // local includes + #include "nvenc_utils.h" + +namespace nvenc { + + nvenc_d3d11_on_cuda::nvenc_d3d11_on_cuda(ID3D11Device *d3d_device): + nvenc_d3d11(NV_ENC_DEVICE_TYPE_CUDA), + d3d_device(d3d_device) { + } + + nvenc_d3d11_on_cuda::~nvenc_d3d11_on_cuda() { + if (encoder) { + destroy_encoder(); + } + + if (cuda_context) { + { + auto autopop_context = push_context(); + + if (cuda_d3d_input_texture) { + if (cuda_failed(cuda_functions.cuGraphicsUnregisterResource(cuda_d3d_input_texture))) { + BOOST_LOG(error) << "NvEnc: cuGraphicsUnregisterResource() failed: error " << last_cuda_error; + } + cuda_d3d_input_texture = nullptr; + } + + if (cuda_surface) { + if (cuda_failed(cuda_functions.cuMemFree(cuda_surface))) { + BOOST_LOG(error) << "NvEnc: cuMemFree() failed: error " << last_cuda_error; + } + cuda_surface = 0; + } + } + + if (cuda_failed(cuda_functions.cuCtxDestroy(cuda_context))) { + BOOST_LOG(error) << "NvEnc: cuCtxDestroy() failed: error " << last_cuda_error; + } + cuda_context = nullptr; + } + + if (cuda_functions.dll) { + FreeLibrary(cuda_functions.dll); + cuda_functions = {}; + } + } + + ID3D11Texture2D *nvenc_d3d11_on_cuda::get_input_texture() { + return d3d_input_texture.GetInterfacePtr(); + } + + bool nvenc_d3d11_on_cuda::init_library() { + if (!nvenc_d3d11::init_library()) { + return false; + } + + constexpr auto dll_name = "nvcuda.dll"; + + if ((cuda_functions.dll = LoadLibraryEx(dll_name, NULL, LOAD_LIBRARY_SEARCH_SYSTEM32))) { + auto load_function = [&](T &location, auto symbol) -> bool { + location = (T) GetProcAddress(cuda_functions.dll, symbol); + return location != nullptr; + }; + if (!load_function(cuda_functions.cuInit, "cuInit") || + !load_function(cuda_functions.cuD3D11GetDevice, "cuD3D11GetDevice") || + !load_function(cuda_functions.cuCtxCreate, "cuCtxCreate_v2") || + !load_function(cuda_functions.cuCtxDestroy, "cuCtxDestroy_v2") || + !load_function(cuda_functions.cuCtxPushCurrent, "cuCtxPushCurrent_v2") || + !load_function(cuda_functions.cuCtxPopCurrent, "cuCtxPopCurrent_v2") || + !load_function(cuda_functions.cuMemAllocPitch, "cuMemAllocPitch_v2") || + !load_function(cuda_functions.cuMemFree, "cuMemFree_v2") || + !load_function(cuda_functions.cuGraphicsD3D11RegisterResource, "cuGraphicsD3D11RegisterResource") || + !load_function(cuda_functions.cuGraphicsUnregisterResource, "cuGraphicsUnregisterResource") || + !load_function(cuda_functions.cuGraphicsMapResources, "cuGraphicsMapResources") || + !load_function(cuda_functions.cuGraphicsUnmapResources, "cuGraphicsUnmapResources") || + !load_function(cuda_functions.cuGraphicsSubResourceGetMappedArray, "cuGraphicsSubResourceGetMappedArray") || + !load_function(cuda_functions.cuMemcpy2D, "cuMemcpy2D_v2")) { + BOOST_LOG(error) << "NvEnc: missing CUDA functions in " << dll_name; + FreeLibrary(cuda_functions.dll); + cuda_functions = {}; + } + } else { + BOOST_LOG(debug) << "NvEnc: couldn't load CUDA dynamic library " << dll_name; + } + + if (cuda_functions.dll) { + IDXGIDevicePtr dxgi_device; + IDXGIAdapterPtr dxgi_adapter; + if (d3d_device && + SUCCEEDED(d3d_device->QueryInterface(IID_PPV_ARGS(&dxgi_device))) && + SUCCEEDED(dxgi_device->GetAdapter(&dxgi_adapter))) { + CUdevice cuda_device; + if (cuda_succeeded(cuda_functions.cuInit(0)) && + cuda_succeeded(cuda_functions.cuD3D11GetDevice(&cuda_device, dxgi_adapter)) && + cuda_succeeded(cuda_functions.cuCtxCreate(&cuda_context, CU_CTX_SCHED_BLOCKING_SYNC, cuda_device)) && + cuda_succeeded(cuda_functions.cuCtxPopCurrent(&cuda_context))) { + device = cuda_context; + } else { + BOOST_LOG(error) << "NvEnc: couldn't create CUDA interop context: error " << last_cuda_error; + } + } else { + BOOST_LOG(error) << "NvEnc: couldn't get DXGI adapter for CUDA interop"; + } + } + + return device != nullptr; + } + + bool nvenc_d3d11_on_cuda::create_and_register_input_buffer() { + if (encoder_params.buffer_format != NV_ENC_BUFFER_FORMAT_YUV444_10BIT) { + BOOST_LOG(error) << "NvEnc: CUDA interop is expected to be used only for 10-bit 4:4:4 encoding"; + return false; + } + + if (!d3d_input_texture) { + D3D11_TEXTURE2D_DESC desc = {}; + desc.Width = encoder_params.width; + desc.Height = encoder_params.height * 3; // Planar YUV + desc.MipLevels = 1; + desc.ArraySize = 1; + desc.Format = dxgi_format_from_nvenc_format(encoder_params.buffer_format); + desc.SampleDesc.Count = 1; + desc.Usage = D3D11_USAGE_DEFAULT; + desc.BindFlags = D3D11_BIND_RENDER_TARGET; + + if (d3d_device->CreateTexture2D(&desc, nullptr, &d3d_input_texture) != S_OK) { + BOOST_LOG(error) << "NvEnc: couldn't create input texture"; + return false; + } + } + + { + auto autopop_context = push_context(); + if (!autopop_context) { + return false; + } + + if (!cuda_d3d_input_texture) { + if (cuda_failed(cuda_functions.cuGraphicsD3D11RegisterResource( + &cuda_d3d_input_texture, + d3d_input_texture, + CU_GRAPHICS_REGISTER_FLAGS_NONE + ))) { + BOOST_LOG(error) << "NvEnc: cuGraphicsD3D11RegisterResource() failed: error " << last_cuda_error; + return false; + } + } + + if (!cuda_surface) { + if (cuda_failed(cuda_functions.cuMemAllocPitch( + &cuda_surface, + &cuda_surface_pitch, + // Planar 16-bit YUV + encoder_params.width * 2, + encoder_params.height * 3, + 16 + ))) { + BOOST_LOG(error) << "NvEnc: cuMemAllocPitch() failed: error " << last_cuda_error; + return false; + } + } + } + + if (!registered_input_buffer) { + NV_ENC_REGISTER_RESOURCE register_resource = {min_struct_version(NV_ENC_REGISTER_RESOURCE_VER, 3, 4)}; + register_resource.resourceType = NV_ENC_INPUT_RESOURCE_TYPE_CUDADEVICEPTR; + register_resource.width = encoder_params.width; + register_resource.height = encoder_params.height; + register_resource.pitch = cuda_surface_pitch; + register_resource.resourceToRegister = (void *) cuda_surface; + register_resource.bufferFormat = encoder_params.buffer_format; + register_resource.bufferUsage = NV_ENC_INPUT_IMAGE; + + if (nvenc_failed(nvenc->nvEncRegisterResource(encoder, ®ister_resource))) { + BOOST_LOG(error) << "NvEnc: NvEncRegisterResource() failed: " << last_nvenc_error_string; + return false; + } + + registered_input_buffer = register_resource.registeredResource; + } + + return true; + } + + bool nvenc_d3d11_on_cuda::synchronize_input_buffer() { + auto autopop_context = push_context(); + if (!autopop_context) { + return false; + } + + if (cuda_failed(cuda_functions.cuGraphicsMapResources(1, &cuda_d3d_input_texture, 0))) { + BOOST_LOG(error) << "NvEnc: cuGraphicsMapResources() failed: error " << last_cuda_error; + return false; + } + + auto unmap = [&]() -> bool { + if (cuda_failed(cuda_functions.cuGraphicsUnmapResources(1, &cuda_d3d_input_texture, 0))) { + BOOST_LOG(error) << "NvEnc: cuGraphicsUnmapResources() failed: error " << last_cuda_error; + return false; + } + return true; + }; + auto unmap_guard = util::fail_guard(unmap); + + CUarray input_texture_array; + if (cuda_failed(cuda_functions.cuGraphicsSubResourceGetMappedArray(&input_texture_array, cuda_d3d_input_texture, 0, 0))) { + BOOST_LOG(error) << "NvEnc: cuGraphicsSubResourceGetMappedArray() failed: error " << last_cuda_error; + return false; + } + + { + CUDA_MEMCPY2D copy_params = {}; + copy_params.srcMemoryType = CU_MEMORYTYPE_ARRAY; + copy_params.srcArray = input_texture_array; + copy_params.dstMemoryType = CU_MEMORYTYPE_DEVICE; + copy_params.dstDevice = cuda_surface; + copy_params.dstPitch = cuda_surface_pitch; + // Planar 16-bit YUV + copy_params.WidthInBytes = encoder_params.width * 2; + copy_params.Height = encoder_params.height * 3; + + if (cuda_failed(cuda_functions.cuMemcpy2D(©_params))) { + BOOST_LOG(error) << "NvEnc: cuMemcpy2D() failed: error " << last_cuda_error; + return false; + } + } + + unmap_guard.disable(); + return unmap(); + } + + bool nvenc_d3d11_on_cuda::cuda_succeeded(CUresult result) { + last_cuda_error = result; + return result == CUDA_SUCCESS; + } + + bool nvenc_d3d11_on_cuda::cuda_failed(CUresult result) { + last_cuda_error = result; + return result != CUDA_SUCCESS; + } + + nvenc_d3d11_on_cuda::autopop_context::~autopop_context() { + if (pushed_context) { + CUcontext popped_context; + if (parent.cuda_failed(parent.cuda_functions.cuCtxPopCurrent(&popped_context))) { + BOOST_LOG(error) << "NvEnc: cuCtxPopCurrent() failed: error " << parent.last_cuda_error; + } + } + } + + nvenc_d3d11_on_cuda::autopop_context nvenc_d3d11_on_cuda::push_context() { + if (cuda_context && + cuda_succeeded(cuda_functions.cuCtxPushCurrent(cuda_context))) { + return {*this, cuda_context}; + } else { + BOOST_LOG(error) << "NvEnc: cuCtxPushCurrent() failed: error " << last_cuda_error; + return {*this, nullptr}; + } + } + +} // namespace nvenc +#endif diff --git a/src/nvenc/nvenc_d3d11_on_cuda.h b/src/nvenc/nvenc_d3d11_on_cuda.h index 81114321947..1c912f50a2f 100644 --- a/src/nvenc/nvenc_d3d11_on_cuda.h +++ b/src/nvenc/nvenc_d3d11_on_cuda.h @@ -1,96 +1,89 @@ -/** - * @file src/nvenc/nvenc_d3d11_on_cuda.h - * @brief Declarations for CUDA NVENC encoder with Direct3D11 input surfaces. - */ -#pragma once -#ifdef _WIN32 - - #include "nvenc_d3d11.h" - - #include - -namespace nvenc { - - /** - * @brief Interop Direct3D11 on CUDA NVENC encoder. - * Input surface is Direct3D11, encoding is performed by CUDA. - */ - class nvenc_d3d11_on_cuda final: public nvenc_d3d11 { - public: - /** - * @param d3d_device Direct3D11 device that will create input surface texture. - * CUDA encoding device will be derived from it. - */ - explicit nvenc_d3d11_on_cuda(ID3D11Device *d3d_device); - ~nvenc_d3d11_on_cuda(); - - ID3D11Texture2D * - get_input_texture() override; - - private: - bool - init_library() override; - - bool - create_and_register_input_buffer() override; - - bool - synchronize_input_buffer() override; - - bool - cuda_succeeded(CUresult result); - - bool - cuda_failed(CUresult result); - - struct autopop_context { - autopop_context(nvenc_d3d11_on_cuda &parent, CUcontext pushed_context): - parent(parent), - pushed_context(pushed_context) { - } - - ~autopop_context(); - - explicit - operator bool() const { - return pushed_context != nullptr; - } - - nvenc_d3d11_on_cuda &parent; - CUcontext pushed_context = nullptr; - }; - - autopop_context - push_context(); - - HMODULE dll = NULL; - const ID3D11DevicePtr d3d_device; - ID3D11Texture2DPtr d3d_input_texture; - - struct { - tcuInit *cuInit; - tcuD3D11GetDevice *cuD3D11GetDevice; - tcuCtxCreate_v2 *cuCtxCreate; - tcuCtxDestroy_v2 *cuCtxDestroy; - tcuCtxPushCurrent_v2 *cuCtxPushCurrent; - tcuCtxPopCurrent_v2 *cuCtxPopCurrent; - tcuMemAllocPitch_v2 *cuMemAllocPitch; - tcuMemFree_v2 *cuMemFree; - tcuGraphicsD3D11RegisterResource *cuGraphicsD3D11RegisterResource; - tcuGraphicsUnregisterResource *cuGraphicsUnregisterResource; - tcuGraphicsMapResources *cuGraphicsMapResources; - tcuGraphicsUnmapResources *cuGraphicsUnmapResources; - tcuGraphicsSubResourceGetMappedArray *cuGraphicsSubResourceGetMappedArray; - tcuMemcpy2D_v2 *cuMemcpy2D; - HMODULE dll; - } cuda_functions = {}; - - CUresult last_cuda_error = CUDA_SUCCESS; - CUcontext cuda_context = nullptr; - CUgraphicsResource cuda_d3d_input_texture = nullptr; - CUdeviceptr cuda_surface = 0; - size_t cuda_surface_pitch = 0; - }; - -} // namespace nvenc -#endif +/** + * @file src/nvenc/nvenc_d3d11_on_cuda.h + * @brief Declarations for CUDA NVENC encoder with Direct3D11 input surfaces. + */ +#pragma once +#ifdef _WIN32 + // lib includes + #include + + // local includes + #include "nvenc_d3d11.h" + +namespace nvenc { + + /** + * @brief Interop Direct3D11 on CUDA NVENC encoder. + * Input surface is Direct3D11, encoding is performed by CUDA. + */ + class nvenc_d3d11_on_cuda final: public nvenc_d3d11 { + public: + /** + * @param d3d_device Direct3D11 device that will create input surface texture. + * CUDA encoding device will be derived from it. + */ + explicit nvenc_d3d11_on_cuda(ID3D11Device *d3d_device); + ~nvenc_d3d11_on_cuda(); + + ID3D11Texture2D *get_input_texture() override; + + private: + bool init_library() override; + + bool create_and_register_input_buffer() override; + + bool synchronize_input_buffer() override; + + bool cuda_succeeded(CUresult result); + + bool cuda_failed(CUresult result); + + struct autopop_context { + autopop_context(nvenc_d3d11_on_cuda &parent, CUcontext pushed_context): + parent(parent), + pushed_context(pushed_context) { + } + + ~autopop_context(); + + explicit operator bool() const { + return pushed_context != nullptr; + } + + nvenc_d3d11_on_cuda &parent; + CUcontext pushed_context = nullptr; + }; + + autopop_context push_context(); + + HMODULE dll = NULL; + const ID3D11DevicePtr d3d_device; + ID3D11Texture2DPtr d3d_input_texture; + + struct { + tcuInit *cuInit; + tcuD3D11GetDevice *cuD3D11GetDevice; + tcuCtxCreate_v2 *cuCtxCreate; + tcuCtxDestroy_v2 *cuCtxDestroy; + tcuCtxPushCurrent_v2 *cuCtxPushCurrent; + tcuCtxPopCurrent_v2 *cuCtxPopCurrent; + tcuMemAllocPitch_v2 *cuMemAllocPitch; + tcuMemFree_v2 *cuMemFree; + tcuGraphicsD3D11RegisterResource *cuGraphicsD3D11RegisterResource; + tcuGraphicsUnregisterResource *cuGraphicsUnregisterResource; + tcuGraphicsMapResources *cuGraphicsMapResources; + tcuGraphicsUnmapResources *cuGraphicsUnmapResources; + tcuGraphicsSubResourceGetMappedArray *cuGraphicsSubResourceGetMappedArray; + tcuMemcpy2D_v2 *cuMemcpy2D; + HMODULE dll; + } cuda_functions = {}; + + CUresult last_cuda_error = CUDA_SUCCESS; + CUcontext cuda_context = nullptr; + CUgraphicsResource cuda_d3d_input_texture = nullptr; + CUdeviceptr cuda_surface = 0; + size_t cuda_surface_pitch = 0; + }; + +} // namespace nvenc +#endif diff --git a/src/nvenc/nvenc_encoded_frame.h b/src/nvenc/nvenc_encoded_frame.h index 46a8e46dee6..ff94de463f9 100644 --- a/src/nvenc/nvenc_encoded_frame.h +++ b/src/nvenc/nvenc_encoded_frame.h @@ -1,22 +1,23 @@ -/** - * @file src/nvenc/nvenc_encoded_frame.h - * @brief Declarations for NVENC encoded frame. - */ -#pragma once - -#include -#include - -namespace nvenc { - - /** - * @brief Encoded frame. - */ - struct nvenc_encoded_frame { - std::vector data; - uint64_t frame_index = 0; - bool idr = false; - bool after_ref_frame_invalidation = false; - }; - -} // namespace nvenc +/** + * @file src/nvenc/nvenc_encoded_frame.h + * @brief Declarations for NVENC encoded frame. + */ +#pragma once + +// standard includes +#include +#include + +namespace nvenc { + + /** + * @brief Encoded frame. + */ + struct nvenc_encoded_frame { + std::vector data; + uint64_t frame_index = 0; + bool idr = false; + bool after_ref_frame_invalidation = false; + }; + +} // namespace nvenc diff --git a/src/nvenc/nvenc_utils.cpp b/src/nvenc/nvenc_utils.cpp index 26e2dc30d24..2d19bd46299 100644 --- a/src/nvenc/nvenc_utils.cpp +++ b/src/nvenc/nvenc_utils.cpp @@ -1,94 +1,93 @@ -/** - * @file src/nvenc/nvenc_utils.cpp - * @brief Definitions for NVENC utilities. - */ -#include - -#include "nvenc_utils.h" - -namespace nvenc { - -#ifdef _WIN32 - DXGI_FORMAT - dxgi_format_from_nvenc_format(NV_ENC_BUFFER_FORMAT format) { - switch (format) { - case NV_ENC_BUFFER_FORMAT_YUV420_10BIT: - return DXGI_FORMAT_P010; - - case NV_ENC_BUFFER_FORMAT_NV12: - return DXGI_FORMAT_NV12; - - case NV_ENC_BUFFER_FORMAT_AYUV: - return DXGI_FORMAT_AYUV; - - case NV_ENC_BUFFER_FORMAT_YUV444_10BIT: - return DXGI_FORMAT_R16_UINT; - - default: - return DXGI_FORMAT_UNKNOWN; - } - } -#endif - - NV_ENC_BUFFER_FORMAT - nvenc_format_from_sunshine_format(platf::pix_fmt_e format) { - switch (format) { - case platf::pix_fmt_e::nv12: - return NV_ENC_BUFFER_FORMAT_NV12; - - case platf::pix_fmt_e::p010: - return NV_ENC_BUFFER_FORMAT_YUV420_10BIT; - - case platf::pix_fmt_e::ayuv: - return NV_ENC_BUFFER_FORMAT_AYUV; - - case platf::pix_fmt_e::yuv444p16: - return NV_ENC_BUFFER_FORMAT_YUV444_10BIT; - - default: - return NV_ENC_BUFFER_FORMAT_UNDEFINED; - } - } - - nvenc_colorspace_t - nvenc_colorspace_from_sunshine_colorspace(const video::sunshine_colorspace_t &sunshine_colorspace) { - nvenc_colorspace_t colorspace; - - switch (sunshine_colorspace.colorspace) { - case video::colorspace_e::rec601: - // Rec. 601 - colorspace.primaries = NV_ENC_VUI_COLOR_PRIMARIES_SMPTE170M; - colorspace.tranfer_function = NV_ENC_VUI_TRANSFER_CHARACTERISTIC_SMPTE170M; - colorspace.matrix = NV_ENC_VUI_MATRIX_COEFFS_SMPTE170M; - break; - - case video::colorspace_e::rec709: - // Rec. 709 - colorspace.primaries = NV_ENC_VUI_COLOR_PRIMARIES_BT709; - colorspace.tranfer_function = NV_ENC_VUI_TRANSFER_CHARACTERISTIC_BT709; - colorspace.matrix = NV_ENC_VUI_MATRIX_COEFFS_BT709; - break; - - case video::colorspace_e::bt2020sdr: - // Rec. 2020 - colorspace.primaries = NV_ENC_VUI_COLOR_PRIMARIES_BT2020; - assert(sunshine_colorspace.bit_depth == 10); - colorspace.tranfer_function = NV_ENC_VUI_TRANSFER_CHARACTERISTIC_BT2020_10; - colorspace.matrix = NV_ENC_VUI_MATRIX_COEFFS_BT2020_NCL; - break; - - case video::colorspace_e::bt2020: - // Rec. 2020 with ST 2084 perceptual quantizer - colorspace.primaries = NV_ENC_VUI_COLOR_PRIMARIES_BT2020; - assert(sunshine_colorspace.bit_depth == 10); - colorspace.tranfer_function = NV_ENC_VUI_TRANSFER_CHARACTERISTIC_SMPTE2084; - colorspace.matrix = NV_ENC_VUI_MATRIX_COEFFS_BT2020_NCL; - break; - } - - colorspace.full_range = sunshine_colorspace.full_range; - - return colorspace; - } - -} // namespace nvenc +/** + * @file src/nvenc/nvenc_utils.cpp + * @brief Definitions for NVENC utilities. + */ +// standard includes +#include + +// local includes +#include "nvenc_utils.h" + +namespace nvenc { + +#ifdef _WIN32 + DXGI_FORMAT dxgi_format_from_nvenc_format(NV_ENC_BUFFER_FORMAT format) { + switch (format) { + case NV_ENC_BUFFER_FORMAT_YUV420_10BIT: + return DXGI_FORMAT_P010; + + case NV_ENC_BUFFER_FORMAT_NV12: + return DXGI_FORMAT_NV12; + + case NV_ENC_BUFFER_FORMAT_AYUV: + return DXGI_FORMAT_AYUV; + + case NV_ENC_BUFFER_FORMAT_YUV444_10BIT: + return DXGI_FORMAT_R16_UINT; + + default: + return DXGI_FORMAT_UNKNOWN; + } + } +#endif + + NV_ENC_BUFFER_FORMAT nvenc_format_from_sunshine_format(platf::pix_fmt_e format) { + switch (format) { + case platf::pix_fmt_e::nv12: + return NV_ENC_BUFFER_FORMAT_NV12; + + case platf::pix_fmt_e::p010: + return NV_ENC_BUFFER_FORMAT_YUV420_10BIT; + + case platf::pix_fmt_e::ayuv: + return NV_ENC_BUFFER_FORMAT_AYUV; + + case platf::pix_fmt_e::yuv444p16: + return NV_ENC_BUFFER_FORMAT_YUV444_10BIT; + + default: + return NV_ENC_BUFFER_FORMAT_UNDEFINED; + } + } + + nvenc_colorspace_t nvenc_colorspace_from_sunshine_colorspace(const video::sunshine_colorspace_t &sunshine_colorspace) { + nvenc_colorspace_t colorspace; + + switch (sunshine_colorspace.colorspace) { + case video::colorspace_e::rec601: + // Rec. 601 + colorspace.primaries = NV_ENC_VUI_COLOR_PRIMARIES_SMPTE170M; + colorspace.tranfer_function = NV_ENC_VUI_TRANSFER_CHARACTERISTIC_SMPTE170M; + colorspace.matrix = NV_ENC_VUI_MATRIX_COEFFS_SMPTE170M; + break; + + case video::colorspace_e::rec709: + // Rec. 709 + colorspace.primaries = NV_ENC_VUI_COLOR_PRIMARIES_BT709; + colorspace.tranfer_function = NV_ENC_VUI_TRANSFER_CHARACTERISTIC_BT709; + colorspace.matrix = NV_ENC_VUI_MATRIX_COEFFS_BT709; + break; + + case video::colorspace_e::bt2020sdr: + // Rec. 2020 + colorspace.primaries = NV_ENC_VUI_COLOR_PRIMARIES_BT2020; + assert(sunshine_colorspace.bit_depth == 10); + colorspace.tranfer_function = NV_ENC_VUI_TRANSFER_CHARACTERISTIC_BT2020_10; + colorspace.matrix = NV_ENC_VUI_MATRIX_COEFFS_BT2020_NCL; + break; + + case video::colorspace_e::bt2020: + // Rec. 2020 with ST 2084 perceptual quantizer + colorspace.primaries = NV_ENC_VUI_COLOR_PRIMARIES_BT2020; + assert(sunshine_colorspace.bit_depth == 10); + colorspace.tranfer_function = NV_ENC_VUI_TRANSFER_CHARACTERISTIC_SMPTE2084; + colorspace.matrix = NV_ENC_VUI_MATRIX_COEFFS_BT2020_NCL; + break; + } + + colorspace.full_range = sunshine_colorspace.full_range; + + return colorspace; + } + +} // namespace nvenc diff --git a/src/nvenc/nvenc_utils.h b/src/nvenc/nvenc_utils.h index db42867679a..e8b48eee455 100644 --- a/src/nvenc/nvenc_utils.h +++ b/src/nvenc/nvenc_utils.h @@ -1,31 +1,30 @@ -/** - * @file src/nvenc/nvenc_utils.h - * @brief Declarations for NVENC utilities. - */ -#pragma once - -#ifdef _WIN32 - #include -#endif - -#include "nvenc_colorspace.h" - -#include "src/platform/common.h" -#include "src/video_colorspace.h" - -#include - -namespace nvenc { - -#ifdef _WIN32 - DXGI_FORMAT - dxgi_format_from_nvenc_format(NV_ENC_BUFFER_FORMAT format); -#endif - - NV_ENC_BUFFER_FORMAT - nvenc_format_from_sunshine_format(platf::pix_fmt_e format); - - nvenc_colorspace_t - nvenc_colorspace_from_sunshine_colorspace(const video::sunshine_colorspace_t &sunshine_colorspace); - -} // namespace nvenc +/** + * @file src/nvenc/nvenc_utils.h + * @brief Declarations for NVENC utilities. + */ +#pragma once + +// plafform includes +#ifdef _WIN32 + #include +#endif + +// lib includes +#include + +// local includes +#include "nvenc_colorspace.h" +#include "src/platform/common.h" +#include "src/video_colorspace.h" + +namespace nvenc { + +#ifdef _WIN32 + DXGI_FORMAT dxgi_format_from_nvenc_format(NV_ENC_BUFFER_FORMAT format); +#endif + + NV_ENC_BUFFER_FORMAT nvenc_format_from_sunshine_format(platf::pix_fmt_e format); + + nvenc_colorspace_t nvenc_colorspace_from_sunshine_colorspace(const video::sunshine_colorspace_t &sunshine_colorspace); + +} // namespace nvenc diff --git a/src/nvhttp.cpp b/src/nvhttp.cpp index ea9248e715a..6b518bf91e9 100644 --- a/src/nvhttp.cpp +++ b/src/nvhttp.cpp @@ -7,21 +7,20 @@ // standard includes #include +#include #include // lib includes -#include -#include #include #include #include #include #include -#include +#include // local includes #include "config.h" -#include "crypto.h" +#include "display_device.h" #include "file_handler.h" #include "globals.h" #include "httpcommon.h" @@ -37,6 +36,7 @@ #include "video.h" using namespace std::literals; + namespace nvhttp { namespace fs = std::filesystem; @@ -44,18 +44,6 @@ namespace nvhttp { crypto::cert_chain_t cert_chain; - class SunshineHTTPS: public SimpleWeb::HTTPS { - public: - SunshineHTTPS(boost::asio::io_service &io_service, boost::asio::ssl::context &ctx): - SimpleWeb::HTTPS(io_service, ctx) {} - - virtual ~SunshineHTTPS() { - // Gracefully shutdown the TLS connection - SimpleWeb::error_code ec; - shutdown(ec); - } - }; - class SunshineHTTPSServer: public SimpleWeb::ServerBase { public: SunshineHTTPSServer(const std::string &certification_file, const std::string &private_key_file): @@ -74,8 +62,7 @@ namespace nvhttp { protected: boost::asio::ssl::context context; - void - after_bind() override { + void after_bind() override { if (verify) { context.set_verify_mode(boost::asio::ssl::verify_peer | boost::asio::ssl::verify_fail_if_no_peer_cert | boost::asio::ssl::verify_client_once); context.set_verify_callback([](int verified, boost::asio::ssl::verify_context &ctx) { @@ -86,17 +73,18 @@ namespace nvhttp { } // This is Server::accept() with SSL validation support added - void - accept() override { + void accept() override { auto connection = create_connection(*io_service, context); acceptor->async_accept(connection->socket->lowest_layer(), [this, connection](const SimpleWeb::error_code &ec) { auto lock = connection->handler_runner->continue_lock(); - if (!lock) + if (!lock) { return; + } - if (ec != SimpleWeb::error::operation_aborted) + if (ec != SimpleWeb::error::operation_aborted) { this->accept(); + } auto session = std::make_shared(config.max_request_streambuf_size, connection); @@ -109,20 +97,22 @@ namespace nvhttp { session->connection->socket->async_handshake(boost::asio::ssl::stream_base::server, [this, session](const SimpleWeb::error_code &ec) { session->connection->cancel_timeout(); auto lock = session->connection->handler_runner->continue_lock(); - if (!lock) + if (!lock) { return; + } if (!ec) { - if (verify && !verify(session->connection->socket->native_handle())) + if (verify && !verify(session->connection->socket->native_handle())) { this->write(session, on_verify_failed); - else + } else { this->read(session); - } - else if (this->on_error) + } + } else if (this->on_error) { this->on_error(session->request, ec); + } }); - } - else if (this->on_error) + } else if (this->on_error) { this->on_error(session->request, ec); + } }); } }; @@ -145,28 +135,6 @@ namespace nvhttp { std::vector named_devices; }; - struct pair_session_t { - struct { - std::string uniqueID; - std::string cert; - std::string name; - } client; - - std::unique_ptr cipher_key; - std::vector clienthash; - - std::string serversecret; - std::string serverchallenge; - - struct { - util::Either< - std::shared_ptr::Response>, - std::shared_ptr::Response>> - response; - std::string salt; - } async_insert_pin; - }; - // uniqueID, session std::unordered_map map_id_sess; client_t client_root; @@ -183,8 +151,7 @@ namespace nvhttp { REMOVE ///< Remove certificate }; - std::string - get_arg(const args_t &args, const char *name, const char *default_value = nullptr) { + 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) { @@ -196,15 +163,13 @@ namespace nvhttp { return it->second; } - void - save_state() { + void save_state() { pt::ptree root; if (fs::exists(config::nvhttp.file_state)) { try { pt::read_json(config::nvhttp.file_state, root); - } - catch (std::exception &e) { + } catch (std::exception &e) { BOOST_LOG(error) << "Couldn't read "sv << config::nvhttp.file_state << ": "sv << e.what(); return; } @@ -228,15 +193,13 @@ namespace nvhttp { try { pt::write_json(config::nvhttp.file_state, root); - } - catch (std::exception &e) { + } catch (std::exception &e) { BOOST_LOG(error) << "Couldn't write "sv << config::nvhttp.file_state << ": "sv << e.what(); return; } } - void - load_state() { + void load_state() { if (!fs::exists(config::nvhttp.file_state)) { BOOST_LOG(info) << "File "sv << config::nvhttp.file_state << " doesn't exist"sv; http::unique_id = uuid_util::uuid_t::generate().string(); @@ -246,8 +209,7 @@ namespace nvhttp { pt::ptree tree; try { pt::read_json(config::nvhttp.file_state, tree); - } - catch (std::exception &e) { + } catch (std::exception &e) { BOOST_LOG(error) << "Couldn't read "sv << config::nvhttp.file_state << ": "sv << e.what(); return; @@ -301,8 +263,7 @@ namespace nvhttp { client_root = client; } - void - add_authorized_client(const std::string &name, std::string &&cert) { + void add_authorized_client(const std::string &name, std::string &&cert) { client_t &client = client_root; named_cert_t named_cert; named_cert.name = name; @@ -315,8 +276,7 @@ namespace nvhttp { } } - std::shared_ptr - make_launch_session(bool host_audio, const args_t &args) { + std::shared_ptr make_launch_session(bool host_audio, const args_t &args) { auto launch_session = std::make_shared(); launch_session->id = ++session_id_counter; @@ -330,9 +290,15 @@ namespace nvhttp { int x = 0; std::string segment; while (std::getline(mode, segment, 'x')) { - if (x == 0) launch_session->width = atoi(segment.c_str()); - if (x == 1) launch_session->height = atoi(segment.c_str()); - if (x == 2) launch_session->fps = atoi(segment.c_str()); + if (x == 0) { + launch_session->width = atoi(segment.c_str()); + } + if (x == 1) { + launch_session->height = atoi(segment.c_str()); + } + if (x == 2) { + launch_session->fps = atoi(segment.c_str()); + } x++; } launch_session->unique_id = (get_arg(args, "uniqueid", "unknown")); @@ -347,7 +313,8 @@ namespace nvhttp { auto corever = util::from_view(get_arg(args, "corever", "0")); if (corever >= 1) { launch_session->rtsp_cipher = crypto::cipher::gcm_t { - launch_session->gcm_key, false + launch_session->gcm_key, + false }; launch_session->rtsp_iv_counter = 0; } @@ -366,16 +333,30 @@ namespace nvhttp { return launch_session; } - void - getservercert(pair_session_t &sess, pt::ptree &tree, const std::string &pin) { + void remove_session(const pair_session_t &sess) { + map_id_sess.erase(sess.client.uniqueID); + } + + void fail_pair(pair_session_t &sess, pt::ptree &tree, const std::string status_msg) { + tree.put("root.paired", 0); + tree.put("root..status_code", 400); + tree.put("root..status_message", status_msg); + remove_session(sess); // Security measure, delete the session when something went wrong and force a re-pair + } + + void getservercert(pair_session_t &sess, pt::ptree &tree, const std::string &pin) { + if (sess.last_phase != PAIR_PHASE::NONE) { + fail_pair(sess, tree, "Out of order call to getservercert"); + return; + } + sess.last_phase = PAIR_PHASE::GETSERVERCERT; + if (sess.async_insert_pin.salt.size() < 32) { - tree.put("root.paired", 0); - tree.put("root..status_code", 400); - tree.put("root..status_message", "Salt too short"); + fail_pair(sess, tree, "Salt too short"); return; } - std::string_view salt_view { sess.async_insert_pin.salt.data(), 32 }; + std::string_view salt_view {sess.async_insert_pin.salt.data(), 32}; auto salt = util::from_hex>(salt_view, true); @@ -387,31 +368,17 @@ namespace nvhttp { tree.put("root..status_code", 200); } - void - serverchallengeresp(pair_session_t &sess, pt::ptree &tree, const args_t &args) { - auto encrypted_response = util::from_hex_vec(get_arg(args, "serverchallengeresp"), true); - - std::vector decrypted; - crypto::cipher::ecb_t cipher(*sess.cipher_key, false); - - cipher.decrypt(encrypted_response, decrypted); - - sess.clienthash = std::move(decrypted); - - auto serversecret = sess.serversecret; - auto sign = crypto::sign256(crypto::pkey(conf_intern.pkey), serversecret); - - serversecret.insert(std::end(serversecret), std::begin(sign), std::end(sign)); - - tree.put("root.pairingsecret", util::hex_vec(serversecret, true)); - tree.put("root.paired", 1); - tree.put("root..status_code", 200); - } - - void - clientchallenge(pair_session_t &sess, pt::ptree &tree, const args_t &args) { - auto challenge = util::from_hex_vec(get_arg(args, "clientchallenge"), true); + void clientchallenge(pair_session_t &sess, pt::ptree &tree, const std::string &challenge) { + if (sess.last_phase != PAIR_PHASE::GETSERVERCERT) { + fail_pair(sess, tree, "Out of order call to clientchallenge"); + return; + } + sess.last_phase = PAIR_PHASE::CLIENTCHALLENGE; + if (!sess.cipher_key) { + fail_pair(sess, tree, "Cipher key not set"); + return; + } crypto::cipher::ecb_t cipher(*sess.cipher_key, false); std::vector decrypted; @@ -424,7 +391,7 @@ namespace nvhttp { decrypted.insert(std::end(decrypted), std::begin(sign), std::end(sign)); decrypted.insert(std::end(decrypted), std::begin(serversecret), std::end(serversecret)); - auto hash = crypto::hash({ (char *) decrypted.data(), decrypted.size() }); + auto hash = crypto::hash({(char *) decrypted.data(), decrypted.size()}); auto serverchallenge = crypto::rand(16); std::string plaintext; @@ -444,22 +411,57 @@ namespace nvhttp { tree.put("root..status_code", 200); } - void - clientpairingsecret(std::shared_ptr> &add_cert, pair_session_t &sess, pt::ptree &tree, const args_t &args) { + void serverchallengeresp(pair_session_t &sess, pt::ptree &tree, const std::string &encrypted_response) { + if (sess.last_phase != PAIR_PHASE::CLIENTCHALLENGE) { + fail_pair(sess, tree, "Out of order call to serverchallengeresp"); + return; + } + sess.last_phase = PAIR_PHASE::SERVERCHALLENGERESP; + + if (!sess.cipher_key || sess.serversecret.empty()) { + fail_pair(sess, tree, "Cipher key or serversecret not set"); + return; + } + + std::vector decrypted; + crypto::cipher::ecb_t cipher(*sess.cipher_key, false); + + cipher.decrypt(encrypted_response, decrypted); + + sess.clienthash = std::move(decrypted); + + auto serversecret = sess.serversecret; + auto sign = crypto::sign256(crypto::pkey(conf_intern.pkey), serversecret); + + serversecret.insert(std::end(serversecret), std::begin(sign), std::end(sign)); + + tree.put("root.pairingsecret", util::hex_vec(serversecret, true)); + tree.put("root.paired", 1); + tree.put("root..status_code", 200); + } + + void clientpairingsecret(pair_session_t &sess, std::shared_ptr> &add_cert, pt::ptree &tree, const std::string &client_pairing_secret) { + if (sess.last_phase != PAIR_PHASE::SERVERCHALLENGERESP) { + fail_pair(sess, tree, "Out of order call to clientpairingsecret"); + return; + } + sess.last_phase = PAIR_PHASE::CLIENTPAIRINGSECRET; + auto &client = sess.client; - auto pairingsecret = util::from_hex_vec(get_arg(args, "clientpairingsecret"), true); - if (pairingsecret.size() <= 16) { - tree.put("root.paired", 0); - tree.put("root..status_code", 400); - tree.put("root..status_message", "Clientpairingsecret too short"); + if (client_pairing_secret.size() <= 16) { + fail_pair(sess, tree, "Client pairing secret too short"); return; } - std::string_view secret { pairingsecret.data(), 16 }; - std::string_view sign { pairingsecret.data() + secret.size(), pairingsecret.size() - secret.size() }; + std::string_view secret {client_pairing_secret.data(), 16}; + std::string_view sign {client_pairing_secret.data() + secret.size(), client_pairing_secret.size() - secret.size()}; auto x509 = crypto::x509(client.cert); + if (!x509) { + fail_pair(sess, tree, "Invalid client certificate"); + return; + } auto x509_sign = crypto::signature(x509); std::string data; @@ -472,39 +474,37 @@ namespace nvhttp { auto hash = crypto::hash(data); // if hash not correct, probably MITM - if (!std::memcmp(hash.data(), sess.clienthash.data(), hash.size()) && crypto::verify256(crypto::x509(client.cert), secret, sign)) { + bool same_hash = hash.size() == sess.clienthash.size() && std::equal(hash.begin(), hash.end(), sess.clienthash.begin()); + auto verify = crypto::verify256(crypto::x509(client.cert), secret, sign); + if (same_hash && verify) { tree.put("root.paired", 1); add_cert->raise(crypto::x509(client.cert)); // The client is now successfully paired and will be authorized to connect - auto it = map_id_sess.find(client.uniqueID); add_authorized_client(client.name, std::move(client.cert)); - map_id_sess.erase(it); - } - else { - map_id_sess.erase(client.uniqueID); + } else { tree.put("root.paired", 0); } + remove_session(sess); tree.put("root..status_code", 200); } - template + template struct tunnel; - template <> + template<> struct tunnel { static auto constexpr to_string = "HTTPS"sv; }; - template <> + template<> struct tunnel { static auto constexpr to_string = "NONE"sv; }; - template - void - print_req(std::shared_ptr::Request> request) { + template + void print_req(std::shared_ptr::Request> request) { BOOST_LOG(debug) << "TUNNEL :: "sv << tunnel::to_string; BOOST_LOG(debug) << "METHOD :: "sv << request->method; @@ -523,9 +523,8 @@ namespace nvhttp { BOOST_LOG(debug) << " [--] "sv; } - template - void - not_found(std::shared_ptr::Response> response, std::shared_ptr::Request> request) { + template + void not_found(std::shared_ptr::Response> response, std::shared_ptr::Request> request) { print_req(request); pt::ptree tree; @@ -543,9 +542,8 @@ namespace nvhttp { response->close_connection_after_response = true; } - template - void - pair(std::shared_ptr> &add_cert, std::shared_ptr::Response> response, std::shared_ptr::Request> request) { + template + void pair(std::shared_ptr> &add_cert, std::shared_ptr::Response> response, std::shared_ptr::Request> request) { print_req(request); pt::ptree tree; @@ -566,8 +564,7 @@ namespace nvhttp { return; } - auto uniqID { get_arg(args, "uniqueid") }; - auto sess_it = map_id_sess.find(uniqID); + auto uniqID {get_arg(args, "uniqueid")}; args_t::const_iterator it; if (it = args.find("phrase"); it != std::end(args)) { @@ -588,8 +585,7 @@ namespace nvhttp { std::getline(std::cin, pin); getservercert(ptr->second, tree, pin); - } - else { + } else { #if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 system_tray::update_tray_require_pin(); #endif @@ -598,29 +594,37 @@ namespace nvhttp { fg.disable(); return; } - } - else if (it->second == "pairchallenge"sv) { + } else if (it->second == "pairchallenge"sv) { tree.put("root.paired", 1); tree.put("root..status_code", 200); + return; } } - else if (it = args.find("clientchallenge"); it != std::end(args)) { - clientchallenge(sess_it->second, tree, args); - } - else if (it = args.find("serverchallengeresp"); it != std::end(args)) { - serverchallengeresp(sess_it->second, tree, args); - } - else if (it = args.find("clientpairingsecret"); it != std::end(args)) { - clientpairingsecret(add_cert, sess_it->second, tree, args); + + auto sess_it = map_id_sess.find(uniqID); + if (sess_it == std::end(map_id_sess)) { + tree.put("root..status_code", 400); + tree.put("root..status_message", "Invalid uniqueid"); + + return; } - else { + + if (it = args.find("clientchallenge"); it != std::end(args)) { + auto challenge = util::from_hex_vec(it->second, true); + clientchallenge(sess_it->second, tree, challenge); + } else if (it = args.find("serverchallengeresp"); it != std::end(args)) { + auto encrypted_response = util::from_hex_vec(it->second, true); + serverchallengeresp(sess_it->second, tree, encrypted_response); + } else if (it = args.find("clientpairingsecret"); it != std::end(args)) { + auto pairingsecret = util::from_hex_vec(it->second, true); + clientpairingsecret(sess_it->second, add_cert, tree, pairingsecret); + } else { tree.put("root..status_code", 404); tree.put("root..status_message", "Invalid pairing request"); } } - bool - pin(std::string pin, std::string name) { + bool pin(std::string pin, std::string name) { pt::ptree tree; if (map_id_sess.empty()) { return false; @@ -631,7 +635,9 @@ namespace nvhttp { tree.put("root.paired", 0); tree.put("root..status_code", 400); tree.put( - "root..status_message", "Pin must be 4 digits, " + std::to_string(pin.size()) + " provided"); + "root..status_message", + "Pin must be 4 digits, " + std::to_string(pin.size()) + " provided" + ); return false; } @@ -654,11 +660,9 @@ namespace nvhttp { auto &async_response = sess.async_insert_pin.response; if (async_response.has_left() && async_response.left()) { async_response.left()->write(data.str()); - } - else if (async_response.has_right() && async_response.right()) { + } else if (async_response.has_right() && async_response.right()) { async_response.right()->write(data.str()); - } - else { + } else { return false; } @@ -668,9 +672,8 @@ namespace nvhttp { return true; } - template - void - serverinfo(std::shared_ptr::Response> response, std::shared_ptr::Request> request) { + template + void serverinfo(std::shared_ptr::Response> response, std::shared_ptr::Request> request) { print_req(request); int pair_status = 0; @@ -701,8 +704,7 @@ namespace nvhttp { // For HTTP requests, use a placeholder MAC address that Moonlight knows to ignore. if constexpr (std::is_same_v) { tree.put("root.mac", platf::get_mac_address(net::addr_to_normalized_string(local_endpoint.address()))); - } - else { + } else { tree.put("root.mac", "00:00:00:00:00:00"); } @@ -717,8 +719,7 @@ namespace nvhttp { // support know to ignore this bogus address. if (local_endpoint.address().is_v6() && !local_endpoint.address().to_v6().is_v4_mapped()) { tree.put("root.LocalIP", "127.0.0.1"); - } - else { + } else { tree.put("root.LocalIP", net::addr_to_normalized_string(local_endpoint.address())); } @@ -764,22 +765,20 @@ namespace nvhttp { response->close_connection_after_response = true; } - pt::ptree - get_all_clients() { - pt::ptree named_cert_nodes; + nlohmann::json get_all_clients() { + nlohmann::json named_cert_nodes = nlohmann::json::array(); client_t &client = client_root; for (auto &named_cert : client.named_devices) { - pt::ptree named_cert_node; - named_cert_node.put("name"s, named_cert.name); - named_cert_node.put("uuid"s, named_cert.uuid); - named_cert_nodes.push_back(std::make_pair(""s, named_cert_node)); + nlohmann::json named_cert_node; + named_cert_node["name"] = named_cert.name; + named_cert_node["uuid"] = named_cert.uuid; + named_cert_nodes.push_back(named_cert_node); } return named_cert_nodes; } - void - applist(resp_https_t response, req_https_t request) { + void applist(resp_https_t response, req_https_t request) { print_req(request); pt::ptree tree; @@ -807,17 +806,21 @@ namespace nvhttp { } } - void - launch(bool &host_audio, resp_https_t response, req_https_t request) { + void launch(bool &host_audio, resp_https_t response, req_https_t request) { print_req(request); pt::ptree tree; + bool revert_display_configuration {false}; auto g = util::fail_guard([&]() { std::ostringstream data; pt::write_xml(data, tree); response->write(data.str()); response->close_connection_after_response = true; + + if (revert_display_configuration) { + display_device::revert_configuration(); + } }); auto args = request->parse_query_string(); @@ -825,7 +828,8 @@ namespace nvhttp { args.find("rikey"s) == std::end(args) || args.find("rikeyid"s) == std::end(args) || args.find("localAudioPlayMode"s) == std::end(args) || - args.find("appid"s) == std::end(args)) { + args.find("appid"s) == std::end(args) + ) { tree.put("root.resume", 0); tree.put("root..status_code", 400); tree.put("root..status_message", "Missing a required launch parameter"); @@ -844,11 +848,22 @@ namespace nvhttp { return; } - // Probe encoders again before streaming to ensure our chosen - // encoder matches the active GPU (which could have changed - // due to hotplugging, driver crash, primary monitor change, - // or any number of other factors). + host_audio = util::from_view(get_arg(args, "localAudioPlayMode")); + auto launch_session = make_launch_session(host_audio, args); + if (rtsp_stream::session_count() == 0) { + // The display should be restored in case something fails as there are no other sessions. + revert_display_configuration = true; + + // We want to prepare display only if there are no active sessions at + // the moment. This should be done before probing encoders as it could + // change the active displays. + display_device::configure_display(config::video, *launch_session); + + // Probe encoders again before streaming to ensure our chosen + // encoder matches the active GPU (which could have changed + // due to hotplugging, driver crash, primary monitor change, + // or any number of other factors). if (video::probe_encoders()) { tree.put("root..status_code", 503); tree.put("root..status_message", "Failed to initialize video capture/encoding. Is a display connected and turned on?"); @@ -858,9 +873,6 @@ namespace nvhttp { } } - host_audio = util::from_view(get_arg(args, "localAudioPlayMode")); - auto launch_session = make_launch_session(host_audio, args); - auto encryption_mode = net::encryption_mode_for_address(request->remote_endpoint().address()); if (!launch_session->rtsp_cipher && encryption_mode == config::ENCRYPTION_MODE_MANDATORY) { BOOST_LOG(error) << "Rejecting client that cannot comply with mandatory encryption requirement"sv; @@ -884,16 +896,16 @@ 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", 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.gamesession", 1); rtsp_stream::launch_session_raise(launch_session); + + // Stream was started successfully, we will revert the config when the app or session terminates + revert_display_configuration = false; } - void - resume(bool &host_audio, resp_https_t response, req_https_t request) { + void resume(bool &host_audio, resp_https_t response, req_https_t request) { print_req(request); pt::ptree tree; @@ -917,7 +929,8 @@ namespace nvhttp { auto args = request->parse_query_string(); if ( args.find("rikey"s) == std::end(args) || - args.find("rikeyid"s) == std::end(args)) { + args.find("rikeyid"s) == std::end(args) + ) { tree.put("root.resume", 0); tree.put("root..status_code", 400); tree.put("root..status_message", "Missing a required resume parameter"); @@ -925,7 +938,21 @@ namespace nvhttp { return; } - if (rtsp_stream::session_count() == 0) { + // Newer Moonlight clients send localAudioPlayMode on /resume too, + // so we should use it if it's present in the args and there are + // no active sessions we could be interfering with. + const bool no_active_sessions {rtsp_stream::session_count() == 0}; + if (no_active_sessions && args.find("localAudioPlayMode"s) != std::end(args)) { + host_audio = util::from_view(get_arg(args, "localAudioPlayMode")); + } + const auto launch_session = make_launch_session(host_audio, args); + + if (no_active_sessions) { + // We want to prepare display only if there are no active sessions at + // the moment. This should be done before probing encoders as it could + // change the active displays. + display_device::configure_display(config::video, *launch_session); + // Probe encoders again before streaming to ensure our chosen // encoder matches the active GPU (which could have changed // due to hotplugging, driver crash, primary monitor change, @@ -937,17 +964,8 @@ namespace nvhttp { return; } - - // Newer Moonlight clients send localAudioPlayMode on /resume too, - // so we should use it if it's present in the args and there are - // no active sessions we could be interfering with. - if (args.find("localAudioPlayMode"s) != std::end(args)) { - host_audio = util::from_view(get_arg(args, "localAudioPlayMode")); - } } - auto launch_session = make_launch_session(host_audio, args); - auto encryption_mode = net::encryption_mode_for_address(request->remote_endpoint().address()); if (!launch_session->rtsp_cipher && encryption_mode == config::ENCRYPTION_MODE_MANDATORY) { BOOST_LOG(error) << "Rejecting client that cannot comply with mandatory encryption requirement"sv; @@ -960,16 +978,13 @@ 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", 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.resume", 1); rtsp_stream::launch_session_raise(launch_session); } - void - cancel(resp_https_t response, req_https_t request) { + void cancel(resp_https_t response, req_https_t request) { print_req(request); pt::ptree tree; @@ -989,10 +1004,12 @@ namespace nvhttp { if (proc::proc.running() > 0) { proc::proc.terminate(); } + + // The config needs to be reverted regardless of whether "proc::proc.terminate()" was called or not. + display_device::revert_configuration(); } - void - appasset(resp_https_t response, req_https_t request) { + void appasset(resp_https_t response, req_https_t request) { print_req(request); auto args = request->parse_query_string(); @@ -1005,8 +1022,12 @@ namespace nvhttp { response->close_connection_after_response = true; } - void - start() { + void setup(const std::string &pkey, const std::string &cert) { + conf_intern.pkey = pkey; + conf_intern.servercert = cert; + } + + void start() { auto shutdown_event = mail::man->event(mail::shutdown); auto port_http = net::map_port(PORT_HTTP); @@ -1019,8 +1040,9 @@ namespace nvhttp { load_state(); } - conf_intern.pkey = file_handler::read_file(config::nvhttp.pkey.c_str()); - conf_intern.servercert = file_handler::read_file(config::nvhttp.cert.c_str()); + auto pkey = file_handler::read_file(config::nvhttp.pkey.c_str()); + auto cert = file_handler::read_file(config::nvhttp.cert.c_str()); + setup(pkey, cert); auto add_cert = std::make_shared>(30); @@ -1028,7 +1050,7 @@ namespace nvhttp { // launch will store it in host_audio bool host_audio {}; - https_server_t https_server { config::nvhttp.cert, config::nvhttp.pkey }; + https_server_t https_server {config::nvhttp.cert, config::nvhttp.pkey}; http_server_t http_server; // Verify certificates after establishing connection @@ -1094,11 +1116,17 @@ namespace nvhttp { https_server.default_resource["GET"] = not_found; https_server.resource["^/serverinfo$"]["GET"] = serverinfo; - https_server.resource["^/pair$"]["GET"] = [&add_cert](auto resp, auto req) { pair(add_cert, resp, req); }; + https_server.resource["^/pair$"]["GET"] = [&add_cert](auto resp, auto req) { + pair(add_cert, resp, req); + }; https_server.resource["^/applist$"]["GET"] = applist; https_server.resource["^/appasset$"]["GET"] = appasset; - https_server.resource["^/launch$"]["GET"] = [&host_audio](auto resp, auto req) { launch(host_audio, resp, req); }; - https_server.resource["^/resume$"]["GET"] = [&host_audio](auto resp, auto req) { resume(host_audio, resp, req); }; + https_server.resource["^/launch$"]["GET"] = [&host_audio](auto resp, auto req) { + launch(host_audio, resp, req); + }; + https_server.resource["^/resume$"]["GET"] = [&host_audio](auto resp, auto req) { + resume(host_audio, resp, req); + }; https_server.resource["^/cancel$"]["GET"] = cancel; https_server.config.reuse_address = true; @@ -1107,7 +1135,9 @@ namespace nvhttp { http_server.default_resource["GET"] = not_found; http_server.resource["^/serverinfo$"]["GET"] = serverinfo; - http_server.resource["^/pair$"]["GET"] = [&add_cert](auto resp, auto req) { pair(add_cert, resp, req); }; + http_server.resource["^/pair$"]["GET"] = [&add_cert](auto resp, auto req) { + pair(add_cert, resp, req); + }; http_server.config.reuse_address = true; http_server.config.address = net::af_to_any_address_string(address_family); @@ -1116,8 +1146,7 @@ namespace nvhttp { auto accept_and_run = [&](auto *http_server) { try { http_server->start(); - } - catch (boost::system::system_error &err) { + } catch (boost::system::system_error &err) { // It's possible the exception gets thrown after calling http_server->stop() from a different thread if (shutdown_event->peek()) { return; @@ -1128,8 +1157,8 @@ namespace nvhttp { return; } }; - std::thread ssl { accept_and_run, &https_server }; - std::thread tcp { accept_and_run, &http_server }; + std::thread ssl {accept_and_run, &https_server}; + std::thread tcp {accept_and_run, &http_server}; // Wait for any event shutdown_event->view(); @@ -1141,24 +1170,21 @@ namespace nvhttp { tcp.join(); } - void - erase_all_clients() { + void erase_all_clients() { client_t client; client_root = client; cert_chain.clear(); save_state(); } - int - unpair_client(std::string uuid) { - int removed = 0; + bool unpair_client(const std::string_view uuid) { + bool removed = false; client_t &client = client_root; for (auto it = client.named_devices.begin(); it != client.named_devices.end();) { if ((*it).uuid == uuid) { it = client.named_devices.erase(it); - removed++; - } - else { + removed = true; + } else { ++it; } } diff --git a/src/nvhttp.h b/src/nvhttp.h index 1f8726c35e0..636337071b9 100644 --- a/src/nvhttp.h +++ b/src/nvhttp.h @@ -10,8 +10,11 @@ // lib includes #include +#include +#include // local includes +#include "crypto.h" #include "thread_safe.h" /** @@ -47,8 +50,119 @@ namespace nvhttp { * nvhttp::start(); * @examples_end */ - void - start(); + void start(); + + /** + * @brief Setup the nvhttp server. + * @param pkey + * @param cert + */ + void setup(const std::string &pkey, const std::string &cert); + + class SunshineHTTPS: public SimpleWeb::HTTPS { + public: + SunshineHTTPS(boost::asio::io_context &io_context, boost::asio::ssl::context &ctx): + SimpleWeb::HTTPS(io_context, ctx) { + } + + virtual ~SunshineHTTPS() { + // Gracefully shutdown the TLS connection + SimpleWeb::error_code ec; + shutdown(ec); + } + }; + + enum class PAIR_PHASE { + NONE, ///< Sunshine is not in a pairing phase + GETSERVERCERT, ///< Sunshine is in the get server certificate phase + CLIENTCHALLENGE, ///< Sunshine is in the client challenge phase + SERVERCHALLENGERESP, ///< Sunshine is in the server challenge response phase + CLIENTPAIRINGSECRET ///< Sunshine is in the client pairing secret phase + }; + + struct pair_session_t { + struct { + std::string uniqueID = {}; + std::string cert = {}; + std::string name = {}; + } client; + + std::unique_ptr cipher_key = {}; + std::vector clienthash = {}; + + std::string serversecret = {}; + std::string serverchallenge = {}; + + struct { + util::Either< + std::shared_ptr::Response>, + std::shared_ptr::Response>> + response; + std::string salt = {}; + } async_insert_pin; + + /** + * @brief used as a security measure to prevent out of order calls + */ + PAIR_PHASE last_phase = PAIR_PHASE::NONE; + }; + + /** + * @brief removes the temporary pairing session + * @param sess + */ + void remove_session(const pair_session_t &sess); + + /** + * @brief Pair, phase 1 + * + * Moonlight will send a salt and client certificate, we'll also need the user provided pin. + * + * PIN and SALT will be used to derive a shared AES key that needs to be stored + * in order to be used to decrypt_symmetric in the next phases. + * + * At this stage we only have to send back our public certificate. + */ + void getservercert(pair_session_t &sess, boost::property_tree::ptree &tree, const std::string &pin); + + /** + * @brief Pair, phase 2 + * + * Using the AES key that we generated in phase 1 we have to decrypt the client challenge, + * + * We generate a SHA256 hash with the following: + * - Decrypted challenge + * - Server certificate signature + * - Server secret: a randomly generated secret + * + * The hash + server_challenge will then be AES encrypted and sent as the `challengeresponse` in the returned XML + */ + void clientchallenge(pair_session_t &sess, boost::property_tree::ptree &tree, const std::string &challenge); + + /** + * @brief Pair, phase 3 + * + * Moonlight will send back a `serverchallengeresp`: an AES encrypted client hash, + * we have to send back the `pairingsecret`: + * using our private key we have to sign the certificate_signature + server_secret (generated in phase 2) + */ + void serverchallengeresp(pair_session_t &sess, boost::property_tree::ptree &tree, const std::string &encrypted_response); + + /** + * @brief Pair, phase 4 (final) + * + * We now have to use everything we exchanged before in order to verify and finally pair the clients + * + * We'll check the client_hash obtained at phase 3, it should contain the following: + * - The original server_challenge + * - The signature of the X509 client_cert + * - The unencrypted client_pairing_secret + * We'll check that SHA256(server_challenge + client_public_cert_signature + client_secret) == client_hash + * + * Then using the client certificate public key we should be able to verify that + * the client secret has been signed by Moonlight + */ + void clientpairingsecret(pair_session_t &sess, std::shared_ptr> &add_cert, boost::property_tree::ptree &tree, const std::string &client_pairing_secret); /** * @brief Compare the user supplied pin to the Moonlight pin. @@ -59,27 +173,25 @@ namespace nvhttp { * bool pin_status = nvhttp::pin("1234", "laptop"); * @examples_end */ - bool - pin(std::string pin, std::string name); + bool pin(std::string pin, std::string name); /** * @brief Remove single client. + * @param uuid The UUID of the client to remove. * @examples * nvhttp::unpair_client("4D7BB2DD-5704-A405-B41C-891A022932E1"); * @examples_end */ - int - unpair_client(std::string uniqueid); + bool unpair_client(std::string_view uuid); /** * @brief Get all paired clients. * @return The list of all paired clients. * @examples - * boost::property_tree::ptree clients = nvhttp::get_all_clients(); + * nlohmann::json clients = nvhttp::get_all_clients(); * @examples_end */ - boost::property_tree::ptree - get_all_clients(); + nlohmann::json get_all_clients(); /** * @brief Remove all paired clients. @@ -87,6 +199,5 @@ namespace nvhttp { * nvhttp::erase_all_clients(); * @examples_end */ - void - erase_all_clients(); + void erase_all_clients(); } // namespace nvhttp diff --git a/src/platform/common.h b/src/platform/common.h index 4b2ca66a06b..28704bb128e 100644 --- a/src/platform/common.h +++ b/src/platform/common.h @@ -4,18 +4,21 @@ */ #pragma once +// standard includes #include #include #include #include #include +// lib includes #include #ifndef _WIN32 #include #include #endif +// local includes #include "src/config.h" #include "src/logging.h" #include "src/thread_safe.h" @@ -44,13 +47,15 @@ namespace boost { class address; } // namespace ip } // namespace asio + namespace filesystem { class path; } + namespace process::inline v1 { class child; class group; - template + template class basic_environment; typedef basic_environment environment; } // namespace process::inline v1 @@ -59,6 +64,7 @@ namespace boost { namespace video { struct config_t; } // namespace video + namespace nvenc { class nvenc_base; } @@ -100,29 +106,27 @@ namespace platf { rumble_triggers, ///< Rumble triggers set_motion_event_state, ///< Set motion event state set_rgb_led, ///< Set RGB LED + set_adaptive_triggers, ///< Set adaptive triggers }; struct gamepad_feedback_msg_t { - static gamepad_feedback_msg_t - make_rumble(std::uint16_t id, std::uint16_t lowfreq, std::uint16_t highfreq) { + static gamepad_feedback_msg_t make_rumble(std::uint16_t id, std::uint16_t lowfreq, std::uint16_t highfreq) { gamepad_feedback_msg_t msg; msg.type = gamepad_feedback_e::rumble; msg.id = id; - msg.data.rumble = { lowfreq, highfreq }; + msg.data.rumble = {lowfreq, highfreq}; return msg; } - static gamepad_feedback_msg_t - make_rumble_triggers(std::uint16_t id, std::uint16_t left, std::uint16_t right) { + static gamepad_feedback_msg_t make_rumble_triggers(std::uint16_t id, std::uint16_t left, std::uint16_t right) { gamepad_feedback_msg_t msg; msg.type = gamepad_feedback_e::rumble_triggers; msg.id = id; - msg.data.rumble_triggers = { left, right }; + msg.data.rumble_triggers = {left, right}; return msg; } - static gamepad_feedback_msg_t - make_motion_event_state(std::uint16_t id, std::uint8_t motion_type, std::uint16_t report_rate) { + static gamepad_feedback_msg_t make_motion_event_state(std::uint16_t id, std::uint8_t motion_type, std::uint16_t report_rate) { gamepad_feedback_msg_t msg; msg.type = gamepad_feedback_e::set_motion_event_state; msg.id = id; @@ -131,35 +135,55 @@ namespace platf { return msg; } - static gamepad_feedback_msg_t - make_rgb_led(std::uint16_t id, std::uint8_t r, std::uint8_t g, std::uint8_t b) { + static gamepad_feedback_msg_t make_rgb_led(std::uint16_t id, std::uint8_t r, std::uint8_t g, std::uint8_t b) { gamepad_feedback_msg_t msg; msg.type = gamepad_feedback_e::set_rgb_led; msg.id = id; - msg.data.rgb_led = { r, g, b }; + msg.data.rgb_led = {r, g, b}; + return msg; + } + + static gamepad_feedback_msg_t make_adaptive_triggers(std::uint16_t id, uint8_t event_flags, uint8_t type_left, uint8_t type_right, const std::array &left, const std::array &right) { + gamepad_feedback_msg_t msg; + msg.type = gamepad_feedback_e::set_adaptive_triggers; + msg.id = id; + msg.data.adaptive_triggers = {.event_flags = event_flags, .type_left = type_left, .type_right = type_right, .left = left, .right = right}; return msg; } gamepad_feedback_e type; std::uint16_t id; + union { struct { std::uint16_t lowfreq; std::uint16_t highfreq; } rumble; + struct { std::uint16_t left_trigger; std::uint16_t right_trigger; } rumble_triggers; + struct { std::uint16_t report_rate; std::uint8_t motion_type; } motion_event_state; + struct { std::uint8_t r; std::uint8_t g; std::uint8_t b; } rgb_led; + + struct { + uint16_t controllerNumber; + uint8_t event_flags; + uint8_t type_left; + uint8_t type_right; + std::array left; + std::array right; + } adaptive_triggers; } data; }; @@ -179,7 +203,8 @@ namespace platf { }; constexpr std::uint8_t map_stereo[] { - FRONT_LEFT, FRONT_RIGHT + FRONT_LEFT, + FRONT_RIGHT }; constexpr std::uint8_t map_surround51[] { FRONT_LEFT, @@ -221,10 +246,9 @@ namespace platf { unknown ///< Unknown }; - inline std::string_view - from_pix_fmt(pix_fmt_e pix_fmt) { + inline std::string_view from_pix_fmt(pix_fmt_e pix_fmt) { using namespace std::literals; -#define _CONVERT(x) \ +#define _CONVERT(x) \ case pix_fmt_e::x: \ return #x##sv switch (pix_fmt) { @@ -344,10 +368,8 @@ namespace platf { img_t(img_t &&) = delete; img_t(const img_t &) = delete; - img_t & - operator=(img_t &&) = delete; - img_t & - operator=(const img_t &) = delete; + img_t &operator=(img_t &&) = delete; + img_t &operator=(const img_t &) = delete; std::uint8_t *data {}; std::int32_t width {}; @@ -371,14 +393,14 @@ namespace platf { std::string surround51; std::string surround71; }; + std::optional null; }; struct encode_device_t { virtual ~encode_device_t() = default; - virtual int - convert(platf::img_t &img) = 0; + virtual int convert(platf::img_t &img) = 0; video::sunshine_colorspace_t colorspace; }; @@ -387,21 +409,18 @@ namespace platf { void *data {}; AVFrame *frame {}; - int - convert(platf::img_t &img) override { + int convert(platf::img_t &img) override { return -1; } - virtual void - apply_colorspace() { + virtual void apply_colorspace() { } /** * @brief Set the frame to be encoded. * @note Implementations must take ownership of 'frame'. */ - virtual int - set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) { + virtual int set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) { BOOST_LOG(error) << "Illegal call to hwdevice_t::set_frame(). Did you forget to override it?"; return -1; }; @@ -410,29 +429,25 @@ namespace platf { * @brief Initialize the hwframes context. * @note Implementations may set parameters during initialization of the hwframes context. */ - virtual void - init_hwframes(AVHWFramesContext *frames) {}; + virtual void init_hwframes(AVHWFramesContext *frames) {}; /** * @brief Provides a hook for allow platform-specific code to adjust codec options. * @note Implementations may set or modify codec options prior to codec initialization. */ - virtual void - init_codec_options(AVCodecContext *ctx, AVDictionary **options) {}; + virtual void init_codec_options(AVCodecContext *ctx, AVDictionary **options) {}; /** * @brief Prepare to derive a context. * @note Implementations may make modifications required before context derivation */ - virtual int - prepare_to_derive_context(int hw_device_type) { + virtual int prepare_to_derive_context(int hw_device_type) { return 0; }; }; struct nvenc_encode_device_t: encode_device_t { - virtual bool - init_encoder(const video::config_t &client_config, const video::sunshine_colorspace_t &colorspace) = 0; + virtual bool init_encoder(const video::config_t &client_config, const video::sunshine_colorspace_t &colorspace) = 0; nvenc::nvenc_base *nvenc = nullptr; }; @@ -466,7 +481,9 @@ namespace platf { using pull_free_image_cb_t = std::function &img_out)>; display_t() noexcept: - offset_x { 0 }, offset_y { 0 } {} + offset_x {0}, + offset_y {0} { + } /** * @brief Capture a frame. @@ -480,32 +497,25 @@ namespace platf { * @retval capture_e::error On error * @retval capture_e::reinit When need of reinitialization */ - virtual capture_e - capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) = 0; + virtual capture_e capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) = 0; - virtual std::shared_ptr - alloc_img() = 0; + virtual std::shared_ptr alloc_img() = 0; - virtual int - dummy_img(img_t *img) = 0; + virtual int dummy_img(img_t *img) = 0; - virtual std::unique_ptr - make_avcodec_encode_device(pix_fmt_e pix_fmt) { + virtual std::unique_ptr make_avcodec_encode_device(pix_fmt_e pix_fmt) { return nullptr; } - virtual std::unique_ptr - make_nvenc_encode_device(pix_fmt_e pix_fmt) { + virtual std::unique_ptr make_nvenc_encode_device(pix_fmt_e pix_fmt) { return nullptr; } - virtual bool - is_hdr() { + virtual bool is_hdr() { return false; } - virtual bool - get_hdr_metadata(SS_HDR_METADATA &metadata) { + virtual bool get_hdr_metadata(SS_HDR_METADATA &metadata) { std::memset(&metadata, 0, sizeof(metadata)); return false; } @@ -516,8 +526,7 @@ namespace platf { * @param config The codec configuration. * @return `true` if supported, `false` otherwise. */ - virtual bool - is_codec_supported(std::string_view name, const ::video::config_t &config) { + virtual bool is_codec_supported(std::string_view name, const ::video::config_t &config) { return true; } @@ -531,49 +540,46 @@ namespace platf { protected: // collect capture timing data (at loglevel debug) - logging::time_delta_periodic_logger sleep_overshoot_logger = { debug, "Frame capture sleep overshoot" }; + logging::time_delta_periodic_logger sleep_overshoot_logger = {debug, "Frame capture sleep overshoot"}; }; class mic_t { public: - virtual capture_e - sample(std::vector &frame_buffer) = 0; + virtual capture_e sample(std::vector &frame_buffer) = 0; virtual ~mic_t() = default; }; class audio_control_t { public: - virtual int - set_sink(const std::string &sink) = 0; + 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) = 0; + + /** + * @brief Check if the audio sink is available in the system. + * @param sink Sink to be checked. + * @returns True if available, false otherwise. + */ + virtual bool is_sink_available(const std::string &sink) = 0; - virtual std::optional - sink_info() = 0; + virtual std::optional sink_info() = 0; virtual ~audio_control_t() = default; }; - void - freeInput(void *); + void freeInput(void *); using input_t = util::safe_ptr; - std::filesystem::path - appdata(); + std::filesystem::path appdata(); - std::string - get_mac_address(const std::string_view &address); + std::string get_mac_address(const std::string_view &address); - std::string - from_sockaddr(const sockaddr *const); - std::pair - from_sockaddr_ex(const sockaddr *const); + std::string from_sockaddr(const sockaddr *const); + std::pair from_sockaddr_ex(const sockaddr *const); - std::unique_ptr - audio_control(); + std::unique_ptr audio_control(); /** * @brief Get the display_t instance for the given hwdevice_type. @@ -583,22 +589,18 @@ namespace platf { * @param config Stream configuration * @return The display_t instance based on hwdevice_type. */ - std::shared_ptr - display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config); + std::shared_ptr display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config); // A list of names of displays accepted as display_name with the mem_type_e - std::vector - display_names(mem_type_e hwdevice_type); + std::vector display_names(mem_type_e hwdevice_type); /** * @brief Check if GPUs/drivers have changed since the last call to this function. * @return `true` if a change has occurred or if it is unknown whether a change occurred. */ - bool - needs_encoder_reenumeration(); + bool needs_encoder_reenumeration(); - boost::process::v1::child - run_command(bool elevated, bool interactive, const std::string &cmd, boost::filesystem::path &working_dir, const boost::process::v1::environment &env, FILE *file, std::error_code &ec, boost::process::v1::group *group); + boost::process::v1::child run_command(bool elevated, bool interactive, const std::string &cmd, boost::filesystem::path &working_dir, const boost::process::v1::environment &env, FILE *file, std::error_code &ec, boost::process::v1::group *group); enum class thread_priority_e : int { low, ///< Low priority @@ -606,17 +608,13 @@ namespace platf { high, ///< High priority critical ///< Critical priority }; - void - adjust_thread_priority(thread_priority_e priority); + void adjust_thread_priority(thread_priority_e priority); // Allow OS-specific actions to be taken to prepare for streaming - void - streaming_will_start(); - void - streaming_will_stop(); + void streaming_will_start(); + void streaming_will_stop(); - void - restart(); + void restart(); /** * @brief Set an environment variable. @@ -624,16 +622,14 @@ namespace platf { * @param value The value to set the environment variable to. * @return 0 on success, non-zero on failure. */ - int - set_env(const std::string &name, const std::string &value); + int set_env(const std::string &name, const std::string &value); /** * @brief Unset an environment variable. * @param name The name of the environment variable. * @return 0 on success, non-zero on failure. */ - int - unset_env(const std::string &name); + int unset_env(const std::string &name); struct buffer_descriptor_t { const char *buffer; @@ -641,9 +637,14 @@ namespace platf { // Constructors required for emplace_back() prior to C++20 buffer_descriptor_t(const char *buffer, size_t size): - buffer(buffer), size(size) {} + buffer(buffer), + size(size) { + } + buffer_descriptor_t(): - buffer(nullptr), size(0) {} + buffer(nullptr), + size(0) { + } }; struct batched_send_info_t { @@ -674,24 +675,22 @@ namespace platf { * @param offset The offset in the total payload data (bytes). * @return Buffer descriptor describing the region at the given offset. */ - buffer_descriptor_t - buffer_for_payload_offset(ptrdiff_t offset) { + buffer_descriptor_t buffer_for_payload_offset(ptrdiff_t offset) { for (const auto &desc : payload_buffers) { if (offset < desc.size) { return { desc.buffer + offset, desc.size - offset, }; - } - else { + } else { offset -= desc.size; } } return {}; } }; - bool - send_batch(batched_send_info_t &send_info); + + bool send_batch(batched_send_info_t &send_info); struct send_info_t { const char *header; @@ -704,8 +703,8 @@ namespace platf { uint16_t target_port; boost::asio::ip::address &source_address; }; - bool - send(send_info_t &send_info); + + bool send(send_info_t &send_info); enum class qos_data_type_e : int { audio, ///< Audio @@ -720,34 +719,29 @@ namespace platf { * @param data_type The type of traffic sent on this socket. * @param dscp_tagging Specifies whether to enable DSCP tagging on outgoing traffic. */ - std::unique_ptr - enable_socket_qos(uintptr_t native_socket, boost::asio::ip::address &address, uint16_t port, qos_data_type_e data_type, bool dscp_tagging); + std::unique_ptr enable_socket_qos(uintptr_t native_socket, boost::asio::ip::address &address, uint16_t port, qos_data_type_e data_type, bool dscp_tagging); /** * @brief Open a url in the default web browser. * @param url The url to open. */ - void - open_url(const std::string &url); + void open_url(const std::string &url); /** * @brief Attempt to gracefully terminate a process group. * @param native_handle The native handle of the process group. * @return `true` if termination was successfully requested. */ - bool - request_process_group_exit(std::uintptr_t native_handle); + bool request_process_group_exit(std::uintptr_t native_handle); /** * @brief Check if a process group still has running children. * @param native_handle The native handle of the process group. * @return `true` if processes are still running. */ - bool - process_group_running(std::uintptr_t native_handle); + bool process_group_running(std::uintptr_t native_handle); - input_t - input(); + input_t input(); /** * @brief Get the current mouse position on screen * @param input The input_t instance to use. @@ -756,24 +750,15 @@ namespace platf { * auto [x, y] = get_mouse_loc(input); * @examples_end */ - util::point_t - get_mouse_loc(input_t &input); - void - move_mouse(input_t &input, int deltaX, int deltaY); - void - abs_mouse(input_t &input, const touch_port_t &touch_port, float x, float y); - void - button_mouse(input_t &input, int button, bool release); - void - scroll(input_t &input, int distance); - void - hscroll(input_t &input, int distance); - void - keyboard_update(input_t &input, uint16_t modcode, bool release, uint8_t flags); - void - gamepad_update(input_t &input, int nr, const gamepad_state_t &gamepad_state); - void - unicode(input_t &input, char *utf8, int size); + util::point_t get_mouse_loc(input_t &input); + void move_mouse(input_t &input, int deltaX, int deltaY); + void abs_mouse(input_t &input, const touch_port_t &touch_port, float x, float y); + void button_mouse(input_t &input, int button, bool release); + void scroll(input_t &input, int distance); + void hscroll(input_t &input, int distance); + void keyboard_update(input_t &input, uint16_t modcode, bool release, uint8_t flags); + void gamepad_update(input_t &input, int nr, const gamepad_state_t &gamepad_state); + void unicode(input_t &input, char *utf8, int size); typedef deinit_t client_input_t; @@ -782,8 +767,7 @@ namespace platf { * @param input The global input context. * @return A unique pointer to a per-client input data context. */ - std::unique_ptr - allocate_client_input_context(input_t &input); + std::unique_ptr allocate_client_input_context(input_t &input); /** * @brief Send a touch event to the OS. @@ -791,8 +775,7 @@ namespace platf { * @param touch_port The current viewport for translating to screen coordinates. * @param touch The touch event. */ - void - touch_update(client_input_t *input, const touch_port_t &touch_port, const touch_input_t &touch); + void touch_update(client_input_t *input, const touch_port_t &touch_port, const touch_input_t &touch); /** * @brief Send a pen event to the OS. @@ -800,32 +783,28 @@ namespace platf { * @param touch_port The current viewport for translating to screen coordinates. * @param pen The pen event. */ - void - pen_update(client_input_t *input, const touch_port_t &touch_port, const pen_input_t &pen); + void pen_update(client_input_t *input, const touch_port_t &touch_port, const pen_input_t &pen); /** * @brief Send a gamepad touch event to the OS. * @param input The global input context. * @param touch The touch event. */ - void - gamepad_touch(input_t &input, const gamepad_touch_t &touch); + void gamepad_touch(input_t &input, const gamepad_touch_t &touch); /** * @brief Send a gamepad motion event to the OS. * @param input The global input context. * @param motion The motion event. */ - void - gamepad_motion(input_t &input, const gamepad_motion_t &motion); + void gamepad_motion(input_t &input, const gamepad_motion_t &motion); /** * @brief Send a gamepad battery event to the OS. * @param input The global input context. * @param battery The battery event. */ - void - gamepad_battery(input_t &input, const gamepad_battery_t &battery); + void gamepad_battery(input_t &input, const gamepad_battery_t &battery); /** * @brief Create a new virtual gamepad. @@ -835,35 +814,29 @@ namespace platf { * @param feedback_queue The queue for posting messages back to the client. * @return 0 on success. */ - int - alloc_gamepad(input_t &input, const gamepad_id_t &id, const gamepad_arrival_t &metadata, feedback_queue_t feedback_queue); - void - free_gamepad(input_t &input, int nr); + int alloc_gamepad(input_t &input, const gamepad_id_t &id, const gamepad_arrival_t &metadata, feedback_queue_t feedback_queue); + void free_gamepad(input_t &input, int nr); /** * @brief Get the supported platform capabilities to advertise to the client. * @return Capability flags. */ - platform_caps::caps_t - get_capabilities(); + platform_caps::caps_t get_capabilities(); #define SERVICE_NAME "Sunshine" #define SERVICE_TYPE "_nvstream._tcp" namespace publish { - [[nodiscard]] std::unique_ptr - start(); + [[nodiscard]] std::unique_ptr start(); } - [[nodiscard]] std::unique_ptr - init(); + [[nodiscard]] std::unique_ptr init(); /** * @brief Returns the current computer name in UTF-8. * @return Computer name or a placeholder upon failure. */ - std::string - get_host_name(); + std::string get_host_name(); /** * @brief Gets the supported gamepads for this platform backend. @@ -871,8 +844,7 @@ namespace platf { * @param input Pointer to the platform's `input_t` or `nullptr`. * @return Vector of gamepad options and status. */ - std::vector & - supported_gamepads(input_t *input); + std::vector &supported_gamepads(input_t *input); struct high_precision_timer: private boost::noncopyable { virtual ~high_precision_timer() = default; @@ -881,22 +853,19 @@ namespace platf { * @brief Sleep for the duration * @param duration Sleep duration */ - virtual void - sleep_for(const std::chrono::nanoseconds &duration) = 0; + virtual void sleep_for(const std::chrono::nanoseconds &duration) = 0; /** * @brief Check if platform-specific timer backend has been initialized successfully * @return `true` on success, `false` on error */ - virtual - operator bool() = 0; + virtual operator bool() = 0; }; /** * @brief Create platform-specific timer capable of high-precision sleep * @return A unique pointer to timer */ - std::unique_ptr - create_high_precision_timer(); + std::unique_ptr create_high_precision_timer(); } // namespace platf diff --git a/src/platform/linux/audio.cpp b/src/platform/linux/audio.cpp index ff231707e61..0e53e939b2a 100644 --- a/src/platform/linux/audio.cpp +++ b/src/platform/linux/audio.cpp @@ -2,20 +2,21 @@ * @file src/platform/linux/audio.cpp * @brief Definitions for audio control on Linux. */ +// standard includes #include #include #include +// lib includes #include - #include #include #include -#include "src/platform/common.h" - +// local includes #include "src/config.h" #include "src/logging.h" +#include "src/platform/common.h" #include "src/thread_safe.h" namespace platf { @@ -32,8 +33,7 @@ namespace platf { PA_CHANNEL_POSITION_SIDE_RIGHT, }; - std::string - to_string(const char *name, const std::uint8_t *mapping, int channels) { + std::string to_string(const char *name, const std::uint8_t *mapping, int channels) { std::stringstream ss; ss << "rate=48000 sink_name="sv << name << " format=float channels="sv << channels << " channel_map="sv; @@ -53,8 +53,7 @@ namespace platf { struct mic_attr_t: public mic_t { util::safe_ptr mic; - capture_e - sample(std::vector &sample_buf) override { + capture_e sample(std::vector &sample_buf) override { auto sample_size = sample_buf.size(); auto buf = sample_buf.data(); @@ -69,11 +68,10 @@ namespace platf { } }; - std::unique_ptr - microphone(const std::uint8_t *mapping, int channels, std::uint32_t sample_rate, std::uint32_t frame_size, std::string source_name) { + std::unique_ptr microphone(const std::uint8_t *mapping, int channels, std::uint32_t sample_rate, std::uint32_t frame_size, std::string source_name) { auto mic = std::make_unique(); - pa_sample_spec ss { PA_SAMPLE_FLOAT32, sample_rate, (std::uint8_t) channels }; + pa_sample_spec ss {PA_SAMPLE_FLOAT32, sample_rate, (std::uint8_t) channels}; pa_channel_map pa_map; pa_map.channels = channels; @@ -92,9 +90,8 @@ namespace platf { int status; mic->mic.reset( - pa_simple_new(nullptr, "sunshine", - pa_stream_direction_t::PA_STREAM_RECORD, source_name.c_str(), - "sunshine-record", &ss, &pa_map, &pa_attr, &status)); + pa_simple_new(nullptr, "sunshine", pa_stream_direction_t::PA_STREAM_RECORD, source_name.c_str(), "sunshine-record", &ss, &pa_map, &pa_attr, &status) + ); if (!mic->mic) { auto err_str = pa_strerror(status); @@ -106,38 +103,37 @@ namespace platf { } namespace pa { - template + template struct add_const_helper; - template + template struct add_const_helper { using type = const std::remove_pointer_t *; }; - template + template struct add_const_helper { using type = const T *; }; - template + template using add_const_t = typename add_const_helper, T>::type; - template - void - pa_free(T *p) { + template + void pa_free(T *p) { pa_xfree(p); } + using ctx_t = util::safe_ptr; using loop_t = util::safe_ptr; using op_t = util::safe_ptr; using string_t = util::safe_ptr>; - template + template using cb_simple_t = std::function i)>; - template - void - cb(ctx_t::pointer ctx, add_const_t i, void *userdata) { + template + void cb(ctx_t::pointer ctx, add_const_t i, void *userdata) { auto &f = *(cb_simple_t *) userdata; // Cannot similarly filter on eol here. Unless reported otherwise assume @@ -145,12 +141,11 @@ namespace platf { f(ctx, i); } - template + template using cb_t = std::function i, int eol)>; - template - void - cb(ctx_t::pointer ctx, add_const_t i, int eol, void *userdata) { + template + void cb(ctx_t::pointer ctx, add_const_t i, int eol, void *userdata) { auto &f = *(cb_t *) userdata; // For some reason, pulseaudio calls this callback after disconnecting @@ -161,22 +156,19 @@ namespace platf { f(ctx, i, eol); } - void - cb_i(ctx_t::pointer ctx, std::uint32_t i, void *userdata) { + void cb_i(ctx_t::pointer ctx, std::uint32_t i, void *userdata) { auto alarm = (safe::alarm_raw_t *) userdata; alarm->ring(i); } - void - ctx_state_cb(ctx_t::pointer ctx, void *userdata) { + void ctx_state_cb(ctx_t::pointer ctx, void *userdata) { auto &f = *(std::function *) userdata; f(ctx); } - void - success_cb(ctx_t::pointer ctx, int status, void *userdata) { + void success_cb(ctx_t::pointer ctx, int status, void *userdata) { assert(userdata != nullptr); auto alarm = (safe::alarm_raw_t *) userdata; @@ -205,8 +197,8 @@ namespace platf { std::unique_ptr> events_cb; std::thread worker; - int - init() { + + int init() { events = std::make_unique>(); loop.reset(pa_mainloop_new()); ctx.reset(pa_context_new(pa_mainloop_get_api(loop.get()), "sunshine")); @@ -262,8 +254,7 @@ namespace platf { return 0; } - int - load_null(const char *name, const std::uint8_t *channel_mapping, int channels) { + int load_null(const char *name, const std::uint8_t *channel_mapping, int channels) { auto alarm = safe::make_alarm(); op_t op { @@ -272,15 +263,15 @@ namespace platf { "module-null-sink", to_string(name, channel_mapping, channels).c_str(), cb_i, - alarm.get()), + alarm.get() + ), }; alarm->wait(); return *alarm->status(); } - int - unload_null(std::uint32_t i) { + int unload_null(std::uint32_t i) { if (i == PA_INVALID_INDEX) { return 0; } @@ -301,8 +292,7 @@ namespace platf { return 0; } - std::optional - sink_info() override { + std::optional sink_info() override { constexpr auto stereo = "sink-sunshine-stereo"; constexpr auto surround51 = "sink-sunshine-surround51"; constexpr auto surround71 = "sink-sunshine-surround71"; @@ -331,20 +321,18 @@ namespace platf { index.stereo = sink_info->owner_module; ++nullcount; - } - else if (!std::strcmp(sink_info->name, surround51)) { + } else if (!std::strcmp(sink_info->name, surround51)) { index.surround51 = sink_info->owner_module; ++nullcount; - } - else if (!std::strcmp(sink_info->name, surround71)) { + } else if (!std::strcmp(sink_info->name, surround71)) { index.surround71 = sink_info->owner_module; ++nullcount; } }; - op_t op { pa_context_get_sink_info_list(ctx.get(), cb, &f) }; + op_t op {pa_context_get_sink_info_list(ctx.get(), cb, &f)}; if (!op) { BOOST_LOG(error) << "Couldn't create card info operation: "sv << pa_strerror(pa_context_errno(ctx.get())); @@ -365,8 +353,7 @@ namespace platf { index.stereo = load_null(stereo, speaker::map_stereo, sizeof(speaker::map_stereo)); if (index.stereo == PA_INVALID_INDEX) { BOOST_LOG(warning) << "Couldn't create virtual sink for stereo: "sv << pa_strerror(pa_context_errno(ctx.get())); - } - else { + } else { ++nullcount; } } @@ -375,8 +362,7 @@ namespace platf { index.surround51 = load_null(surround51, speaker::map_surround51, sizeof(speaker::map_surround51)); if (index.surround51 == PA_INVALID_INDEX) { BOOST_LOG(warning) << "Couldn't create virtual sink for surround-51: "sv << pa_strerror(pa_context_errno(ctx.get())); - } - else { + } else { ++nullcount; } } @@ -385,8 +371,7 @@ namespace platf { index.surround71 = load_null(surround71, speaker::map_surround71, sizeof(speaker::map_surround71)); if (index.surround71 == PA_INVALID_INDEX) { BOOST_LOG(warning) << "Couldn't create virtual sink for surround-71: "sv << pa_strerror(pa_context_errno(ctx.get())); - } - else { + } else { ++nullcount; } } @@ -396,14 +381,13 @@ namespace platf { } if (nullcount == 3) { - sink.null = std::make_optional(sink_t::null_t { stereo, surround51, surround71 }); + sink.null = std::make_optional(sink_t::null_t {stereo, surround51, surround71}); } return std::make_optional(std::move(sink)); } - std::string - get_default_sink_name() { + std::string get_default_sink_name() { std::string sink_name; auto alarm = safe::make_alarm(); @@ -419,14 +403,13 @@ namespace platf { alarm->ring(0); }; - op_t server_op { pa_context_get_server_info(ctx.get(), cb, &server_f) }; + op_t server_op {pa_context_get_server_info(ctx.get(), cb, &server_f)}; alarm->wait(); // No need to check status. If it failed just return default name. return sink_name; } - std::string - get_monitor_name(const std::string &sink_name) { + std::string get_monitor_name(const std::string &sink_name) { std::string monitor_name; auto alarm = safe::make_alarm(); @@ -449,7 +432,7 @@ namespace platf { monitor_name = sink_info->monitor_source_name; }; - op_t sink_op { pa_context_get_sink_info_by_name(ctx.get(), sink_name.c_str(), cb, &sink_f) }; + op_t sink_op {pa_context_get_sink_info_by_name(ctx.get(), sink_name.c_str(), cb, &sink_f)}; alarm->wait(); // No need to check status. If it failed just return default name. @@ -457,8 +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) override { // Sink choice priority: // 1. Config sink // 2. Last sink swapped to (Usually virtual in this case) @@ -467,20 +449,32 @@ namespace platf { // but this happens right after the swap so the default returned by PA was not // the new one just set! auto sink_name = config::audio.sink; - if (sink_name.empty()) sink_name = requested_sink; - if (sink_name.empty()) sink_name = get_default_sink_name(); + if (sink_name.empty()) { + sink_name = requested_sink; + } + if (sink_name.empty()) { + sink_name = get_default_sink_name(); + } return ::platf::microphone(mapping, channels, sample_rate, frame_size, get_monitor_name(sink_name)); } - int - set_sink(const std::string &sink) override { + bool is_sink_available(const std::string &sink) override { + BOOST_LOG(warning) << "audio_control_t::is_sink_available() unimplemented: "sv << sink; + return true; + } + + int set_sink(const std::string &sink) override { auto alarm = safe::make_alarm(); BOOST_LOG(info) << "Setting default sink to: ["sv << sink << "]"sv; op_t op { pa_context_set_default_sink( - ctx.get(), sink.c_str(), success_cb, alarm.get()), + ctx.get(), + sink.c_str(), + success_cb, + alarm.get() + ), }; if (!op) { @@ -519,8 +513,7 @@ namespace platf { }; } // namespace pa - std::unique_ptr - audio_control() { + std::unique_ptr audio_control() { auto audio = std::make_unique(); if (audio->init()) { @@ -529,4 +522,4 @@ namespace platf { return audio; } -} // namespace platf \ No newline at end of file +} // namespace platf diff --git a/src/platform/linux/cuda.cpp b/src/platform/linux/cuda.cpp index 5498d9a81d5..3a17b9cf1fa 100644 --- a/src/platform/linux/cuda.cpp +++ b/src/platform/linux/cuda.cpp @@ -2,13 +2,15 @@ * @file src/platform/linux/cuda.cpp * @brief Definitions for CUDA encoding. */ +// standard includes #include #include #include #include -#include +// lib includes #include +#include extern "C" { #include @@ -16,6 +18,7 @@ extern "C" { #include } +// local includes #include "cuda.h" #include "graphics.h" #include "src/logging.h" @@ -27,7 +30,8 @@ extern "C" { #define SUNSHINE_STRINGVIEW(x) SUNSHINE_STRINGVIEW_HELPER(x) #define CU_CHECK(x, y) \ - if (check((x), SUNSHINE_STRINGVIEW(y ": "))) return -1 + if (check((x), SUNSHINE_STRINGVIEW(y ": "))) \ + return -1 #define CU_CHECK_IGNORE(x, y) \ check((x), SUNSHINE_STRINGVIEW(y ": ")) @@ -35,17 +39,16 @@ extern "C" { namespace fs = std::filesystem; using namespace std::literals; + namespace cuda { constexpr auto cudaDevAttrMaxThreadsPerBlock = (CUdevice_attribute) 1; constexpr auto cudaDevAttrMaxThreadsPerMultiProcessor = (CUdevice_attribute) 39; - void - pass_error(const std::string_view &sv, const char *name, const char *description) { + void pass_error(const std::string_view &sv, const char *name, const char *description) { BOOST_LOG(error) << sv << name << ':' << description; } - void - cff(CudaFunctions *cf) { + void cff(CudaFunctions *cf) { cuda_free_functions(&cf); } @@ -53,8 +56,7 @@ namespace cuda { static cdf_t cdf; - inline static int - check(CUresult result, const std::string_view &sv) { + inline static int check(CUresult result, const std::string_view &sv) { if (result != CUDA_SUCCESS) { const char *name; const char *description; @@ -69,13 +71,11 @@ namespace cuda { return 0; } - void - freeStream(CUstream stream) { + void freeStream(CUstream stream) { CU_CHECK_IGNORE(cdf->cuStreamDestroy(stream), "Couldn't destroy cuda stream"); } - void - unregisterResource(CUgraphicsResource resource) { + void unregisterResource(CUgraphicsResource resource) { CU_CHECK_IGNORE(cdf->cuGraphicsUnregisterResource(resource), "Couldn't unregister resource"); } @@ -86,8 +86,7 @@ namespace cuda { tex_t tex; }; - int - init() { + int init() { auto status = cuda_load_functions(&cdf, nullptr); if (status) { BOOST_LOG(error) << "Couldn't load cuda: "sv << status; @@ -102,8 +101,7 @@ namespace cuda { class cuda_t: public platf::avcodec_encode_device_t { public: - int - init(int in_width, int in_height) { + int init(int in_width, int in_height) { if (!cdf) { BOOST_LOG(warning) << "cuda not initialized"sv; return -1; @@ -117,8 +115,7 @@ namespace cuda { return 0; } - int - set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) override { + int set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) override { this->hwframe.reset(frame); this->frame = frame; @@ -156,8 +153,7 @@ namespace cuda { return 0; } - void - apply_colorspace() override { + void apply_colorspace() override { sws.apply_colorspace(colorspace); auto tex = tex_t::make(height, width * 4); @@ -182,11 +178,10 @@ namespace cuda { return; } - sws.convert(frame->data[0], frame->data[1], frame->linesize[0], frame->linesize[1], tex->texture.linear, stream.get(), { frame->width, frame->height, 0, 0 }); + sws.convert(frame->data[0], frame->data[1], frame->linesize[0], frame->linesize[1], tex->texture.linear, stream.get(), {frame->width, frame->height, 0, 0}); } - cudaTextureObject_t - tex_obj(const tex_t &tex) const { + cudaTextureObject_t tex_obj(const tex_t &tex) const { return linear_interpolation ? tex.texture.linear : tex.texture.point; } @@ -203,13 +198,11 @@ namespace cuda { class cuda_ram_t: public cuda_t { public: - int - convert(platf::img_t &img) override { + int convert(platf::img_t &img) override { 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) { if (cuda_t::set_frame(frame, hw_frames_ctx)) { return -1; } @@ -229,8 +222,7 @@ namespace cuda { class cuda_vram_t: public cuda_t { public: - int - convert(platf::img_t &img) override { + int convert(platf::img_t &img) override { return sws.convert(frame->data[0], frame->data[1], frame->linesize[0], frame->linesize[1], tex_obj(((img_t *) &img)->tex), stream.get()); } }; @@ -240,8 +232,7 @@ namespace cuda { * @param index CUDA device index to open. * @return File descriptor or -1 on failure. */ - file_t - open_drm_fd_for_cuda_device(int index) { + file_t open_drm_fd_for_cuda_device(int index) { CUdevice device; CU_CHECK(cdf->cuDeviceGet(&device, index), "Couldn't get CUDA device"); @@ -252,29 +243,29 @@ namespace cuda { BOOST_LOG(debug) << "Found CUDA device with PCI bus ID: "sv << pci_bus_id.data(); // Linux uses lowercase hexadecimal while CUDA uses uppercase - std::transform(pci_bus_id.begin(), pci_bus_id.end(), pci_bus_id.begin(), - [](char c) { return std::tolower(c); }); + std::transform(pci_bus_id.begin(), pci_bus_id.end(), pci_bus_id.begin(), [](char c) { + return std::tolower(c); + }); // Look for the name of the primary node in sysfs try { char sysfs_path[PATH_MAX]; std::snprintf(sysfs_path, sizeof(sysfs_path), "/sys/bus/pci/devices/%s/drm", pci_bus_id.data()); - fs::path sysfs_dir { sysfs_path }; - for (auto &entry : fs::directory_iterator { sysfs_dir }) { + fs::path sysfs_dir {sysfs_path}; + for (auto &entry : fs::directory_iterator {sysfs_dir}) { auto file = entry.path().filename(); auto filestring = file.generic_string(); - if (std::string_view { filestring }.substr(0, 4) != "card"sv) { + if (std::string_view {filestring}.substr(0, 4) != "card"sv) { continue; } BOOST_LOG(debug) << "Found DRM primary node: "sv << filestring; - fs::path dri_path { "/dev/dri"sv }; + fs::path dri_path {"/dev/dri"sv}; auto device_path = dri_path / file; return open(device_path.c_str(), O_RDWR); } - } - catch (const std::filesystem::filesystem_error &err) { + } catch (const std::filesystem::filesystem_error &err) { BOOST_LOG(error) << "Failed to read sysfs: "sv << err.what(); } @@ -292,8 +283,7 @@ namespace cuda { * @param offset_y Offset of content in captured frame. * @return 0 on success or -1 on failure. */ - int - init(int in_width, int in_height, int offset_x, int offset_y) { + int init(int in_width, int in_height, int offset_x, int offset_y) { // This must be non-zero to tell the video core that it's a hardware encoding device. data = (void *) 0x1; @@ -340,8 +330,7 @@ namespace cuda { * @param hw_frames_ctx_buf FFmpeg hardware frame context. * @return 0 on success or -1 on failure. */ - int - set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx_buf) override { + int set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx_buf) override { this->hwframe.reset(frame); this->frame = frame; @@ -377,10 +366,8 @@ namespace cuda { cuda_ctx->stream = stream.get(); - CU_CHECK(cdf->cuGraphicsGLRegisterImage(&y_res, nv12->tex[0], GL_TEXTURE_2D, CU_GRAPHICS_REGISTER_FLAGS_READ_ONLY), - "Couldn't register Y plane texture"); - CU_CHECK(cdf->cuGraphicsGLRegisterImage(&uv_res, nv12->tex[1], GL_TEXTURE_2D, CU_GRAPHICS_REGISTER_FLAGS_READ_ONLY), - "Couldn't register UV plane texture"); + CU_CHECK(cdf->cuGraphicsGLRegisterImage(&y_res, nv12->tex[0], GL_TEXTURE_2D, CU_GRAPHICS_REGISTER_FLAGS_READ_ONLY), "Couldn't register Y plane texture"); + CU_CHECK(cdf->cuGraphicsGLRegisterImage(&uv_res, nv12->tex[1], GL_TEXTURE_2D, CU_GRAPHICS_REGISTER_FLAGS_READ_ONLY), "Couldn't register UV plane texture"); return 0; } @@ -390,15 +377,13 @@ namespace cuda { * @param img Captured screen image. * @return 0 on success or -1 on failure. */ - int - convert(platf::img_t &img) override { + int convert(platf::img_t &img) override { auto &descriptor = (egl::img_descriptor_t &) img; if (descriptor.sequence == 0) { // For dummy images, use a blank RGB texture instead of importing a DMA-BUF rgb = egl::create_blank(img); - } - else if (descriptor.sequence > sequence) { + } else if (descriptor.sequence > sequence) { sequence = descriptor.sequence; rgb = egl::rgb_t {}; @@ -419,7 +404,7 @@ namespace cuda { auto fmt_desc = av_pix_fmt_desc_get(sw_format); // Map the GL textures to read for CUDA - CUgraphicsResource resources[2] = { y_res.get(), uv_res.get() }; + CUgraphicsResource resources[2] = {y_res.get(), uv_res.get()}; CU_CHECK(cdf->cuGraphicsMapResources(2, resources, stream.get()), "Couldn't map GL textures in CUDA"); // Copy from the GL textures to the target CUDA frame @@ -445,8 +430,7 @@ namespace cuda { /** * @brief Configures shader parameters for the specified colorspace. */ - void - apply_colorspace() override { + void apply_colorspace() override { sws.apply_colorspace(colorspace); } @@ -474,8 +458,7 @@ namespace cuda { int offset_x, offset_y; }; - std::unique_ptr - make_avcodec_encode_device(int width, int height, bool vram) { + std::unique_ptr make_avcodec_encode_device(int width, int height, bool vram) { if (init()) { return nullptr; } @@ -484,8 +467,7 @@ namespace cuda { if (vram) { cuda = std::make_unique(); - } - else { + } else { cuda = std::make_unique(); } @@ -504,8 +486,7 @@ namespace cuda { * @param offset_y Offset of content in captured frame. * @return FFmpeg encoding device context. */ - std::unique_ptr - make_avcodec_gl_encode_device(int width, int height, int offset_x, int offset_y) { + std::unique_ptr make_avcodec_gl_encode_device(int width, int height, int offset_x, int offset_y) { if (init()) { return nullptr; } @@ -521,29 +502,30 @@ namespace cuda { namespace nvfbc { static PNVFBCCREATEINSTANCE createInstance {}; - static NVFBC_API_FUNCTION_LIST func { NVFBC_VERSION }; + static NVFBC_API_FUNCTION_LIST func {NVFBC_VERSION}; - static constexpr inline NVFBC_BOOL - nv_bool(bool b) { + static constexpr inline NVFBC_BOOL nv_bool(bool b) { return b ? NVFBC_TRUE : NVFBC_FALSE; } - static void *handle { nullptr }; - int - init() { + static void *handle {nullptr}; + + int init() { static bool funcs_loaded = false; - if (funcs_loaded) return 0; + if (funcs_loaded) { + return 0; + } if (!handle) { - handle = dyn::handle({ "libnvidia-fbc.so.1", "libnvidia-fbc.so" }); + handle = dyn::handle({"libnvidia-fbc.so.1", "libnvidia-fbc.so"}); if (!handle) { return -1; } } std::vector> funcs { - { (dyn::apiproc *) &createInstance, "NvFBCCreateInstance" }, + {(dyn::apiproc *) &createInstance, "NvFBCCreateInstance"}, }; if (dyn::load(handle, funcs)) { @@ -569,7 +551,7 @@ namespace cuda { class ctx_t { public: ctx_t(NVFBC_SESSION_HANDLE handle) { - NVFBC_BIND_CONTEXT_PARAMS params { NVFBC_BIND_CONTEXT_PARAMS_VER }; + NVFBC_BIND_CONTEXT_PARAMS params {NVFBC_BIND_CONTEXT_PARAMS_VER}; if (func.nvFBCBindContext(handle, ¶ms)) { BOOST_LOG(error) << "Couldn't bind NvFBC context to current thread: " << func.nvFBCGetLastErrorStr(handle); @@ -579,7 +561,7 @@ namespace cuda { } ~ctx_t() { - NVFBC_RELEASE_CONTEXT_PARAMS params { NVFBC_RELEASE_CONTEXT_PARAMS_VER }; + NVFBC_RELEASE_CONTEXT_PARAMS params {NVFBC_RELEASE_CONTEXT_PARAMS_VER}; if (func.nvFBCReleaseContext(handle, ¶ms)) { BOOST_LOG(error) << "Couldn't release NvFBC context from current thread: " << func.nvFBCGetLastErrorStr(handle); } @@ -597,26 +579,26 @@ namespace cuda { public: handle_t() = default; + handle_t(handle_t &&other): - handle_flags { other.handle_flags }, handle { other.handle } { + handle_flags {other.handle_flags}, + handle {other.handle} { other.handle_flags.reset(); } - handle_t & - operator=(handle_t &&other) { + handle_t &operator=(handle_t &&other) { std::swap(handle_flags, other.handle_flags); std::swap(handle, other.handle); return *this; } - static std::optional - make() { - NVFBC_CREATE_HANDLE_PARAMS params { NVFBC_CREATE_HANDLE_PARAMS_VER }; + static std::optional make() { + NVFBC_CREATE_HANDLE_PARAMS params {NVFBC_CREATE_HANDLE_PARAMS_VER}; // Set privateData to allow NvFBC on consumer NVIDIA GPUs. // Based on https://github.com/keylase/nvidia-patch/blob/3193b4b1cea91527bf09ea9b8db5aade6a3f3c0a/win/nvfbcwrp/nvfbcwrp_main.cpp#L23-L25 . - const unsigned int MAGIC_PRIVATE_DATA[4] = { 0xAEF57AC5, 0x401D1A39, 0x1B856BBE, 0x9ED0CEBA }; + const unsigned int MAGIC_PRIVATE_DATA[4] = {0xAEF57AC5, 0x401D1A39, 0x1B856BBE, 0x9ED0CEBA}; params.privateData = MAGIC_PRIVATE_DATA; params.privateDataSize = sizeof(MAGIC_PRIVATE_DATA); @@ -633,14 +615,12 @@ namespace cuda { return handle; } - const char * - last_error() { + const char *last_error() { return func.nvFBCGetLastErrorStr(handle); } - std::optional - status() { - NVFBC_GET_STATUS_PARAMS params { NVFBC_GET_STATUS_PARAMS_VER }; + std::optional status() { + NVFBC_GET_STATUS_PARAMS params {NVFBC_GET_STATUS_PARAMS_VER}; auto status = func.nvFBCGetStatus(handle, ¶ms); if (status) { @@ -652,8 +632,7 @@ namespace cuda { return params; } - int - capture(NVFBC_CREATE_CAPTURE_SESSION_PARAMS &capture_params) { + int capture(NVFBC_CREATE_CAPTURE_SESSION_PARAMS &capture_params) { if (func.nvFBCCreateCaptureSession(handle, &capture_params)) { BOOST_LOG(error) << "Failed to start capture session: "sv << last_error(); return -1; @@ -673,13 +652,12 @@ namespace cuda { return 0; } - int - stop() { + int stop() { if (!handle_flags[SESSION_CAPTURE]) { return 0; } - NVFBC_DESTROY_CAPTURE_SESSION_PARAMS params { NVFBC_DESTROY_CAPTURE_SESSION_PARAMS_VER }; + NVFBC_DESTROY_CAPTURE_SESSION_PARAMS params {NVFBC_DESTROY_CAPTURE_SESSION_PARAMS_VER}; if (func.nvFBCDestroyCaptureSession(handle, ¶ms)) { BOOST_LOG(error) << "Couldn't destroy capture session: "sv << last_error(); @@ -692,17 +670,16 @@ namespace cuda { return 0; } - int - reset() { + int reset() { if (!handle_flags[SESSION_HANDLE]) { return 0; } stop(); - NVFBC_DESTROY_HANDLE_PARAMS params { NVFBC_DESTROY_HANDLE_PARAMS_VER }; + NVFBC_DESTROY_HANDLE_PARAMS params {NVFBC_DESTROY_HANDLE_PARAMS_VER}; - ctx_t ctx { handle }; + ctx_t ctx {handle}; if (func.nvFBCDestroyHandle(handle, ¶ms)) { BOOST_LOG(error) << "Couldn't destroy session handle: "sv << func.nvFBCGetLastErrorStr(handle); } @@ -723,14 +700,13 @@ namespace cuda { class display_t: public platf::display_t { public: - int - init(const std::string_view &display_name, const ::video::config_t &config) { + int init(const std::string_view &display_name, const ::video::config_t &config) { auto handle = handle_t::make(); if (!handle) { return -1; } - ctx_t ctx { handle->handle }; + ctx_t ctx {handle->handle}; auto status_params = handle->status(); if (!status_params) { @@ -744,19 +720,17 @@ namespace cuda { if (monitor_nr < 0 || monitor_nr >= status_params->dwOutputNum) { BOOST_LOG(warning) << "Can't stream monitor ["sv << monitor_nr << "], it needs to be between [0] and ["sv << status_params->dwOutputNum - 1 << "], defaulting to virtual desktop"sv; - } - else { + } else { streamedMonitor = monitor_nr; } - } - else { + } else { BOOST_LOG(warning) << "XrandR not available, streaming entire virtual desktop"sv; } } - delay = std::chrono::nanoseconds { 1s } / config.framerate; + delay = std::chrono::nanoseconds {1s} / config.framerate; - capture_params = NVFBC_CREATE_CAPTURE_SESSION_PARAMS { NVFBC_CREATE_CAPTURE_SESSION_PARAMS_VER }; + capture_params = NVFBC_CREATE_CAPTURE_SESSION_PARAMS {NVFBC_CREATE_CAPTURE_SESSION_PARAMS_VER}; capture_params.eCaptureType = NVFBC_CAPTURE_SHARED_CUDA; capture_params.bDisableAutoModesetRecovery = nv_bool(true); @@ -773,8 +747,7 @@ namespace cuda { capture_params.eTrackingType = NVFBC_TRACKING_OUTPUT; capture_params.dwOutputId = output.dwId; - } - else { + } else { capture_params.eTrackingType = NVFBC_TRACKING_SCREEN; width = status_params->screenSize.w; @@ -788,8 +761,7 @@ namespace cuda { return 0; } - platf::capture_e - capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override { + platf::capture_e capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override { auto next_frame = std::chrono::steady_clock::now(); { @@ -802,7 +774,7 @@ namespace cuda { // Force display_t::capture to initialize handle_t::capture cursor_visible = !*cursor; - ctx_t ctx { handle.handle }; + ctx_t ctx {handle.handle}; auto fg = util::fail_guard([&]() { handle.reset(); }); @@ -849,8 +821,7 @@ namespace cuda { } // Reinitialize the capture session. - platf::capture_e - reinit(bool cursor) { + platf::capture_e reinit(bool cursor) { if (handle.stop()) { return platf::capture_e::error; } @@ -860,8 +831,7 @@ namespace cuda { capture_params.bPushModel = nv_bool(false); capture_params.bWithCursor = nv_bool(true); capture_params.bAllowDirectCapture = nv_bool(false); - } - else { + } else { capture_params.bPushModel = nv_bool(true); capture_params.bWithCursor = nv_bool(false); capture_params.bAllowDirectCapture = nv_bool(true); @@ -919,8 +889,7 @@ namespace cuda { return platf::capture_e::ok; } - platf::capture_e - snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor) { + platf::capture_e snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor) { if (cursor != cursor_visible) { auto status = reinit(cursor); if (status != platf::capture_e::ok) { @@ -960,13 +929,11 @@ 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) { return ::cuda::make_avcodec_encode_device(width, height, true); } - std::shared_ptr - alloc_img() override { + std::shared_ptr alloc_img() override { auto img = std::make_shared(); img->data = nullptr; @@ -985,8 +952,7 @@ namespace cuda { return img; }; - int - dummy_img(platf::img_t *) override { + int dummy_img(platf::img_t *) override { return 0; } @@ -1001,8 +967,7 @@ namespace cuda { } // namespace cuda namespace platf { - std::shared_ptr - nvfbc_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config) { + std::shared_ptr nvfbc_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config) { if (hwdevice_type != mem_type_e::cuda) { BOOST_LOG(error) << "Could not initialize nvfbc display with the given hw device type"sv; return nullptr; @@ -1017,8 +982,7 @@ namespace platf { return display; } - std::vector - nvfbc_display_names() { + std::vector nvfbc_display_names() { if (cuda::init() || cuda::nvfbc::init()) { return {}; } diff --git a/src/platform/linux/cuda.cu b/src/platform/linux/cuda.cu index 7456dd4d06c..6d288ef8d46 100644 --- a/src/platform/linux/cuda.cu +++ b/src/platform/linux/cuda.cu @@ -2,14 +2,17 @@ * @file src/platform/linux/cuda.cu * @brief CUDA implementation for Linux. */ -// #include -#include +// standard includes #include #include #include #include #include +// platform includes +#include + +// local includes #include "cuda.h" using namespace std::literals; @@ -18,16 +21,20 @@ using namespace std::literals; #define SUNSHINE_STRINGVIEW(x) SUNSHINE_STRINGVIEW_HELPER(x) #define CU_CHECK(x, y) \ - if(check((x), SUNSHINE_STRINGVIEW(y ": "))) return -1 + if (check((x), SUNSHINE_STRINGVIEW(y ": "))) \ + return -1 #define CU_CHECK_VOID(x, y) \ - if(check((x), SUNSHINE_STRINGVIEW(y ": "))) return; + if (check((x), SUNSHINE_STRINGVIEW(y ": "))) \ + return; #define CU_CHECK_PTR(x, y) \ - if(check((x), SUNSHINE_STRINGVIEW(y ": "))) return nullptr; + if (check((x), SUNSHINE_STRINGVIEW(y ": "))) \ + return nullptr; #define CU_CHECK_OPT(x, y) \ - if(check((x), SUNSHINE_STRINGVIEW(y ": "))) return std::nullopt; + if (check((x), SUNSHINE_STRINGVIEW(y ": "))) \ + return std::nullopt; #define CU_CHECK_IGNORE(x, y) \ check((x), SUNSHINE_STRINGVIEW(y ": ")) @@ -42,277 +49,293 @@ using namespace std::literals; * Not pretty and extremely error-prone, fix at earliest convenience. */ namespace platf { -struct img_t: std::enable_shared_from_this { -public: - std::uint8_t *data {}; - std::int32_t width {}; - std::int32_t height {}; - std::int32_t pixel_pitch {}; - std::int32_t row_pitch {}; + struct img_t: std::enable_shared_from_this { + public: + std::uint8_t *data {}; + std::int32_t width {}; + std::int32_t height {}; + std::int32_t pixel_pitch {}; + std::int32_t row_pitch {}; - std::optional frame_timestamp; + std::optional frame_timestamp; - virtual ~img_t() = default; -}; -} // namespace platf + virtual ~img_t() = default; + }; +} // namespace platf // End special declarations namespace cuda { -struct alignas(16) cuda_color_t { - float4 color_vec_y; - float4 color_vec_u; - float4 color_vec_v; - float2 range_y; - float2 range_uv; -}; - -static_assert(sizeof(video::color_t) == sizeof(cuda::cuda_color_t), "color matrix struct mismatch"); + struct alignas(16) cuda_color_t { + float4 color_vec_y; + float4 color_vec_u; + float4 color_vec_v; + float2 range_y; + float2 range_uv; + }; -auto constexpr INVALID_TEXTURE = std::numeric_limits::max(); + static_assert(sizeof(video::color_t) == sizeof(cuda::cuda_color_t), "color matrix struct mismatch"); -template -inline T div_align(T l, T r) { - return (l + r - 1) / r; -} + auto constexpr INVALID_TEXTURE = std::numeric_limits::max(); -void pass_error(const std::string_view &sv, const char *name, const char *description); -inline static int check(cudaError_t result, const std::string_view &sv) { - if(result) { - auto name = cudaGetErrorName(result); - auto description = cudaGetErrorString(result); - - pass_error(sv, name, description); - return -1; + template + inline T div_align(T l, T r) { + return (l + r - 1) / r; } - return 0; -} - -template -ptr_t make_ptr() { - void *p; - CU_CHECK_PTR(cudaMalloc(&p, sizeof(T)), "Couldn't allocate color matrix"); + void pass_error(const std::string_view &sv, const char *name, const char *description); - ptr_t ptr { p }; + inline static int check(cudaError_t result, const std::string_view &sv) { + if (result) { + auto name = cudaGetErrorName(result); + auto description = cudaGetErrorString(result); - return ptr; -} + pass_error(sv, name, description); + return -1; + } -void freeCudaPtr_t::operator()(void *ptr) { - CU_CHECK_IGNORE(cudaFree(ptr), "Couldn't free cuda device pointer"); -} + return 0; + } -void freeCudaStream_t::operator()(cudaStream_t ptr) { - CU_CHECK_IGNORE(cudaStreamDestroy(ptr), "Couldn't free cuda stream"); -} + template + ptr_t make_ptr() { + void *p; + CU_CHECK_PTR(cudaMalloc(&p, sizeof(T)), "Couldn't allocate color matrix"); -stream_t make_stream(int flags) { - cudaStream_t stream; + ptr_t ptr {p}; - if(!flags) { - CU_CHECK_PTR(cudaStreamCreate(&stream), "Couldn't create cuda stream"); - } - else { - CU_CHECK_PTR(cudaStreamCreateWithFlags(&stream, flags), "Couldn't create cuda stream with flags"); + return ptr; } - return stream_t { stream }; -} - -inline __device__ float3 bgra_to_rgb(uchar4 vec) { - return make_float3((float)vec.z, (float)vec.y, (float)vec.x); -} - -inline __device__ float3 bgra_to_rgb(float4 vec) { - return make_float3(vec.z, vec.y, vec.x); -} + void freeCudaPtr_t::operator()(void *ptr) { + CU_CHECK_IGNORE(cudaFree(ptr), "Couldn't free cuda device pointer"); + } -inline __device__ float2 calcUV(float3 pixel, const cuda_color_t *const color_matrix) { - float4 vec_u = color_matrix->color_vec_u; - float4 vec_v = color_matrix->color_vec_v; + void freeCudaStream_t::operator()(cudaStream_t ptr) { + CU_CHECK_IGNORE(cudaStreamDestroy(ptr), "Couldn't free cuda stream"); + } - float u = dot(pixel, make_float3(vec_u)) + vec_u.w; - float v = dot(pixel, make_float3(vec_v)) + vec_v.w; + stream_t make_stream(int flags) { + cudaStream_t stream; - u = u * color_matrix->range_uv.x + color_matrix->range_uv.y; - v = v * color_matrix->range_uv.x + color_matrix->range_uv.y; + if (!flags) { + CU_CHECK_PTR(cudaStreamCreate(&stream), "Couldn't create cuda stream"); + } else { + CU_CHECK_PTR(cudaStreamCreateWithFlags(&stream, flags), "Couldn't create cuda stream with flags"); + } - return make_float2(u, v); -} + return stream_t {stream}; + } -inline __device__ float calcY(float3 pixel, const cuda_color_t *const color_matrix) { - float4 vec_y = color_matrix->color_vec_y; + inline __device__ float3 bgra_to_rgb(uchar4 vec) { + return make_float3((float) vec.z, (float) vec.y, (float) vec.x); + } - return (dot(pixel, make_float3(vec_y)) + vec_y.w) * color_matrix->range_y.x + color_matrix->range_y.y; -} + inline __device__ float3 bgra_to_rgb(float4 vec) { + return make_float3(vec.z, vec.y, vec.x); + } -__global__ void RGBA_to_NV12( - cudaTextureObject_t srcImage, std::uint8_t *dstY, std::uint8_t *dstUV, - std::uint32_t dstPitchY, std::uint32_t dstPitchUV, - float scale, const viewport_t viewport, const cuda_color_t *const color_matrix) { + inline __device__ float2 calcUV(float3 pixel, const cuda_color_t *const color_matrix) { + float4 vec_u = color_matrix->color_vec_u; + float4 vec_v = color_matrix->color_vec_v; - int idX = (threadIdx.x + blockDim.x * blockIdx.x) * 2; - int idY = (threadIdx.y + blockDim.y * blockIdx.y) * 2; + float u = dot(pixel, make_float3(vec_u)) + vec_u.w; + float v = dot(pixel, make_float3(vec_v)) + vec_v.w; - if(idX >= viewport.width) return; - if(idY >= viewport.height) return; + u = u * color_matrix->range_uv.x + color_matrix->range_uv.y; + v = v * color_matrix->range_uv.x + color_matrix->range_uv.y; - float x = idX * scale; - float y = idY * scale; + return make_float2(u, v); + } - idX += viewport.offsetX; - idY += viewport.offsetY; + inline __device__ float calcY(float3 pixel, const cuda_color_t *const color_matrix) { + float4 vec_y = color_matrix->color_vec_y; - uint8_t *dstY0 = dstY + idX + idY * dstPitchY; - uint8_t *dstY1 = dstY + idX + (idY + 1) * dstPitchY; - dstUV = dstUV + idX + (idY / 2 * dstPitchUV); + return (dot(pixel, make_float3(vec_y)) + vec_y.w) * color_matrix->range_y.x + color_matrix->range_y.y; + } - float3 rgb_lt = bgra_to_rgb(tex2D(srcImage, x, y)); - float3 rgb_rt = bgra_to_rgb(tex2D(srcImage, x + scale, y)); - float3 rgb_lb = bgra_to_rgb(tex2D(srcImage, x, y + scale)); - float3 rgb_rb = bgra_to_rgb(tex2D(srcImage, x + scale, y + scale)); + __global__ void RGBA_to_NV12( + cudaTextureObject_t srcImage, + std::uint8_t *dstY, + std::uint8_t *dstUV, + std::uint32_t dstPitchY, + std::uint32_t dstPitchUV, + float scale, + const viewport_t viewport, + const cuda_color_t *const color_matrix + ) { + int idX = (threadIdx.x + blockDim.x * blockIdx.x) * 2; + int idY = (threadIdx.y + blockDim.y * blockIdx.y) * 2; + + if (idX >= viewport.width) { + return; + } + if (idY >= viewport.height) { + return; + } + + float x = idX * scale; + float y = idY * scale; + + idX += viewport.offsetX; + idY += viewport.offsetY; + + uint8_t *dstY0 = dstY + idX + idY * dstPitchY; + uint8_t *dstY1 = dstY + idX + (idY + 1) * dstPitchY; + dstUV = dstUV + idX + (idY / 2 * dstPitchUV); + + float3 rgb_lt = bgra_to_rgb(tex2D(srcImage, x, y)); + float3 rgb_rt = bgra_to_rgb(tex2D(srcImage, x + scale, y)); + float3 rgb_lb = bgra_to_rgb(tex2D(srcImage, x, y + scale)); + float3 rgb_rb = bgra_to_rgb(tex2D(srcImage, x + scale, y + scale)); + + float2 uv_lt = calcUV(rgb_lt, color_matrix) * 256.0f; + float2 uv_rt = calcUV(rgb_rt, color_matrix) * 256.0f; + float2 uv_lb = calcUV(rgb_lb, color_matrix) * 256.0f; + float2 uv_rb = calcUV(rgb_rb, color_matrix) * 256.0f; + + float2 uv = (uv_lt + uv_lb + uv_rt + uv_rb) * 0.25f; + + dstUV[0] = uv.x; + dstUV[1] = uv.y; + dstY0[0] = calcY(rgb_lt, color_matrix) * 245.0f; // 245.0f is a magic number to ensure slight changes in luminosity are more visible + dstY0[1] = calcY(rgb_rt, color_matrix) * 245.0f; // 245.0f is a magic number to ensure slight changes in luminosity are more visible + dstY1[0] = calcY(rgb_lb, color_matrix) * 245.0f; // 245.0f is a magic number to ensure slight changes in luminosity are more visible + dstY1[1] = calcY(rgb_rb, color_matrix) * 245.0f; // 245.0f is a magic number to ensure slight changes in luminosity are more visible + } - float2 uv_lt = calcUV(rgb_lt, color_matrix) * 256.0f; - float2 uv_rt = calcUV(rgb_rt, color_matrix) * 256.0f; - float2 uv_lb = calcUV(rgb_lb, color_matrix) * 256.0f; - float2 uv_rb = calcUV(rgb_rb, color_matrix) * 256.0f; + int tex_t::copy(std::uint8_t *src, int height, int pitch) { + CU_CHECK(cudaMemcpy2DToArray(array, 0, 0, src, pitch, pitch, height, cudaMemcpyDeviceToDevice), "Couldn't copy to cuda array from deviceptr"); - float2 uv = (uv_lt + uv_lb + uv_rt + uv_rb) * 0.25f; + return 0; + } - dstUV[0] = uv.x; - dstUV[1] = uv.y; - dstY0[0] = calcY(rgb_lt, color_matrix) * 245.0f; // 245.0f is a magic number to ensure slight changes in luminosity are more visible - dstY0[1] = calcY(rgb_rt, color_matrix) * 245.0f; // 245.0f is a magic number to ensure slight changes in luminosity are more visible - dstY1[0] = calcY(rgb_lb, color_matrix) * 245.0f; // 245.0f is a magic number to ensure slight changes in luminosity are more visible - dstY1[1] = calcY(rgb_rb, color_matrix) * 245.0f; // 245.0f is a magic number to ensure slight changes in luminosity are more visible -} + std::optional tex_t::make(int height, int pitch) { + tex_t tex; -int tex_t::copy(std::uint8_t *src, int height, int pitch) { - CU_CHECK(cudaMemcpy2DToArray(array, 0, 0, src, pitch, pitch, height, cudaMemcpyDeviceToDevice), "Couldn't copy to cuda array from deviceptr"); + auto format = cudaCreateChannelDesc(); + CU_CHECK_OPT(cudaMallocArray(&tex.array, &format, pitch, height, cudaArrayDefault), "Couldn't allocate cuda array"); - return 0; -} + cudaResourceDesc res {}; + res.resType = cudaResourceTypeArray; + res.res.array.array = tex.array; -std::optional tex_t::make(int height, int pitch) { - tex_t tex; + cudaTextureDesc desc {}; - auto format = cudaCreateChannelDesc(); - CU_CHECK_OPT(cudaMallocArray(&tex.array, &format, pitch, height, cudaArrayDefault), "Couldn't allocate cuda array"); + desc.readMode = cudaReadModeNormalizedFloat; + desc.filterMode = cudaFilterModePoint; + desc.normalizedCoords = false; - cudaResourceDesc res {}; - res.resType = cudaResourceTypeArray; - res.res.array.array = tex.array; + std::fill_n(std::begin(desc.addressMode), 2, cudaAddressModeClamp); - cudaTextureDesc desc {}; + CU_CHECK_OPT(cudaCreateTextureObject(&tex.texture.point, &res, &desc, nullptr), "Couldn't create cuda texture that uses point interpolation"); - desc.readMode = cudaReadModeNormalizedFloat; - desc.filterMode = cudaFilterModePoint; - desc.normalizedCoords = false; + desc.filterMode = cudaFilterModeLinear; - std::fill_n(std::begin(desc.addressMode), 2, cudaAddressModeClamp); + CU_CHECK_OPT(cudaCreateTextureObject(&tex.texture.linear, &res, &desc, nullptr), "Couldn't create cuda texture that uses linear interpolation"); - CU_CHECK_OPT(cudaCreateTextureObject(&tex.texture.point, &res, &desc, nullptr), "Couldn't create cuda texture that uses point interpolation"); + return tex; + } - desc.filterMode = cudaFilterModeLinear; + tex_t::tex_t(): + array {}, + texture {INVALID_TEXTURE, INVALID_TEXTURE} { + } - CU_CHECK_OPT(cudaCreateTextureObject(&tex.texture.linear, &res, &desc, nullptr), "Couldn't create cuda texture that uses linear interpolation"); + tex_t::tex_t(tex_t &&other): + array {other.array}, + texture {other.texture} { + other.array = 0; + other.texture.point = INVALID_TEXTURE; + other.texture.linear = INVALID_TEXTURE; + } - return tex; -} + tex_t &tex_t::operator=(tex_t &&other) { + std::swap(array, other.array); + std::swap(texture, other.texture); -tex_t::tex_t() : array {}, texture { INVALID_TEXTURE, INVALID_TEXTURE } {} -tex_t::tex_t(tex_t &&other) : array { other.array }, texture { other.texture } { - other.array = 0; - other.texture.point = INVALID_TEXTURE; - other.texture.linear = INVALID_TEXTURE; -} + return *this; + } -tex_t &tex_t::operator=(tex_t &&other) { - std::swap(array, other.array); - std::swap(texture, other.texture); + tex_t::~tex_t() { + if (texture.point != INVALID_TEXTURE) { + CU_CHECK_IGNORE(cudaDestroyTextureObject(texture.point), "Couldn't deallocate cuda texture that uses point interpolation"); - return *this; -} + texture.point = INVALID_TEXTURE; + } -tex_t::~tex_t() { - if(texture.point != INVALID_TEXTURE) { - CU_CHECK_IGNORE(cudaDestroyTextureObject(texture.point), "Couldn't deallocate cuda texture that uses point interpolation"); + if (texture.linear != INVALID_TEXTURE) { + CU_CHECK_IGNORE(cudaDestroyTextureObject(texture.linear), "Couldn't deallocate cuda texture that uses linear interpolation"); - texture.point = INVALID_TEXTURE; - } + texture.linear = INVALID_TEXTURE; + } - if(texture.linear != INVALID_TEXTURE) { - CU_CHECK_IGNORE(cudaDestroyTextureObject(texture.linear), "Couldn't deallocate cuda texture that uses linear interpolation"); + if (array) { + CU_CHECK_IGNORE(cudaFreeArray(array), "Couldn't deallocate cuda array"); - texture.linear = INVALID_TEXTURE; + array = cudaArray_t {}; + } } - if(array) { - CU_CHECK_IGNORE(cudaFreeArray(array), "Couldn't deallocate cuda array"); + sws_t::sws_t(int in_width, int in_height, int out_width, int out_height, int pitch, int threadsPerBlock, ptr_t &&color_matrix): + threadsPerBlock {threadsPerBlock}, + color_matrix {std::move(color_matrix)} { + // Ensure aspect ratio is maintained + auto scalar = std::fminf(out_width / (float) in_width, out_height / (float) in_height); + auto out_width_f = in_width * scalar; + auto out_height_f = in_height * scalar; - array = cudaArray_t {}; - } -} + // result is always positive + auto offsetX_f = (out_width - out_width_f) / 2; + auto offsetY_f = (out_height - out_height_f) / 2; -sws_t::sws_t(int in_width, int in_height, int out_width, int out_height, int pitch, int threadsPerBlock, ptr_t &&color_matrix) - : threadsPerBlock { threadsPerBlock }, color_matrix { std::move(color_matrix) } { - // Ensure aspect ratio is maintained - auto scalar = std::fminf(out_width / (float)in_width, out_height / (float)in_height); - auto out_width_f = in_width * scalar; - auto out_height_f = in_height * scalar; + viewport.width = out_width_f; + viewport.height = out_height_f; - // result is always positive - auto offsetX_f = (out_width - out_width_f) / 2; - auto offsetY_f = (out_height - out_height_f) / 2; + viewport.offsetX = offsetX_f; + viewport.offsetY = offsetY_f; - viewport.width = out_width_f; - viewport.height = out_height_f; - - viewport.offsetX = offsetX_f; - viewport.offsetY = offsetY_f; + scale = 1.0f / scalar; + } - scale = 1.0f / scalar; -} + std::optional sws_t::make(int in_width, int in_height, int out_width, int out_height, int pitch) { + cudaDeviceProp props; + int device; + CU_CHECK_OPT(cudaGetDevice(&device), "Couldn't get cuda device"); + CU_CHECK_OPT(cudaGetDeviceProperties(&props, device), "Couldn't get cuda device properties"); -std::optional sws_t::make(int in_width, int in_height, int out_width, int out_height, int pitch) { - cudaDeviceProp props; - int device; - CU_CHECK_OPT(cudaGetDevice(&device), "Couldn't get cuda device"); - CU_CHECK_OPT(cudaGetDeviceProperties(&props, device), "Couldn't get cuda device properties"); + auto ptr = make_ptr(); + if (!ptr) { + return std::nullopt; + } - auto ptr = make_ptr(); - if(!ptr) { - return std::nullopt; + return std::make_optional(in_width, in_height, out_width, out_height, pitch, props.maxThreadsPerMultiProcessor / props.maxBlocksPerMultiProcessor, std::move(ptr)); } - return std::make_optional(in_width, in_height, out_width, out_height, pitch, props.maxThreadsPerMultiProcessor / props.maxBlocksPerMultiProcessor, std::move(ptr)); -} - -int sws_t::convert(std::uint8_t *Y, std::uint8_t *UV, std::uint32_t pitchY, std::uint32_t pitchUV, cudaTextureObject_t texture, stream_t::pointer stream) { - return convert(Y, UV, pitchY, pitchUV, texture, stream, viewport); -} + int sws_t::convert(std::uint8_t *Y, std::uint8_t *UV, std::uint32_t pitchY, std::uint32_t pitchUV, cudaTextureObject_t texture, stream_t::pointer stream) { + return convert(Y, UV, pitchY, pitchUV, texture, stream, viewport); + } -int sws_t::convert(std::uint8_t *Y, std::uint8_t *UV, std::uint32_t pitchY, std::uint32_t pitchUV, cudaTextureObject_t texture, stream_t::pointer stream, const viewport_t &viewport) { - int threadsX = viewport.width / 2; - int threadsY = viewport.height / 2; + int sws_t::convert(std::uint8_t *Y, std::uint8_t *UV, std::uint32_t pitchY, std::uint32_t pitchUV, cudaTextureObject_t texture, stream_t::pointer stream, const viewport_t &viewport) { + int threadsX = viewport.width / 2; + int threadsY = viewport.height / 2; - dim3 block(threadsPerBlock); - dim3 grid(div_align(threadsX, threadsPerBlock), threadsY); + dim3 block(threadsPerBlock); + dim3 grid(div_align(threadsX, threadsPerBlock), threadsY); - RGBA_to_NV12<<>>(texture, Y, UV, pitchY, pitchUV, scale, viewport, (cuda_color_t *)color_matrix.get()); + RGBA_to_NV12<<>>(texture, Y, UV, pitchY, pitchUV, scale, viewport, (cuda_color_t *) color_matrix.get()); - return CU_CHECK_IGNORE(cudaGetLastError(), "RGBA_to_NV12 failed"); -} + return CU_CHECK_IGNORE(cudaGetLastError(), "RGBA_to_NV12 failed"); + } -void sws_t::apply_colorspace(const video::sunshine_colorspace_t& colorspace) { - auto color_p = video::color_vectors_from_colorspace(colorspace); - CU_CHECK_IGNORE(cudaMemcpy(color_matrix.get(), color_p, sizeof(video::color_t), cudaMemcpyHostToDevice), "Couldn't copy color matrix to cuda"); -} + void sws_t::apply_colorspace(const video::sunshine_colorspace_t &colorspace) { + auto color_p = video::color_vectors_from_colorspace(colorspace); + CU_CHECK_IGNORE(cudaMemcpy(color_matrix.get(), color_p, sizeof(video::color_t), cudaMemcpyHostToDevice), "Couldn't copy color matrix to cuda"); + } -int sws_t::load_ram(platf::img_t &img, cudaArray_t array) { - return CU_CHECK_IGNORE(cudaMemcpy2DToArray(array, 0, 0, img.data, img.row_pitch, img.width * img.pixel_pitch, img.height, cudaMemcpyHostToDevice), "Couldn't copy to cuda array"); -} + int sws_t::load_ram(platf::img_t &img, cudaArray_t array) { + return CU_CHECK_IGNORE(cudaMemcpy2DToArray(array, 0, 0, img.data, img.row_pitch, img.width * img.pixel_pitch, img.height, cudaMemcpyHostToDevice), "Couldn't copy to cuda array"); + } -} // namespace cuda +} // namespace cuda diff --git a/src/platform/linux/cuda.h b/src/platform/linux/cuda.h index 88b684b9ad6..c7f5daac4b6 100644 --- a/src/platform/linux/cuda.h +++ b/src/platform/linux/cuda.h @@ -5,15 +5,16 @@ #pragma once #if defined(SUNSHINE_BUILD_CUDA) - - #include "src/video_colorspace.h" - + // standard includes #include #include #include #include #include + // local includes + #include "src/video_colorspace.h" + namespace platf { class avcodec_encode_device_t; class img_t; @@ -22,11 +23,10 @@ namespace platf { namespace cuda { namespace nvfbc { - std::vector - display_names(); + std::vector display_names(); } - std::unique_ptr - make_avcodec_encode_device(int width, int height, bool vram); + + std::unique_ptr make_avcodec_encode_device(int width, int height, bool vram); /** * @brief Create a GL->CUDA encoding device for consuming captured dmabufs. @@ -36,11 +36,9 @@ namespace cuda { * @param offset_y Offset of content in captured frame. * @return FFmpeg encoding device context. */ - std::unique_ptr - make_avcodec_gl_encode_device(int width, int height, int offset_x, int offset_y); + std::unique_ptr make_avcodec_gl_encode_device(int width, int height, int offset_x, int offset_y); - int - init(); + int init(); } // namespace cuda typedef struct cudaArray *cudaArray_t; @@ -57,21 +55,18 @@ namespace cuda { class freeCudaPtr_t { public: - void - operator()(void *ptr); + void operator()(void *ptr); }; class freeCudaStream_t { public: - void - operator()(cudaStream_t ptr); + void operator()(cudaStream_t ptr); }; using ptr_t = std::unique_ptr; using stream_t = std::unique_ptr; - stream_t - make_stream(int flags = 0); + stream_t make_stream(int flags = 0); struct viewport_t { int width, height; @@ -80,19 +75,16 @@ namespace cuda { class tex_t { public: - static std::optional - make(int height, int pitch); + static std::optional make(int height, int pitch); tex_t(); tex_t(tex_t &&); - tex_t & - operator=(tex_t &&other); + tex_t &operator=(tex_t &&other); ~tex_t(); - int - copy(std::uint8_t *src, int height, int pitch); + int copy(std::uint8_t *src, int height, int pitch); cudaArray_t array; @@ -113,20 +105,15 @@ namespace cuda { * * pitch -- The size of a single row of pixels in bytes */ - static std::optional - make(int in_width, int in_height, int out_width, int out_height, int pitch); + static std::optional make(int in_width, int in_height, int out_width, int out_height, int pitch); // Converts loaded image into a CUDevicePtr - int - convert(std::uint8_t *Y, std::uint8_t *UV, std::uint32_t pitchY, std::uint32_t pitchUV, cudaTextureObject_t texture, stream_t::pointer stream); - int - convert(std::uint8_t *Y, std::uint8_t *UV, std::uint32_t pitchY, std::uint32_t pitchUV, cudaTextureObject_t texture, stream_t::pointer stream, const viewport_t &viewport); + int convert(std::uint8_t *Y, std::uint8_t *UV, std::uint32_t pitchY, std::uint32_t pitchUV, cudaTextureObject_t texture, stream_t::pointer stream); + int convert(std::uint8_t *Y, std::uint8_t *UV, std::uint32_t pitchY, std::uint32_t pitchUV, cudaTextureObject_t texture, stream_t::pointer stream, const viewport_t &viewport); - void - apply_colorspace(const video::sunshine_colorspace_t &colorspace); + void apply_colorspace(const video::sunshine_colorspace_t &colorspace); - int - load_ram(platf::img_t &img, cudaArray_t array); + int load_ram(platf::img_t &img, cudaArray_t array); ptr_t color_matrix; @@ -138,4 +125,4 @@ namespace cuda { }; } // namespace cuda -#endif \ No newline at end of file +#endif diff --git a/src/platform/linux/graphics.cpp b/src/platform/linux/graphics.cpp index 8ffba6675ea..245addb6a0a 100644 --- a/src/platform/linux/graphics.cpp +++ b/src/platform/linux/graphics.cpp @@ -2,13 +2,15 @@ * @file src/platform/linux/graphics.cpp * @brief Definitions for graphics related functions. */ +// standard includes +#include + +// local includes #include "graphics.h" #include "src/file_handler.h" #include "src/logging.h" #include "src/video.h" -#include - extern "C" { #include } @@ -17,8 +19,7 @@ extern "C" { // There aren't that many DRM_FORMAT I need to use, so define them here // // They aren't likely to change any time soon. -#define fourcc_code(a, b, c, d) ((std::uint32_t)(a) | ((std::uint32_t)(b) << 8) | \ - ((std::uint32_t)(c) << 16) | ((std::uint32_t)(d) << 24)) +#define fourcc_code(a, b, c, d) ((std::uint32_t) (a) | ((std::uint32_t) (b) << 8) | ((std::uint32_t) (c) << 16) | ((std::uint32_t) (d) << 24)) #define fourcc_mod_code(vendor, val) ((((uint64_t) vendor) << 56) | ((val) & 0x00ffffffffffffffULL)) #define DRM_FORMAT_MOD_INVALID fourcc_mod_code(0, ((1ULL << 56) - 1)) @@ -27,11 +28,11 @@ extern "C" { #endif using namespace std::literals; + namespace gl { GladGLContext ctx; - void - drain_errors(const std::string_view &prefix) { + void drain_errors(const std::string_view &prefix) { GLenum err; while ((err = ctx.GetError()) != GL_NO_ERROR) { BOOST_LOG(error) << "GL: "sv << prefix << ": ["sv << util::hex(err).to_string_view() << ']'; @@ -44,13 +45,12 @@ namespace gl { } } - tex_t - tex_t::make(std::size_t count) { - tex_t textures { count }; + tex_t tex_t::make(std::size_t count) { + tex_t textures {count}; ctx.GenTextures(textures.size(), textures.begin()); - float color[] = { 0.0f, 0.0f, 0.0f, 1.0f }; + float color[] = {0.0f, 0.0f, 0.0f, 1.0f}; for (auto tex : textures) { gl::ctx.BindTexture(GL_TEXTURE_2D, tex); @@ -70,25 +70,22 @@ namespace gl { } } - frame_buf_t - frame_buf_t::make(std::size_t count) { - frame_buf_t frame_buf { count }; + frame_buf_t frame_buf_t::make(std::size_t count) { + frame_buf_t frame_buf {count}; ctx.GenFramebuffers(frame_buf.size(), frame_buf.begin()); return frame_buf; } - void - frame_buf_t::copy(int id, int texture, int offset_x, int offset_y, int width, int height) { + void frame_buf_t::copy(int id, int texture, int offset_x, int offset_y, int width, int height) { gl::ctx.BindFramebuffer(GL_FRAMEBUFFER, (*this)[id]); gl::ctx.ReadBuffer(GL_COLOR_ATTACHMENT0 + id); gl::ctx.BindTexture(GL_TEXTURE_2D, texture); gl::ctx.CopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, offset_x, offset_y, width, height); } - std::string - shader_t::err_str() { + std::string shader_t::err_str() { int length; ctx.GetShaderiv(handle(), GL_INFO_LOG_LENGTH, &length); @@ -102,8 +99,7 @@ namespace gl { return string; } - util::Either - shader_t::compile(const std::string_view &source, GLenum type) { + util::Either shader_t::compile(const std::string_view &source, GLenum type) { shader_t shader; auto data = source.data(); @@ -123,13 +119,11 @@ namespace gl { return shader; } - GLuint - shader_t::handle() const { + GLuint shader_t::handle() const { return _shader.el; } - buffer_t - buffer_t::make(util::buffer_t &&offsets, const char *block, const std::string_view &data) { + buffer_t buffer_t::make(util::buffer_t &&offsets, const char *block, const std::string_view &data) { buffer_t buffer; buffer._block = block; buffer._size = data.size(); @@ -142,25 +136,21 @@ namespace gl { return buffer; } - GLuint - buffer_t::handle() const { + GLuint buffer_t::handle() const { return _buffer.el; } - const char * - buffer_t::block() const { + const char *buffer_t::block() const { return _block; } - void - buffer_t::update(const std::string_view &view, std::size_t offset) { + void buffer_t::update(const std::string_view &view, std::size_t offset) { ctx.BindBuffer(GL_UNIFORM_BUFFER, handle()); ctx.BufferSubData(GL_UNIFORM_BUFFER, offset, view.size(), (const void *) view.data()); } - void - buffer_t::update(std::string_view *members, std::size_t count, std::size_t offset) { - util::buffer_t buffer { _size }; + void buffer_t::update(std::string_view *members, std::size_t count, std::size_t offset) { + util::buffer_t buffer {_size}; for (int x = 0; x < count; ++x) { auto val = members[x]; @@ -171,8 +161,7 @@ namespace gl { update(util::view(buffer.begin(), buffer.end()), offset); } - std::string - program_t::err_str() { + std::string program_t::err_str() { int length; ctx.GetProgramiv(handle(), GL_INFO_LOG_LENGTH, &length); @@ -186,8 +175,7 @@ namespace gl { return string; } - util::Either - program_t::link(const shader_t &vert, const shader_t &frag) { + util::Either program_t::link(const shader_t &vert, const shader_t &frag) { program_t program; program._program.el = ctx.CreateProgram(); @@ -214,16 +202,14 @@ namespace gl { return program; } - void - program_t::bind(const buffer_t &buffer) { + void program_t::bind(const buffer_t &buffer) { ctx.UseProgram(handle()); auto i = ctx.GetUniformBlockIndex(handle(), buffer.block()); ctx.BindBufferBase(GL_UNIFORM_BUFFER, i, buffer.handle()); } - std::optional - program_t::uniform(const char *block, std::pair *members, std::size_t count) { + std::optional program_t::uniform(const char *block, std::pair *members, std::size_t count) { auto i = ctx.GetUniformBlockIndex(handle(), block); if (i == GL_INVALID_INDEX) { BOOST_LOG(error) << "Couldn't find index of ["sv << block << ']'; @@ -235,7 +221,7 @@ namespace gl { bool error_flag = false; - util::buffer_t offsets { count }; + util::buffer_t offsets {count}; auto indices = (std::uint32_t *) alloca(count * sizeof(std::uint32_t)); auto names = (const char **) alloca(count * sizeof(const char *)); auto names_p = names; @@ -260,7 +246,7 @@ namespace gl { } ctx.GetActiveUniformsiv(handle(), count, indices, GL_UNIFORM_OFFSET, offsets.begin()); - util::buffer_t buffer { (std::size_t) size }; + util::buffer_t buffer {(std::size_t) size}; for (int x = 0; x < count; ++x) { auto val = std::get<1>(members[x]); @@ -268,11 +254,10 @@ namespace gl { std::copy_n((const std::uint8_t *) val.data(), val.size(), &buffer[offsets[x]]); } - return buffer_t::make(std::move(offsets), block, std::string_view { (char *) buffer.begin(), buffer.size() }); + return buffer_t::make(std::move(offsets), block, std::string_view {(char *) buffer.begin(), buffer.size()}); } - GLuint - program_t::handle() const { + GLuint program_t::handle() const { return _program.el; } @@ -282,23 +267,24 @@ namespace gbm { device_destroy_fn device_destroy; create_device_fn create_device; - int - init() { - static void *handle { nullptr }; + int init() { + static void *handle {nullptr}; static bool funcs_loaded = false; - if (funcs_loaded) return 0; + if (funcs_loaded) { + return 0; + } if (!handle) { - handle = dyn::handle({ "libgbm.so.1", "libgbm.so" }); + handle = dyn::handle({"libgbm.so.1", "libgbm.so"}); if (!handle) { return -1; } } std::vector> funcs { - { (GLADapiproc *) &device_destroy, "gbm_device_destroy" }, - { (GLADapiproc *) &create_device, "gbm_create_device" }, + {(GLADapiproc *) &device_destroy, "gbm_device_destroy"}, + {(GLADapiproc *) &create_device, "gbm_create_device"}, }; if (dyn::load(handle, funcs)) { @@ -334,16 +320,14 @@ namespace egl { constexpr auto EGL_DMA_BUF_PLANE3_MODIFIER_LO_EXT = 0x3449; constexpr auto EGL_DMA_BUF_PLANE3_MODIFIER_HI_EXT = 0x344A; - bool - fail() { + bool fail() { return eglGetError() != EGL_SUCCESS; } /** * @memberof egl::display_t */ - display_t - make_display(std::variant native_display) { + display_t make_display(std::variant native_display) { constexpr auto EGL_PLATFORM_GBM_MESA = 0x31D7; constexpr auto EGL_PLATFORM_WAYLAND_KHR = 0x31D8; constexpr auto EGL_PLATFORM_X11_KHR = 0x31D5; @@ -408,10 +392,11 @@ namespace egl { return display; } - std::optional - make_ctx(display_t::pointer display) { + std::optional make_ctx(display_t::pointer display) { constexpr int conf_attr[] { - EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, EGL_NONE + EGL_RENDERABLE_TYPE, + EGL_OPENGL_BIT, + EGL_NONE }; int count; @@ -427,10 +412,12 @@ namespace egl { } constexpr int attr[] { - EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE + EGL_CONTEXT_CLIENT_VERSION, + 3, + EGL_NONE }; - ctx_t ctx { display, eglCreateContext(display, conf, EGL_NO_CONTEXT, attr) }; + ctx_t ctx {display, eglCreateContext(display, conf, EGL_NO_CONTEXT, attr)}; if (fail()) { BOOST_LOG(error) << "Couldn't create EGL context: ["sv << util::hex(eglGetError()).to_string_view() << ']'; return std::nullopt; @@ -465,8 +452,7 @@ namespace egl { EGLAttrib hi; }; - inline plane_attr_t - get_plane(std::uint32_t plane_indice) { + inline plane_attr_t get_plane(std::uint32_t plane_indice) { switch (plane_indice) { case 0: return { @@ -511,8 +497,7 @@ namespace egl { * @param surface The surface descriptor. * @return Vector of EGL attributes. */ - std::vector - surface_descriptor_to_egl_attribs(const surface_descriptor_t &surface) { + std::vector surface_descriptor_to_egl_attribs(const surface_descriptor_t &surface) { std::vector attribs; attribs.emplace_back(EGL_WIDTH); @@ -549,8 +534,7 @@ namespace egl { return attribs; } - std::optional - import_source(display_t::pointer egl_display, const surface_descriptor_t &xrgb) { + std::optional import_source(display_t::pointer egl_display, const surface_descriptor_t &xrgb) { auto attribs = surface_descriptor_to_egl_attribs(xrgb); rgb_t rgb { @@ -580,8 +564,7 @@ namespace egl { * @param img The image to use for texture sizing. * @return The new RGB texture. */ - rgb_t - create_blank(platf::img_t &img) { + rgb_t create_blank(platf::img_t &img) { rgb_t rgb { EGL_NO_DISPLAY, EGL_NO_IMAGE, @@ -597,7 +580,7 @@ namespace egl { GLenum attachment = GL_COLOR_ATTACHMENT0; gl::ctx.DrawBuffers(1, &attachment); - const GLuint rgb_black[] = { 0, 0, 0, 0 }; + const GLuint rgb_black[] = {0, 0, 0, 0}; gl::ctx.ClearBufferuiv(GL_COLOR, 0, rgb_black); gl_drain_errors; @@ -605,8 +588,7 @@ namespace egl { return rgb; } - std::optional - import_target(display_t::pointer egl_display, std::array &&fds, const surface_descriptor_t &y, const surface_descriptor_t &uv) { + std::optional import_target(display_t::pointer egl_display, std::array &&fds, const surface_descriptor_t &y, const surface_descriptor_t &uv) { auto y_attribs = surface_descriptor_to_egl_attribs(y); auto uv_attribs = surface_descriptor_to_egl_attribs(uv); @@ -642,8 +624,8 @@ namespace egl { gl::ctx.BindFramebuffer(GL_FRAMEBUFFER, nv12->buf[x]); gl::ctx.DrawBuffers(1, &attachments[x]); - const float y_black[] = { 0.0f, 0.0f, 0.0f, 0.0f }; - const float uv_black[] = { 0.5f, 0.5f, 0.5f, 0.5f }; + const float y_black[] = {0.0f, 0.0f, 0.0f, 0.0f}; + const float uv_black[] = {0.5f, 0.5f, 0.5f, 0.5f}; gl::ctx.ClearBufferfv(GL_COLOR, 0, x == 0 ? y_black : uv_black); } @@ -661,8 +643,7 @@ namespace egl { * @param format Format of the target frame. * @return The new RGB texture. */ - std::optional - create_target(int width, int height, AVPixelFormat format) { + std::optional create_target(int width, int height, AVPixelFormat format) { nv12_t nv12 { EGL_NO_DISPLAY, EGL_NO_IMAGE, @@ -679,12 +660,10 @@ namespace egl { if (fmt_desc->comp[0].depth <= 8) { y_format = GL_R8; uv_format = GL_RG8; - } - else if (fmt_desc->comp[0].depth <= 16) { + } else if (fmt_desc->comp[0].depth <= 16) { y_format = GL_R16; uv_format = GL_RG16; - } - else { + } else { BOOST_LOG(error) << "Unsupported target pixel format: "sv << format; return std::nullopt; } @@ -693,8 +672,7 @@ namespace egl { gl::ctx.TexStorage2D(GL_TEXTURE_2D, 1, y_format, width, height); gl::ctx.BindTexture(GL_TEXTURE_2D, nv12->tex[1]); - gl::ctx.TexStorage2D(GL_TEXTURE_2D, 1, uv_format, - width >> fmt_desc->log2_chroma_w, height >> fmt_desc->log2_chroma_h); + gl::ctx.TexStorage2D(GL_TEXTURE_2D, 1, uv_format, width >> fmt_desc->log2_chroma_w, height >> fmt_desc->log2_chroma_h); nv12->buf.bind(std::begin(nv12->tex), std::end(nv12->tex)); @@ -707,8 +685,8 @@ namespace egl { gl::ctx.BindFramebuffer(GL_FRAMEBUFFER, nv12->buf[x]); gl::ctx.DrawBuffers(1, &attachments[x]); - const float y_black[] = { 0.0f, 0.0f, 0.0f, 0.0f }; - const float uv_black[] = { 0.5f, 0.5f, 0.5f, 0.5f }; + const float y_black[] = {0.0f, 0.0f, 0.0f, 0.0f}; + const float uv_black[] = {0.5f, 0.5f, 0.5f, 0.5f}; gl::ctx.ClearBufferfv(GL_COLOR, 0, x == 0 ? y_black : uv_black); } @@ -719,8 +697,7 @@ namespace egl { return nv12; } - void - sws_t::apply_colorspace(const video::sunshine_colorspace_t &colorspace) { + void sws_t::apply_colorspace(const video::sunshine_colorspace_t &colorspace) { auto color_p = video::color_vectors_from_colorspace(colorspace); std::string_view members[] { @@ -737,8 +714,7 @@ namespace egl { program[1].bind(color_matrix); } - std::optional - sws_t::make(int in_width, int in_height, int out_width, int out_height, gl::tex_t &&tex) { + std::optional sws_t::make(int in_width, int in_height, int out_width, int out_height, gl::tex_t &&tex) { sws_t sws; sws.serial = std::numeric_limits::max(); @@ -866,8 +842,7 @@ namespace egl { return sws; } - int - sws_t::blank(gl::frame_buf_t &fb, int offsetX, int offsetY, int width, int height) { + int sws_t::blank(gl::frame_buf_t &fb, int offsetX, int offsetY, int width, int height) { auto f = [&]() { std::swap(offsetX, this->offsetX); std::swap(offsetY, this->offsetY); @@ -881,8 +856,7 @@ namespace egl { return convert(fb); } - std::optional - sws_t::make(int in_width, int in_height, int out_width, int out_height, AVPixelFormat format) { + std::optional sws_t::make(int in_width, int in_height, int out_width, int out_height, AVPixelFormat format) { GLint gl_format; // Decide the bit depth format of the backing texture based the target frame format @@ -916,16 +890,14 @@ namespace egl { return make(in_width, in_height, out_width, out_height, std::move(tex)); } - void - sws_t::load_ram(platf::img_t &img) { + void sws_t::load_ram(platf::img_t &img) { loaded_texture = tex[0]; gl::ctx.BindTexture(GL_TEXTURE_2D, loaded_texture); gl::ctx.TexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, img.width, img.height, GL_BGRA, GL_UNSIGNED_BYTE, img.data); } - void - sws_t::load_vram(img_descriptor_t &img, int offset_x, int offset_y, int texture) { + void sws_t::load_vram(img_descriptor_t &img, int offset_x, int offset_y, int texture) { // When only a sub-part of the image must be encoded... const bool copy = offset_x || offset_y || img.sd.width != in_width || img.sd.height != in_height; if (copy) { @@ -934,8 +906,7 @@ namespace egl { loaded_texture = tex[0]; framebuf.copy(0, loaded_texture, offset_x, offset_y, in_width, in_height); - } - else { + } else { loaded_texture = texture; } @@ -985,8 +956,7 @@ namespace egl { } } - int - sws_t::convert(gl::frame_buf_t &fb) { + int sws_t::convert(gl::frame_buf_t &fb) { gl::ctx.BindTexture(GL_TEXTURE_2D, loaded_texture); GLenum attachments[] { @@ -1019,7 +989,6 @@ namespace egl { } } // namespace egl -void -free_frame(AVFrame *frame) { +void free_frame(AVFrame *frame) { av_frame_free(&frame); } diff --git a/src/platform/linux/graphics.h b/src/platform/linux/graphics.h index d7dbcf1b4a6..00e38943cbf 100644 --- a/src/platform/linux/graphics.h +++ b/src/platform/linux/graphics.h @@ -4,12 +4,15 @@ */ #pragma once +// standard includes #include #include +// lib includes #include #include +// local includes #include "misc.h" #include "src/logging.h" #include "src/platform/common.h" @@ -21,35 +24,30 @@ #define gl_drain_errors_helper(x) gl::drain_errors(x) #define gl_drain_errors gl_drain_errors_helper(__FILE__ ":" SUNSHINE_STRINGIFY(__LINE__)) -extern "C" int -close(int __fd); +extern "C" int close(int __fd); // X11 Display extern "C" struct _XDisplay; struct AVFrame; -void -free_frame(AVFrame *frame); +void free_frame(AVFrame *frame); using frame_t = util::safe_ptr; namespace gl { extern GladGLContext ctx; - void - drain_errors(const std::string_view &prefix); + void drain_errors(const std::string_view &prefix); class tex_t: public util::buffer_t { using util::buffer_t::buffer_t; public: tex_t(tex_t &&) = default; - tex_t & - operator=(tex_t &&) = default; + tex_t &operator=(tex_t &&) = default; ~tex_t(); - static tex_t - make(std::size_t count); + static tex_t make(std::size_t count); }; class frame_buf_t: public util::buffer_t { @@ -57,16 +55,13 @@ namespace gl { public: frame_buf_t(frame_buf_t &&) = default; - frame_buf_t & - operator=(frame_buf_t &&) = default; + frame_buf_t &operator=(frame_buf_t &&) = default; ~frame_buf_t(); - static frame_buf_t - make(std::size_t count); + static frame_buf_t make(std::size_t count); - inline void - bind(std::nullptr_t, std::nullptr_t) { + inline void bind(std::nullptr_t, std::nullptr_t) { int x = 0; for (auto fb : (*this)) { ctx.BindFramebuffer(GL_FRAMEBUFFER, fb); @@ -77,9 +72,8 @@ namespace gl { return; } - template - void - bind(It it_begin, It it_end) { + template + void bind(It it_begin, It it_end) { using namespace std::literals; if (std::distance(it_begin, it_end) > size()) { BOOST_LOG(warning) << "To many elements to bind"sv; @@ -100,8 +94,7 @@ namespace gl { /** * Copies a part of the framebuffer to texture */ - void - copy(int id, int texture, int offset_x, int offset_y, int width, int height); + void copy(int id, int texture, int offset_x, int offset_y, int width, int height); }; class shader_t { @@ -112,14 +105,11 @@ namespace gl { }); public: - std::string - err_str(); + std::string err_str(); - static util::Either - compile(const std::string_view &source, GLenum type); + static util::Either compile(const std::string_view &source, GLenum type); - GLuint - handle() const; + GLuint handle() const; private: shader_internal_t _shader; @@ -133,19 +123,14 @@ namespace gl { }); public: - static buffer_t - make(util::buffer_t &&offsets, const char *block, const std::string_view &data); + static buffer_t make(util::buffer_t &&offsets, const char *block, const std::string_view &data); - GLuint - handle() const; + GLuint handle() const; - const char * - block() const; + const char *block() const; - void - update(const std::string_view &view, std::size_t offset = 0); - void - update(std::string_view *members, std::size_t count, std::size_t offset = 0); + void update(const std::string_view &view, std::size_t offset = 0); + void update(std::string_view *members, std::size_t count, std::size_t offset = 0); private: const char *_block; @@ -165,20 +150,15 @@ namespace gl { }); public: - std::string - err_str(); + std::string err_str(); - static util::Either - link(const shader_t &vert, const shader_t &frag); + static util::Either link(const shader_t &vert, const shader_t &frag); - void - bind(const buffer_t &buffer); + void bind(const buffer_t &buffer); - std::optional - uniform(const char *block, std::pair *members, std::size_t count); + std::optional uniform(const char *block, std::pair *members, std::size_t count); - GLuint - handle() const; + GLuint handle() const; private: program_internal_t _program; @@ -195,8 +175,7 @@ namespace gbm { using gbm_t = util::dyn_safe_ptr; - int - init(); + int init(); } // namespace gbm @@ -258,24 +237,23 @@ namespace egl { std::uint32_t offsets[4]; }; - display_t - make_display(std::variant native_display); - std::optional - make_ctx(display_t::pointer display); + display_t make_display(std::variant native_display); + std::optional make_ctx(display_t::pointer display); std::optional - import_source( - display_t::pointer egl_display, - const surface_descriptor_t &xrgb); + import_source( + display_t::pointer egl_display, + const surface_descriptor_t &xrgb + ); - rgb_t - create_blank(platf::img_t &img); + rgb_t create_blank(platf::img_t &img); - std::optional - import_target( + std::optional import_target( display_t::pointer egl_display, std::array &&fds, - const surface_descriptor_t &y, const surface_descriptor_t &uv); + const surface_descriptor_t &y, + const surface_descriptor_t &uv + ); /** * @brief Creates biplanar YUV textures to render into. @@ -284,8 +262,7 @@ namespace egl { * @param format Format of the target frame. * @return The new RGB texture. */ - std::optional - create_target(int width, int height, AVPixelFormat format); + std::optional create_target(int width, int height, AVPixelFormat format); class cursor_t: public platf::img_t { public: @@ -304,8 +281,7 @@ namespace egl { reset(); } - void - reset() { + void reset() { for (auto x = 0; x < 4; ++x) { if (sd.fds[x] >= 0) { close(sd.fds[x]); @@ -323,26 +299,19 @@ namespace egl { class sws_t { public: - static std::optional - make(int in_width, int in_height, int out_width, int out_height, gl::tex_t &&tex); - static std::optional - make(int in_width, int in_height, int out_width, int out_height, AVPixelFormat format); + static std::optional make(int in_width, int in_height, int out_width, int out_height, gl::tex_t &&tex); + static std::optional make(int in_width, int in_height, int out_width, int out_height, AVPixelFormat format); // Convert the loaded image into the first two framebuffers - int - convert(gl::frame_buf_t &fb); + int convert(gl::frame_buf_t &fb); // Make an area of the image black - int - blank(gl::frame_buf_t &fb, int offsetX, int offsetY, int width, int height); + int blank(gl::frame_buf_t &fb, int offsetX, int offsetY, int width, int height); - void - load_ram(platf::img_t &img); - void - load_vram(img_descriptor_t &img, int offset_x, int offset_y, int texture); + void load_ram(platf::img_t &img); + void load_vram(img_descriptor_t &img, int offset_x, int offset_y, int texture); - void - apply_colorspace(const video::sunshine_colorspace_t &colorspace); + void apply_colorspace(const video::sunshine_colorspace_t &colorspace); // The first texture is the monitor image. // The second texture is the cursor image @@ -367,6 +336,5 @@ namespace egl { std::uint64_t serial; }; - bool - fail(); + bool fail(); } // namespace egl diff --git a/src/platform/linux/input/inputtino.cpp b/src/platform/linux/input/inputtino.cpp index 5a3278dbc75..3102fdd9120 100644 --- a/src/platform/linux/input/inputtino.cpp +++ b/src/platform/linux/input/inputtino.cpp @@ -2,132 +2,114 @@ * @file src/platform/linux/input/inputtino.cpp * @brief Definitions for the inputtino Linux input handling. */ +// lib includes #include #include -#include "src/config.h" -#include "src/platform/common.h" -#include "src/utility.h" - +// local includes #include "inputtino_common.h" #include "inputtino_gamepad.h" #include "inputtino_keyboard.h" #include "inputtino_mouse.h" #include "inputtino_pen.h" #include "inputtino_touch.h" +#include "src/config.h" +#include "src/platform/common.h" +#include "src/utility.h" using namespace std::literals; namespace platf { - input_t - input() { - return { new input_raw_t() }; + input_t input() { + return {new input_raw_t()}; } - std::unique_ptr - allocate_client_input_context(input_t &input) { + std::unique_ptr allocate_client_input_context(input_t &input) { return std::make_unique(input); } - void - freeInput(void *p) { + void freeInput(void *p) { auto *input = (input_raw_t *) p; delete input; } - void - move_mouse(input_t &input, int deltaX, int deltaY) { + void move_mouse(input_t &input, int deltaX, int deltaY) { auto raw = (input_raw_t *) input.get(); platf::mouse::move(raw, deltaX, deltaY); } - void - abs_mouse(input_t &input, const touch_port_t &touch_port, float x, float y) { + void abs_mouse(input_t &input, const touch_port_t &touch_port, float x, float y) { auto raw = (input_raw_t *) input.get(); platf::mouse::move_abs(raw, touch_port, x, y); } - void - button_mouse(input_t &input, int button, bool release) { + void button_mouse(input_t &input, int button, bool release) { auto raw = (input_raw_t *) input.get(); platf::mouse::button(raw, button, release); } - void - scroll(input_t &input, int high_res_distance) { + void scroll(input_t &input, int high_res_distance) { auto raw = (input_raw_t *) input.get(); platf::mouse::scroll(raw, high_res_distance); } - void - hscroll(input_t &input, int high_res_distance) { + void hscroll(input_t &input, int high_res_distance) { auto raw = (input_raw_t *) input.get(); platf::mouse::hscroll(raw, high_res_distance); } - void - keyboard_update(input_t &input, uint16_t modcode, bool release, uint8_t flags) { + void keyboard_update(input_t &input, uint16_t modcode, bool release, uint8_t flags) { auto raw = (input_raw_t *) input.get(); platf::keyboard::update(raw, modcode, release, flags); } - void - unicode(input_t &input, char *utf8, int size) { + void unicode(input_t &input, char *utf8, int size) { auto raw = (input_raw_t *) input.get(); platf::keyboard::unicode(raw, utf8, size); } - void - touch_update(client_input_t *input, const touch_port_t &touch_port, const touch_input_t &touch) { + void touch_update(client_input_t *input, const touch_port_t &touch_port, const touch_input_t &touch) { auto raw = (client_input_raw_t *) input; platf::touch::update(raw, touch_port, touch); } - void - pen_update(client_input_t *input, const touch_port_t &touch_port, const pen_input_t &pen) { + void pen_update(client_input_t *input, const touch_port_t &touch_port, const pen_input_t &pen) { auto raw = (client_input_raw_t *) input; platf::pen::update(raw, touch_port, pen); } - int - alloc_gamepad(input_t &input, const gamepad_id_t &id, const gamepad_arrival_t &metadata, feedback_queue_t feedback_queue) { + int alloc_gamepad(input_t &input, const gamepad_id_t &id, const gamepad_arrival_t &metadata, feedback_queue_t feedback_queue) { auto raw = (input_raw_t *) input.get(); return platf::gamepad::alloc(raw, id, metadata, feedback_queue); } - void - free_gamepad(input_t &input, int nr) { + void free_gamepad(input_t &input, int nr) { auto raw = (input_raw_t *) input.get(); platf::gamepad::free(raw, nr); } - void - gamepad_update(input_t &input, int nr, const gamepad_state_t &gamepad_state) { + void gamepad_update(input_t &input, int nr, const gamepad_state_t &gamepad_state) { auto raw = (input_raw_t *) input.get(); platf::gamepad::update(raw, nr, gamepad_state); } - void - gamepad_touch(input_t &input, const gamepad_touch_t &touch) { + void gamepad_touch(input_t &input, const gamepad_touch_t &touch) { auto raw = (input_raw_t *) input.get(); platf::gamepad::touch(raw, touch); } - void - gamepad_motion(input_t &input, const gamepad_motion_t &motion) { + void gamepad_motion(input_t &input, const gamepad_motion_t &motion) { auto raw = (input_raw_t *) input.get(); platf::gamepad::motion(raw, motion); } - void - gamepad_battery(input_t &input, const gamepad_battery_t &battery) { + void gamepad_battery(input_t &input, const gamepad_battery_t &battery) { auto raw = (input_raw_t *) input.get(); platf::gamepad::battery(raw, battery); } - platform_caps::caps_t - get_capabilities() { + platform_caps::caps_t get_capabilities() { platform_caps::caps_t caps = 0; // TODO: if has_uinput caps |= platform_caps::pen_touch; @@ -140,14 +122,12 @@ namespace platf { return caps; } - util::point_t - get_mouse_loc(input_t &input) { + util::point_t get_mouse_loc(input_t &input) { auto raw = (input_raw_t *) input.get(); return platf::mouse::get_location(raw); } - std::vector & - supported_gamepads(input_t *input) { + std::vector &supported_gamepads(input_t *input) { return platf::gamepad::supported_gamepads(input); } } // namespace platf diff --git a/src/platform/linux/input/inputtino_common.h b/src/platform/linux/input/inputtino_common.h index 67d1932856f..855f88a4186 100644 --- a/src/platform/linux/input/inputtino_common.h +++ b/src/platform/linux/input/inputtino_common.h @@ -4,10 +4,12 @@ */ #pragma once +// lib includes #include #include #include +// local includes #include "src/config.h" #include "src/logging.h" #include "src/platform/common.h" @@ -94,8 +96,7 @@ namespace platf { inputtino::Result pen; }; - inline float - deg2rad(float degree) { + inline float deg2rad(float degree) { return degree * (M_PI / 180.f); } } // namespace platf diff --git a/src/platform/linux/input/inputtino_gamepad.cpp b/src/platform/linux/input/inputtino_gamepad.cpp index 15fbfcaf501..438146317c6 100644 --- a/src/platform/linux/input/inputtino_gamepad.cpp +++ b/src/platform/linux/input/inputtino_gamepad.cpp @@ -2,18 +2,19 @@ * @file src/platform/linux/input/inputtino_gamepad.cpp * @brief Definitions for inputtino gamepad input handling. */ +// lib includes #include #include #include +// local includes +#include "inputtino_common.h" +#include "inputtino_gamepad.h" #include "src/config.h" #include "src/logging.h" #include "src/platform/common.h" #include "src/utility.h" -#include "inputtino_common.h" -#include "inputtino_gamepad.h" - using namespace std::literals; namespace platf::gamepad { @@ -25,69 +26,54 @@ namespace platf::gamepad { GAMEPAD_STATUS ///< Helper to indicate the number of status }; - auto - create_xbox_one() { - return inputtino::XboxOneJoypad::create({ .name = "Sunshine X-Box One (virtual) pad", - // https://github.com/torvalds/linux/blob/master/drivers/input/joystick/xpad.c#L147 - .vendor_id = 0x045E, - .product_id = 0x02EA, - .version = 0x0408 }); + auto create_xbox_one() { + return inputtino::XboxOneJoypad::create({.name = "Sunshine X-Box One (virtual) pad", + // https://github.com/torvalds/linux/blob/master/drivers/input/joystick/xpad.c#L147 + .vendor_id = 0x045E, + .product_id = 0x02EA, + .version = 0x0408}); } - auto - create_switch() { - return inputtino::SwitchJoypad::create({ .name = "Sunshine Nintendo (virtual) pad", - // https://github.com/torvalds/linux/blob/master/drivers/hid/hid-ids.h#L981 - .vendor_id = 0x057e, - .product_id = 0x2009, - .version = 0x8111 }); + auto create_switch() { + return inputtino::SwitchJoypad::create({.name = "Sunshine Nintendo (virtual) pad", + // https://github.com/torvalds/linux/blob/master/drivers/hid/hid-ids.h#L981 + .vendor_id = 0x057e, + .product_id = 0x2009, + .version = 0x8111}); } - auto - create_ds5() { - return inputtino::PS5Joypad::create({ .name = "Sunshine DualSense (virtual) pad", - .vendor_id = 0x054C, - .product_id = 0x0CE6, - .version = 0x8111 }); + auto create_ds5() { + return inputtino::PS5Joypad::create({.name = "Sunshine PS5 (virtual) pad", .vendor_id = 0x054C, .product_id = 0x0CE6, .version = 0x8111}); } - int - alloc(input_raw_t *raw, const gamepad_id_t &id, const gamepad_arrival_t &metadata, feedback_queue_t feedback_queue) { + int alloc(input_raw_t *raw, const gamepad_id_t &id, const gamepad_arrival_t &metadata, feedback_queue_t feedback_queue) { ControllerType selectedGamepadType; if (config::input.gamepad == "xone"sv) { BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be Xbox One controller (manual selection)"sv; selectedGamepadType = XboxOneWired; - } - else if (config::input.gamepad == "ds5"sv) { + } else if (config::input.gamepad == "ds5"sv) { BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be DualSense 5 controller (manual selection)"sv; selectedGamepadType = DualSenseWired; - } - else if (config::input.gamepad == "switch"sv) { + } else if (config::input.gamepad == "switch"sv) { BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be Nintendo Pro controller (manual selection)"sv; selectedGamepadType = SwitchProWired; - } - else if (metadata.type == LI_CTYPE_XBOX) { + } else if (metadata.type == LI_CTYPE_XBOX) { BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be Xbox One controller (auto-selected by client-reported type)"sv; selectedGamepadType = XboxOneWired; - } - else if (metadata.type == LI_CTYPE_PS) { + } else if (metadata.type == LI_CTYPE_PS) { BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be DualShock 5 controller (auto-selected by client-reported type)"sv; selectedGamepadType = DualSenseWired; - } - else if (metadata.type == LI_CTYPE_NINTENDO) { + } else if (metadata.type == LI_CTYPE_NINTENDO) { BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be Nintendo Pro controller (auto-selected by client-reported type)"sv; selectedGamepadType = SwitchProWired; - } - else if (config::input.motion_as_ds4 && (metadata.capabilities & (LI_CCAP_ACCEL | LI_CCAP_GYRO))) { + } else if (config::input.motion_as_ds4 && (metadata.capabilities & (LI_CCAP_ACCEL | LI_CCAP_GYRO))) { BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be DualShock 5 controller (auto-selected by motion sensor presence)"sv; selectedGamepadType = DualSenseWired; - } - else if (config::input.touchpad_as_ds4 && (metadata.capabilities & LI_CCAP_TOUCHPAD)) { + } else if (config::input.touchpad_as_ds4 && (metadata.capabilities & LI_CCAP_TOUCHPAD)) { BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be DualShock 5 controller (auto-selected by touchpad presence)"sv; selectedGamepadType = DualSenseWired; - } - else { + } else { BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be Xbox One controller (default)"sv; selectedGamepadType = XboxOneWired; } @@ -102,8 +88,7 @@ namespace platf::gamepad { if (metadata.capabilities & LI_CCAP_RGB_LED) { BOOST_LOG(warning) << "Gamepad " << id.globalIndex << " has an RGB LED, but it is not usable when emulating a joypad different from DS5"sv; } - } - else if (selectedGamepadType == DualSenseWired) { + } else if (selectedGamepadType == DualSenseWired) { if (!(metadata.capabilities & (LI_CCAP_ACCEL | LI_CCAP_GYRO))) { BOOST_LOG(warning) << "Gamepad " << id.globalIndex << " is emulating a DualShock 5 controller, but the client gamepad doesn't have motion sensors active"sv; } @@ -125,73 +110,75 @@ namespace platf::gamepad { }; switch (selectedGamepadType) { - case XboxOneWired: { - auto xOne = create_xbox_one(); - if (xOne) { - (*xOne).set_on_rumble(on_rumble_fn); - gamepad->joypad = std::make_unique(std::move(*xOne)); - raw->gamepads[id.globalIndex] = std::move(gamepad); - return 0; - } - else { - BOOST_LOG(warning) << "Unable to create virtual Xbox One controller: " << xOne.getErrorMessage(); - return -1; - } - } - case SwitchProWired: { - auto switchPro = create_switch(); - if (switchPro) { - (*switchPro).set_on_rumble(on_rumble_fn); - gamepad->joypad = std::make_unique(std::move(*switchPro)); - raw->gamepads[id.globalIndex] = std::move(gamepad); - return 0; + case XboxOneWired: + { + auto xOne = create_xbox_one(); + if (xOne) { + (*xOne).set_on_rumble(on_rumble_fn); + gamepad->joypad = std::make_unique(std::move(*xOne)); + raw->gamepads[id.globalIndex] = std::move(gamepad); + return 0; + } else { + BOOST_LOG(warning) << "Unable to create virtual Xbox One controller: " << xOne.getErrorMessage(); + return -1; + } } - else { - BOOST_LOG(warning) << "Unable to create virtual Switch Pro controller: " << switchPro.getErrorMessage(); - return -1; + case SwitchProWired: + { + auto switchPro = create_switch(); + if (switchPro) { + (*switchPro).set_on_rumble(on_rumble_fn); + gamepad->joypad = std::make_unique(std::move(*switchPro)); + raw->gamepads[id.globalIndex] = std::move(gamepad); + return 0; + } else { + BOOST_LOG(warning) << "Unable to create virtual Switch Pro controller: " << switchPro.getErrorMessage(); + return -1; + } } - } - case DualSenseWired: { - auto ds5 = create_ds5(); - 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) { - // Don't resend duplicate LED data - if (gamepad->last_rgb_led.type == platf::gamepad_feedback_e::set_rgb_led && gamepad->last_rgb_led.data.rgb_led.r == r && gamepad->last_rgb_led.data.rgb_led.g == g && gamepad->last_rgb_led.data.rgb_led.b == b) { - return; - } + case DualSenseWired: + { + auto ds5 = create_ds5(); + 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) { + // Don't resend duplicate LED data + if (gamepad->last_rgb_led.type == platf::gamepad_feedback_e::set_rgb_led && gamepad->last_rgb_led.data.rgb_led.r == r && gamepad->last_rgb_led.data.rgb_led.g == g && gamepad->last_rgb_led.data.rgb_led.b == b) { + return; + } - auto msg = gamepad_feedback_msg_t::make_rgb_led(idx, r, g, b); - feedback_queue->raise(msg); - gamepad->last_rgb_led = msg; - }); + auto msg = gamepad_feedback_msg_t::make_rgb_led(idx, r, g, b); + feedback_queue->raise(msg); + gamepad->last_rgb_led = msg; + }); - // Activate the motion sensors - feedback_queue->raise(gamepad_feedback_msg_t::make_motion_event_state(id.clientRelativeIndex, LI_MOTION_TYPE_ACCEL, 100)); - feedback_queue->raise(gamepad_feedback_msg_t::make_motion_event_state(id.clientRelativeIndex, LI_MOTION_TYPE_GYRO, 100)); + (*ds5).set_on_trigger_effect([feedback_queue, idx = id.clientRelativeIndex](const inputtino::PS5Joypad::TriggerEffect &trigger_effect) { + feedback_queue->raise(gamepad_feedback_msg_t::make_adaptive_triggers(idx, trigger_effect.event_flags, trigger_effect.type_left, trigger_effect.type_right, trigger_effect.left, trigger_effect.right)); + }); - gamepad->joypad = std::make_unique(std::move(*ds5)); - raw->gamepads[id.globalIndex] = std::move(gamepad); - return 0; - } - else { - BOOST_LOG(warning) << "Unable to create virtual DualShock 5 controller: " << ds5.getErrorMessage(); - return -1; + // Activate the motion sensors + feedback_queue->raise(gamepad_feedback_msg_t::make_motion_event_state(id.clientRelativeIndex, LI_MOTION_TYPE_ACCEL, 100)); + feedback_queue->raise(gamepad_feedback_msg_t::make_motion_event_state(id.clientRelativeIndex, LI_MOTION_TYPE_GYRO, 100)); + + gamepad->joypad = std::make_unique(std::move(*ds5)); + raw->gamepads[id.globalIndex] = std::move(gamepad); + return 0; + } else { + BOOST_LOG(warning) << "Unable to create virtual DualShock 5 controller: " << ds5.getErrorMessage(); + return -1; + } } - } } return -1; } - void - free(input_raw_t *raw, int nr) { + void free(input_raw_t *raw, int nr) { // This will call the destructor which in turn will stop the background threads for rumble and LED (and ultimately remove the joypad device) raw->gamepads[nr]->joypad.reset(); raw->gamepads[nr].reset(); } - void - update(input_raw_t *raw, int nr, const gamepad_state_t &gamepad_state) { + void update(input_raw_t *raw, int nr, const gamepad_state_t &gamepad_state) { auto gamepad = raw->gamepads[nr]; if (!gamepad) { return; @@ -203,11 +190,10 @@ namespace platf::gamepad { gc.set_stick(inputtino::Joypad::RS, gamepad_state.rsX, gamepad_state.rsY); gc.set_triggers(gamepad_state.lt, gamepad_state.rt); }, - *gamepad->joypad); + *gamepad->joypad); } - void - touch(input_raw_t *raw, const gamepad_touch_t &touch) { + void touch(input_raw_t *raw, const gamepad_touch_t &touch) { auto gamepad = raw->gamepads[touch.id.globalIndex]; if (!gamepad) { return; @@ -216,15 +202,13 @@ namespace platf::gamepad { if (std::holds_alternative(*gamepad->joypad)) { if (touch.pressure > 0.5) { std::get(*gamepad->joypad).place_finger(touch.pointerId, touch.x * inputtino::PS5Joypad::touchpad_width, touch.y * inputtino::PS5Joypad::touchpad_height); - } - else { + } else { std::get(*gamepad->joypad).release_finger(touch.pointerId); } } } - void - motion(input_raw_t *raw, const gamepad_motion_t &motion) { + void motion(input_raw_t *raw, const gamepad_motion_t &motion) { auto gamepad = raw->gamepads[motion.id.globalIndex]; if (!gamepad) { return; @@ -242,8 +226,7 @@ namespace platf::gamepad { } } - void - battery(input_raw_t *raw, const gamepad_battery_t &battery) { + void battery(input_raw_t *raw, const gamepad_battery_t &battery) { auto gamepad = raw->gamepads[battery.id.globalIndex]; if (!gamepad) { return; @@ -272,14 +255,13 @@ namespace platf::gamepad { } } - std::vector & - supported_gamepads(input_t *input) { + std::vector &supported_gamepads(input_t *input) { if (!input) { static std::vector gps { - supported_gamepad_t { "auto", true, "" }, - supported_gamepad_t { "xone", false, "" }, - supported_gamepad_t { "ds5", false, "" }, - supported_gamepad_t { "switch", false, "" }, + supported_gamepad_t {"auto", true, ""}, + supported_gamepad_t {"xone", false, ""}, + supported_gamepad_t {"ds5", false, ""}, + supported_gamepad_t {"switch", false, ""}, }; return gps; @@ -290,10 +272,10 @@ namespace platf::gamepad { auto xOne = create_xbox_one(); static std::vector gps { - supported_gamepad_t { "auto", true, "" }, - supported_gamepad_t { "xone", static_cast(xOne), !xOne ? xOne.getErrorMessage() : "" }, - supported_gamepad_t { "ds5", static_cast(ds5), !ds5 ? ds5.getErrorMessage() : "" }, - supported_gamepad_t { "switch", static_cast(switchPro), !switchPro ? switchPro.getErrorMessage() : "" }, + supported_gamepad_t {"auto", true, ""}, + supported_gamepad_t {"xone", static_cast(xOne), !xOne ? xOne.getErrorMessage() : ""}, + supported_gamepad_t {"ds5", static_cast(ds5), !ds5 ? ds5.getErrorMessage() : ""}, + supported_gamepad_t {"switch", static_cast(switchPro), !switchPro ? switchPro.getErrorMessage() : ""}, }; for (auto &[name, is_enabled, reason_disabled] : gps) { diff --git a/src/platform/linux/input/inputtino_gamepad.h b/src/platform/linux/input/inputtino_gamepad.h index 481b809136b..8d26c9e1ff6 100644 --- a/src/platform/linux/input/inputtino_gamepad.h +++ b/src/platform/linux/input/inputtino_gamepad.h @@ -4,13 +4,14 @@ */ #pragma once +// lib includes #include #include #include -#include "src/platform/common.h" - +// local includes #include "inputtino_common.h" +#include "src/platform/common.h" using namespace std::literals; @@ -22,24 +23,17 @@ namespace platf::gamepad { SwitchProWired ///< Switch Pro Wired Controller }; - int - alloc(input_raw_t *raw, const gamepad_id_t &id, const gamepad_arrival_t &metadata, feedback_queue_t feedback_queue); + int alloc(input_raw_t *raw, const gamepad_id_t &id, const gamepad_arrival_t &metadata, feedback_queue_t feedback_queue); - void - free(input_raw_t *raw, int nr); + void free(input_raw_t *raw, int nr); - void - update(input_raw_t *raw, int nr, const gamepad_state_t &gamepad_state); + void update(input_raw_t *raw, int nr, const gamepad_state_t &gamepad_state); - void - touch(input_raw_t *raw, const gamepad_touch_t &touch); + void touch(input_raw_t *raw, const gamepad_touch_t &touch); - void - motion(input_raw_t *raw, const gamepad_motion_t &motion); + void motion(input_raw_t *raw, const gamepad_motion_t &motion); - void - battery(input_raw_t *raw, const gamepad_battery_t &battery); + void battery(input_raw_t *raw, const gamepad_battery_t &battery); - std::vector & - supported_gamepads(input_t *input); + std::vector &supported_gamepads(input_t *input); } // namespace platf::gamepad diff --git a/src/platform/linux/input/inputtino_keyboard.cpp b/src/platform/linux/input/inputtino_keyboard.cpp index ef5c330bd15..e384b312f18 100644 --- a/src/platform/linux/input/inputtino_keyboard.cpp +++ b/src/platform/linux/input/inputtino_keyboard.cpp @@ -2,18 +2,19 @@ * @file src/platform/linux/input/inputtino_keyboard.cpp * @brief Definitions for inputtino keyboard input handling. */ +// lib includes #include #include #include +// local includes +#include "inputtino_common.h" +#include "inputtino_keyboard.h" #include "src/config.h" #include "src/logging.h" #include "src/platform/common.h" #include "src/utility.h" -#include "inputtino_common.h" -#include "inputtino_keyboard.h" - using namespace std::literals; namespace platf::keyboard { @@ -25,8 +26,7 @@ namespace platf::keyboard { * * adapted from: https://stackoverflow.com/a/7639754 */ - std::string - to_hex(const std::basic_string &str) { + std::string to_hex(const std::basic_string &str) { std::stringstream ss; ss << std::hex << std::setfill('0'); for (const auto &ch : str) { @@ -42,48 +42,123 @@ namespace platf::keyboard { * A map of linux scan code -> Moonlight keyboard code */ static const std::map key_mappings = { - { KEY_BACKSPACE, 0x08 }, { KEY_TAB, 0x09 }, { KEY_ENTER, 0x0D }, { KEY_LEFTSHIFT, 0x10 }, - { KEY_LEFTCTRL, 0x11 }, { KEY_CAPSLOCK, 0x14 }, { KEY_ESC, 0x1B }, { KEY_SPACE, 0x20 }, - { KEY_PAGEUP, 0x21 }, { KEY_PAGEDOWN, 0x22 }, { KEY_END, 0x23 }, { KEY_HOME, 0x24 }, - { KEY_LEFT, 0x25 }, { KEY_UP, 0x26 }, { KEY_RIGHT, 0x27 }, { KEY_DOWN, 0x28 }, - { KEY_SYSRQ, 0x2C }, { KEY_INSERT, 0x2D }, { KEY_DELETE, 0x2E }, { KEY_0, 0x30 }, - { KEY_1, 0x31 }, { KEY_2, 0x32 }, { KEY_3, 0x33 }, { KEY_4, 0x34 }, - { KEY_5, 0x35 }, { KEY_6, 0x36 }, { KEY_7, 0x37 }, { KEY_8, 0x38 }, - { KEY_9, 0x39 }, { KEY_A, 0x41 }, { KEY_B, 0x42 }, { KEY_C, 0x43 }, - { KEY_D, 0x44 }, { KEY_E, 0x45 }, { KEY_F, 0x46 }, { KEY_G, 0x47 }, - { KEY_H, 0x48 }, { KEY_I, 0x49 }, { KEY_J, 0x4A }, { KEY_K, 0x4B }, - { KEY_L, 0x4C }, { KEY_M, 0x4D }, { KEY_N, 0x4E }, { KEY_O, 0x4F }, - { KEY_P, 0x50 }, { KEY_Q, 0x51 }, { KEY_R, 0x52 }, { KEY_S, 0x53 }, - { KEY_T, 0x54 }, { KEY_U, 0x55 }, { KEY_V, 0x56 }, { KEY_W, 0x57 }, - { KEY_X, 0x58 }, { KEY_Y, 0x59 }, { KEY_Z, 0x5A }, { KEY_LEFTMETA, 0x5B }, - { KEY_RIGHTMETA, 0x5C }, { KEY_KP0, 0x60 }, { KEY_KP1, 0x61 }, { KEY_KP2, 0x62 }, - { KEY_KP3, 0x63 }, { KEY_KP4, 0x64 }, { KEY_KP5, 0x65 }, { KEY_KP6, 0x66 }, - { KEY_KP7, 0x67 }, { KEY_KP8, 0x68 }, { KEY_KP9, 0x69 }, { KEY_KPASTERISK, 0x6A }, - { KEY_KPPLUS, 0x6B }, { KEY_KPMINUS, 0x6D }, { KEY_KPDOT, 0x6E }, { KEY_KPSLASH, 0x6F }, - { KEY_F1, 0x70 }, { KEY_F2, 0x71 }, { KEY_F3, 0x72 }, { KEY_F4, 0x73 }, - { KEY_F5, 0x74 }, { KEY_F6, 0x75 }, { KEY_F7, 0x76 }, { KEY_F8, 0x77 }, - { KEY_F9, 0x78 }, { KEY_F10, 0x79 }, { KEY_F11, 0x7A }, { KEY_F12, 0x7B }, - { KEY_NUMLOCK, 0x90 }, { KEY_SCROLLLOCK, 0x91 }, { KEY_LEFTSHIFT, 0xA0 }, { KEY_RIGHTSHIFT, 0xA1 }, - { KEY_LEFTCTRL, 0xA2 }, { KEY_RIGHTCTRL, 0xA3 }, { KEY_LEFTALT, 0xA4 }, { KEY_RIGHTALT, 0xA5 }, - { KEY_SEMICOLON, 0xBA }, { KEY_EQUAL, 0xBB }, { KEY_COMMA, 0xBC }, { KEY_MINUS, 0xBD }, - { KEY_DOT, 0xBE }, { KEY_SLASH, 0xBF }, { KEY_GRAVE, 0xC0 }, { KEY_LEFTBRACE, 0xDB }, - { KEY_BACKSLASH, 0xDC }, { KEY_RIGHTBRACE, 0xDD }, { KEY_APOSTROPHE, 0xDE }, { KEY_102ND, 0xE2 } + {KEY_BACKSPACE, 0x08}, + {KEY_TAB, 0x09}, + {KEY_ENTER, 0x0D}, + {KEY_LEFTSHIFT, 0x10}, + {KEY_LEFTCTRL, 0x11}, + {KEY_CAPSLOCK, 0x14}, + {KEY_ESC, 0x1B}, + {KEY_SPACE, 0x20}, + {KEY_PAGEUP, 0x21}, + {KEY_PAGEDOWN, 0x22}, + {KEY_END, 0x23}, + {KEY_HOME, 0x24}, + {KEY_LEFT, 0x25}, + {KEY_UP, 0x26}, + {KEY_RIGHT, 0x27}, + {KEY_DOWN, 0x28}, + {KEY_SYSRQ, 0x2C}, + {KEY_INSERT, 0x2D}, + {KEY_DELETE, 0x2E}, + {KEY_0, 0x30}, + {KEY_1, 0x31}, + {KEY_2, 0x32}, + {KEY_3, 0x33}, + {KEY_4, 0x34}, + {KEY_5, 0x35}, + {KEY_6, 0x36}, + {KEY_7, 0x37}, + {KEY_8, 0x38}, + {KEY_9, 0x39}, + {KEY_A, 0x41}, + {KEY_B, 0x42}, + {KEY_C, 0x43}, + {KEY_D, 0x44}, + {KEY_E, 0x45}, + {KEY_F, 0x46}, + {KEY_G, 0x47}, + {KEY_H, 0x48}, + {KEY_I, 0x49}, + {KEY_J, 0x4A}, + {KEY_K, 0x4B}, + {KEY_L, 0x4C}, + {KEY_M, 0x4D}, + {KEY_N, 0x4E}, + {KEY_O, 0x4F}, + {KEY_P, 0x50}, + {KEY_Q, 0x51}, + {KEY_R, 0x52}, + {KEY_S, 0x53}, + {KEY_T, 0x54}, + {KEY_U, 0x55}, + {KEY_V, 0x56}, + {KEY_W, 0x57}, + {KEY_X, 0x58}, + {KEY_Y, 0x59}, + {KEY_Z, 0x5A}, + {KEY_LEFTMETA, 0x5B}, + {KEY_RIGHTMETA, 0x5C}, + {KEY_KP0, 0x60}, + {KEY_KP1, 0x61}, + {KEY_KP2, 0x62}, + {KEY_KP3, 0x63}, + {KEY_KP4, 0x64}, + {KEY_KP5, 0x65}, + {KEY_KP6, 0x66}, + {KEY_KP7, 0x67}, + {KEY_KP8, 0x68}, + {KEY_KP9, 0x69}, + {KEY_KPASTERISK, 0x6A}, + {KEY_KPPLUS, 0x6B}, + {KEY_KPMINUS, 0x6D}, + {KEY_KPDOT, 0x6E}, + {KEY_KPSLASH, 0x6F}, + {KEY_F1, 0x70}, + {KEY_F2, 0x71}, + {KEY_F3, 0x72}, + {KEY_F4, 0x73}, + {KEY_F5, 0x74}, + {KEY_F6, 0x75}, + {KEY_F7, 0x76}, + {KEY_F8, 0x77}, + {KEY_F9, 0x78}, + {KEY_F10, 0x79}, + {KEY_F11, 0x7A}, + {KEY_F12, 0x7B}, + {KEY_NUMLOCK, 0x90}, + {KEY_SCROLLLOCK, 0x91}, + {KEY_LEFTSHIFT, 0xA0}, + {KEY_RIGHTSHIFT, 0xA1}, + {KEY_LEFTCTRL, 0xA2}, + {KEY_RIGHTCTRL, 0xA3}, + {KEY_LEFTALT, 0xA4}, + {KEY_RIGHTALT, 0xA5}, + {KEY_SEMICOLON, 0xBA}, + {KEY_EQUAL, 0xBB}, + {KEY_COMMA, 0xBC}, + {KEY_MINUS, 0xBD}, + {KEY_DOT, 0xBE}, + {KEY_SLASH, 0xBF}, + {KEY_GRAVE, 0xC0}, + {KEY_LEFTBRACE, 0xDB}, + {KEY_BACKSLASH, 0xDC}, + {KEY_RIGHTBRACE, 0xDD}, + {KEY_APOSTROPHE, 0xDE}, + {KEY_102ND, 0xE2} }; - void - update(input_raw_t *raw, uint16_t modcode, bool release, uint8_t flags) { + void update(input_raw_t *raw, uint16_t modcode, bool release, uint8_t flags) { if (raw->keyboard) { if (release) { (*raw->keyboard).release(modcode); - } - else { + } else { (*raw->keyboard).press(modcode); } } } - void - unicode(input_raw_t *raw, char *utf8, int size) { + void unicode(input_raw_t *raw, char *utf8, int size) { if (raw->keyboard) { /* Reading input text as UTF-8 */ auto utf8_str = boost::locale::conv::to_utf(utf8, utf8 + size, "UTF-8"); @@ -106,8 +181,7 @@ namespace platf::keyboard { auto wincode = key_mappings.find(keycode); if (keycode == -1 || wincode == key_mappings.end()) { BOOST_LOG(warning) << "Unicode, unable to find keycode for: "sv << ch; - } - else { + } else { (*raw->keyboard).press(wincode->second); (*raw->keyboard).release(wincode->second); } diff --git a/src/platform/linux/input/inputtino_keyboard.h b/src/platform/linux/input/inputtino_keyboard.h index 22be4666b91..6405b62587c 100644 --- a/src/platform/linux/input/inputtino_keyboard.h +++ b/src/platform/linux/input/inputtino_keyboard.h @@ -4,18 +4,18 @@ */ #pragma once +// lib includes #include #include #include +// local includes #include "inputtino_common.h" using namespace std::literals; namespace platf::keyboard { - void - update(input_raw_t *raw, uint16_t modcode, bool release, uint8_t flags); + void update(input_raw_t *raw, uint16_t modcode, bool release, uint8_t flags); - void - unicode(input_raw_t *raw, char *utf8, int size); + void unicode(input_raw_t *raw, char *utf8, int size); } // namespace platf::keyboard diff --git a/src/platform/linux/input/inputtino_mouse.cpp b/src/platform/linux/input/inputtino_mouse.cpp index 5e26e126333..f8d822de22c 100644 --- a/src/platform/linux/input/inputtino_mouse.cpp +++ b/src/platform/linux/input/inputtino_mouse.cpp @@ -2,38 +2,36 @@ * @file src/platform/linux/input/inputtino_mouse.cpp * @brief Definitions for inputtino mouse input handling. */ +// lib includes #include #include #include +// local includes +#include "inputtino_common.h" +#include "inputtino_mouse.h" #include "src/config.h" #include "src/logging.h" #include "src/platform/common.h" #include "src/utility.h" -#include "inputtino_common.h" -#include "inputtino_mouse.h" - using namespace std::literals; namespace platf::mouse { - void - move(input_raw_t *raw, int deltaX, int deltaY) { + void move(input_raw_t *raw, int deltaX, int deltaY) { if (raw->mouse) { (*raw->mouse).move(deltaX, deltaY); } } - void - move_abs(input_raw_t *raw, const touch_port_t &touch_port, float x, float y) { + void move_abs(input_raw_t *raw, const touch_port_t &touch_port, float x, float y) { if (raw->mouse) { (*raw->mouse).move_abs(x, y, touch_port.width, touch_port.height); } } - void - button(input_raw_t *raw, int button, bool release) { + void button(input_raw_t *raw, int button, bool release) { if (raw->mouse) { inputtino::Mouse::MOUSE_BUTTON btn_type; switch (button) { @@ -58,35 +56,31 @@ namespace platf::mouse { } if (release) { (*raw->mouse).release(btn_type); - } - else { + } else { (*raw->mouse).press(btn_type); } } } - void - scroll(input_raw_t *raw, int high_res_distance) { + void scroll(input_raw_t *raw, int high_res_distance) { if (raw->mouse) { (*raw->mouse).vertical_scroll(high_res_distance); } } - void - hscroll(input_raw_t *raw, int high_res_distance) { + void hscroll(input_raw_t *raw, int high_res_distance) { if (raw->mouse) { (*raw->mouse).horizontal_scroll(high_res_distance); } } - util::point_t - get_location(input_raw_t *raw) { + util::point_t get_location(input_raw_t *raw) { if (raw->mouse) { // TODO: decide what to do after https://github.com/games-on-whales/inputtino/issues/6 is resolved. // TODO: auto x = (*raw->mouse).get_absolute_x(); // TODO: auto y = (*raw->mouse).get_absolute_y(); - return { 0, 0 }; + return {0, 0}; } - return { 0, 0 }; + return {0, 0}; } } // namespace platf::mouse diff --git a/src/platform/linux/input/inputtino_mouse.h b/src/platform/linux/input/inputtino_mouse.h index 06c55e87f91..67eaf97af6b 100644 --- a/src/platform/linux/input/inputtino_mouse.h +++ b/src/platform/linux/input/inputtino_mouse.h @@ -3,33 +3,27 @@ * @brief Declarations for inputtino mouse input handling. */ #pragma once - +// lib includes #include #include #include -#include "src/platform/common.h" - +// local includes #include "inputtino_common.h" +#include "src/platform/common.h" using namespace std::literals; namespace platf::mouse { - void - move(input_raw_t *raw, int deltaX, int deltaY); + void move(input_raw_t *raw, int deltaX, int deltaY); - void - move_abs(input_raw_t *raw, const touch_port_t &touch_port, float x, float y); + void move_abs(input_raw_t *raw, const touch_port_t &touch_port, float x, float y); - void - button(input_raw_t *raw, int button, bool release); + void button(input_raw_t *raw, int button, bool release); - void - scroll(input_raw_t *raw, int high_res_distance); + void scroll(input_raw_t *raw, int high_res_distance); - void - hscroll(input_raw_t *raw, int high_res_distance); + void hscroll(input_raw_t *raw, int high_res_distance); - util::point_t - get_location(input_raw_t *raw); + util::point_t get_location(input_raw_t *raw); } // namespace platf::mouse diff --git a/src/platform/linux/input/inputtino_pen.cpp b/src/platform/linux/input/inputtino_pen.cpp index 1e171382851..ed10f4071a1 100644 --- a/src/platform/linux/input/inputtino_pen.cpp +++ b/src/platform/linux/input/inputtino_pen.cpp @@ -2,23 +2,23 @@ * @file src/platform/linux/input/inputtino_pen.cpp * @brief Definitions for inputtino pen input handling. */ +// lib includes #include #include #include +// local includes +#include "inputtino_common.h" +#include "inputtino_pen.h" #include "src/config.h" #include "src/logging.h" #include "src/platform/common.h" #include "src/utility.h" -#include "inputtino_common.h" -#include "inputtino_pen.h" - using namespace std::literals; namespace platf::pen { - void - update(client_input_raw_t *raw, const touch_port_t &touch_port, const pen_input_t &pen) { + void update(client_input_raw_t *raw, const touch_port_t &touch_port, const pen_input_t &pen) { if (raw->pen) { // First set the buttons (*raw->pen).set_btn(inputtino::PenTablet::PRIMARY, pen.penButtons & LI_PEN_BUTTON_PRIMARY); @@ -63,13 +63,7 @@ namespace platf::pen { bool is_touching = pen.eventType == LI_TOUCH_EVENT_DOWN || pen.eventType == LI_TOUCH_EVENT_MOVE; - (*raw->pen).place_tool(tool, - pen.x, - pen.y, - is_touching ? pen.pressureOrDistance : -1, - is_touching ? -1 : pen.pressureOrDistance, - tilt_x, - tilt_y); + (*raw->pen).place_tool(tool, pen.x, pen.y, is_touching ? pen.pressureOrDistance : -1, is_touching ? -1 : pen.pressureOrDistance, tilt_x, tilt_y); } } } // namespace platf::pen diff --git a/src/platform/linux/input/inputtino_pen.h b/src/platform/linux/input/inputtino_pen.h index 69bb1773e97..667f4ad8d58 100644 --- a/src/platform/linux/input/inputtino_pen.h +++ b/src/platform/linux/input/inputtino_pen.h @@ -4,17 +4,17 @@ */ #pragma once +// lib includes #include #include #include -#include "src/platform/common.h" - +// local includes #include "inputtino_common.h" +#include "src/platform/common.h" using namespace std::literals; namespace platf::pen { - void - update(client_input_raw_t *raw, const touch_port_t &touch_port, const pen_input_t &pen); + void update(client_input_raw_t *raw, const touch_port_t &touch_port, const pen_input_t &pen); } diff --git a/src/platform/linux/input/inputtino_touch.cpp b/src/platform/linux/input/inputtino_touch.cpp index 5a1f4c5de12..c7dce6044d9 100644 --- a/src/platform/linux/input/inputtino_touch.cpp +++ b/src/platform/linux/input/inputtino_touch.cpp @@ -2,52 +2,53 @@ * @file src/platform/linux/input/inputtino_touch.cpp * @brief Definitions for inputtino touch input handling. */ +// lib includes #include #include #include +// local includes +#include "inputtino_common.h" +#include "inputtino_touch.h" #include "src/config.h" #include "src/logging.h" #include "src/platform/common.h" #include "src/utility.h" -#include "inputtino_common.h" -#include "inputtino_touch.h" - using namespace std::literals; namespace platf::touch { - void - update(client_input_raw_t *raw, const touch_port_t &touch_port, const touch_input_t &touch) { + void update(client_input_raw_t *raw, const touch_port_t &touch_port, const touch_input_t &touch) { if (raw->touch) { switch (touch.eventType) { case LI_TOUCH_EVENT_HOVER: case LI_TOUCH_EVENT_DOWN: - case LI_TOUCH_EVENT_MOVE: { - // Convert our 0..360 range to -90..90 relative to Y axis - int adjusted_angle = touch.rotation; + case LI_TOUCH_EVENT_MOVE: + { + // Convert our 0..360 range to -90..90 relative to Y axis + int adjusted_angle = touch.rotation; - if (adjusted_angle > 90 && adjusted_angle < 270) { - // Lower hemisphere - adjusted_angle = 180 - adjusted_angle; - } + if (adjusted_angle > 90 && adjusted_angle < 270) { + // Lower hemisphere + adjusted_angle = 180 - adjusted_angle; + } - // Wrap the value if it's out of range - if (adjusted_angle > 90) { - adjusted_angle -= 360; + // Wrap the value if it's out of range + if (adjusted_angle > 90) { + adjusted_angle -= 360; + } else if (adjusted_angle < -90) { + adjusted_angle += 360; + } + (*raw->touch).place_finger(touch.pointerId, touch.x, touch.y, touch.pressureOrDistance, adjusted_angle); + break; } - else if (adjusted_angle < -90) { - adjusted_angle += 360; - } - (*raw->touch).place_finger(touch.pointerId, touch.x, touch.y, touch.pressureOrDistance, adjusted_angle); - break; - } case LI_TOUCH_EVENT_CANCEL: case LI_TOUCH_EVENT_UP: - case LI_TOUCH_EVENT_HOVER_LEAVE: { - (*raw->touch).release_finger(touch.pointerId); - break; - } + case LI_TOUCH_EVENT_HOVER_LEAVE: + { + (*raw->touch).release_finger(touch.pointerId); + break; + } // TODO: LI_TOUCH_EVENT_CANCEL_ALL } } diff --git a/src/platform/linux/input/inputtino_touch.h b/src/platform/linux/input/inputtino_touch.h index 250ba1ec312..5e1d6581616 100644 --- a/src/platform/linux/input/inputtino_touch.h +++ b/src/platform/linux/input/inputtino_touch.h @@ -4,17 +4,17 @@ */ #pragma once +// lib includes #include #include #include -#include "src/platform/common.h" - +// local includes #include "inputtino_common.h" +#include "src/platform/common.h" using namespace std::literals; namespace platf::touch { - void - update(client_input_raw_t *raw, const touch_port_t &touch_port, const touch_input_t &touch); + void update(client_input_raw_t *raw, const touch_port_t &touch_port, const touch_input_t &touch); } diff --git a/src/platform/linux/input/legacy_input.cpp b/src/platform/linux/input/legacy_input.cpp deleted file mode 100644 index 3d894d47390..00000000000 --- a/src/platform/linux/input/legacy_input.cpp +++ /dev/null @@ -1,2571 +0,0 @@ -/** - * @file src/platform/linux/input/legacy_input.cpp - * @brief Implementation of input handling, prior to migration to inputtino - * @todo Remove this file after the next stable release - */ -#include -#include -#include - -extern "C" { -#include -#include -} - -#ifdef SUNSHINE_BUILD_X11 - #include - #include - #include - #include -#endif - -#include -#include -#include -#include -#include - -#include "src/config.h" -#include "src/input.h" -#include "src/logging.h" -#include "src/platform/common.h" -#include "src/utility.h" - -#include "src/platform/common.h" - -#include "src/platform/linux/misc.h" - -// Support older versions -#ifndef REL_HWHEEL_HI_RES - #define REL_HWHEEL_HI_RES 0x0c -#endif - -#ifndef REL_WHEEL_HI_RES - #define REL_WHEEL_HI_RES 0x0b -#endif - -using namespace std::literals; - -namespace platf { - static bool has_uinput = false; - -#ifdef SUNSHINE_BUILD_X11 - namespace x11 { - #define _FN(x, ret, args) \ - typedef ret(*x##_fn) args; \ - static x##_fn x - - _FN(OpenDisplay, Display *, (_Xconst char *display_name)); - _FN(CloseDisplay, int, (Display * display)); - _FN(InitThreads, Status, (void) ); - _FN(Flush, int, (Display *) ); - - namespace tst { - _FN(FakeMotionEvent, int, (Display * dpy, int screen_numer, int x, int y, unsigned long delay)); - _FN(FakeRelativeMotionEvent, int, (Display * dpy, int deltaX, int deltaY, unsigned long delay)); - _FN(FakeButtonEvent, int, (Display * dpy, unsigned int button, Bool is_press, unsigned long delay)); - _FN(FakeKeyEvent, int, (Display * dpy, unsigned int keycode, Bool is_press, unsigned long delay)); - - static int - init() { - static void *handle { nullptr }; - static bool funcs_loaded = false; - - if (funcs_loaded) return 0; - - if (!handle) { - handle = dyn::handle({ "libXtst.so.6", "libXtst.so" }); - if (!handle) { - return -1; - } - } - - std::vector> funcs { - { (dyn::apiproc *) &FakeMotionEvent, "XTestFakeMotionEvent" }, - { (dyn::apiproc *) &FakeRelativeMotionEvent, "XTestFakeRelativeMotionEvent" }, - { (dyn::apiproc *) &FakeButtonEvent, "XTestFakeButtonEvent" }, - { (dyn::apiproc *) &FakeKeyEvent, "XTestFakeKeyEvent" }, - }; - - if (dyn::load(handle, funcs)) { - return -1; - } - - funcs_loaded = true; - return 0; - } - } // namespace tst - - static int - init() { - static void *handle { nullptr }; - static bool funcs_loaded = false; - - if (funcs_loaded) return 0; - - if (!handle) { - handle = dyn::handle({ "libX11.so.6", "libX11.so" }); - if (!handle) { - return -1; - } - } - - std::vector> funcs { - { (dyn::apiproc *) &OpenDisplay, "XOpenDisplay" }, - { (dyn::apiproc *) &CloseDisplay, "XCloseDisplay" }, - { (dyn::apiproc *) &InitThreads, "XInitThreads" }, - { (dyn::apiproc *) &Flush, "XFlush" }, - }; - - if (dyn::load(handle, funcs)) { - return -1; - } - - funcs_loaded = true; - return 0; - } - } // namespace x11 -#endif - - constexpr auto mail_evdev = "platf::evdev"sv; - - using evdev_t = util::safe_ptr; - using uinput_t = util::safe_ptr; - - constexpr pollfd read_pollfd { -1, 0, 0 }; - KITTY_USING_MOVE_T(pollfd_t, pollfd, read_pollfd, { - if (el.fd >= 0) { - ioctl(el.fd, EVIOCGRAB, (void *) 0); - - close(el.fd); - } - }); - - using mail_evdev_t = std::tuple; - - struct keycode_t { - std::uint32_t keycode; - std::uint32_t scancode; - -#ifdef SUNSHINE_BUILD_X11 - KeySym keysym; -#endif - }; - - constexpr auto UNKNOWN = 0; - - /** - * @brief Initializes the keycode constants for translating - * moonlight keycodes to linux/X11 keycodes. - */ - static constexpr std::array - init_keycodes() { - std::array keycodes {}; - -#ifdef SUNSHINE_BUILD_X11 - #define __CONVERT_UNSAFE(wincode, linuxcode, scancode, keysym) \ - keycodes[wincode] = keycode_t { linuxcode, scancode, keysym }; -#else - #define __CONVERT_UNSAFE(wincode, linuxcode, scancode, keysym) \ - keycodes[wincode] = keycode_t { linuxcode, scancode }; -#endif - -#define __CONVERT(wincode, linuxcode, scancode, keysym) \ - static_assert(wincode < keycodes.size(), "Keycode doesn't fit into keycode array"); \ - static_assert(wincode >= 0, "Are you mad?, keycode needs to be greater than zero"); \ - __CONVERT_UNSAFE(wincode, linuxcode, scancode, keysym) - - __CONVERT(0x08 /* VKEY_BACK */, KEY_BACKSPACE, 0x7002A, XK_BackSpace); - __CONVERT(0x09 /* VKEY_TAB */, KEY_TAB, 0x7002B, XK_Tab); - __CONVERT(0x0C /* VKEY_CLEAR */, KEY_CLEAR, UNKNOWN, XK_Clear); - __CONVERT(0x0D /* VKEY_RETURN */, KEY_ENTER, 0x70028, XK_Return); - __CONVERT(0x10 /* VKEY_SHIFT */, KEY_LEFTSHIFT, 0x700E1, XK_Shift_L); - __CONVERT(0x11 /* VKEY_CONTROL */, KEY_LEFTCTRL, 0x700E0, XK_Control_L); - __CONVERT(0x12 /* VKEY_MENU */, KEY_LEFTALT, UNKNOWN, XK_Alt_L); - __CONVERT(0x13 /* VKEY_PAUSE */, KEY_PAUSE, UNKNOWN, XK_Pause); - __CONVERT(0x14 /* VKEY_CAPITAL */, KEY_CAPSLOCK, 0x70039, XK_Caps_Lock); - __CONVERT(0x15 /* VKEY_KANA */, KEY_KATAKANAHIRAGANA, UNKNOWN, XK_Kana_Shift); - __CONVERT(0x16 /* VKEY_HANGUL */, KEY_HANGEUL, UNKNOWN, XK_Hangul); - __CONVERT(0x17 /* VKEY_JUNJA */, KEY_HANJA, UNKNOWN, XK_Hangul_Jeonja); - __CONVERT(0x19 /* VKEY_KANJI */, KEY_KATAKANA, UNKNOWN, XK_Kanji); - __CONVERT(0x1B /* VKEY_ESCAPE */, KEY_ESC, 0x70029, XK_Escape); - __CONVERT(0x20 /* VKEY_SPACE */, KEY_SPACE, 0x7002C, XK_space); - __CONVERT(0x21 /* VKEY_PRIOR */, KEY_PAGEUP, 0x7004B, XK_Page_Up); - __CONVERT(0x22 /* VKEY_NEXT */, KEY_PAGEDOWN, 0x7004E, XK_Page_Down); - __CONVERT(0x23 /* VKEY_END */, KEY_END, 0x7004D, XK_End); - __CONVERT(0x24 /* VKEY_HOME */, KEY_HOME, 0x7004A, XK_Home); - __CONVERT(0x25 /* VKEY_LEFT */, KEY_LEFT, 0x70050, XK_Left); - __CONVERT(0x26 /* VKEY_UP */, KEY_UP, 0x70052, XK_Up); - __CONVERT(0x27 /* VKEY_RIGHT */, KEY_RIGHT, 0x7004F, XK_Right); - __CONVERT(0x28 /* VKEY_DOWN */, KEY_DOWN, 0x70051, XK_Down); - __CONVERT(0x29 /* VKEY_SELECT */, KEY_SELECT, UNKNOWN, XK_Select); - __CONVERT(0x2A /* VKEY_PRINT */, KEY_PRINT, UNKNOWN, XK_Print); - __CONVERT(0x2C /* VKEY_SNAPSHOT */, KEY_SYSRQ, 0x70046, XK_Sys_Req); - __CONVERT(0x2D /* VKEY_INSERT */, KEY_INSERT, 0x70049, XK_Insert); - __CONVERT(0x2E /* VKEY_DELETE */, KEY_DELETE, 0x7004C, XK_Delete); - __CONVERT(0x2F /* VKEY_HELP */, KEY_HELP, UNKNOWN, XK_Help); - __CONVERT(0x30 /* VKEY_0 */, KEY_0, 0x70027, XK_0); - __CONVERT(0x31 /* VKEY_1 */, KEY_1, 0x7001E, XK_1); - __CONVERT(0x32 /* VKEY_2 */, KEY_2, 0x7001F, XK_2); - __CONVERT(0x33 /* VKEY_3 */, KEY_3, 0x70020, XK_3); - __CONVERT(0x34 /* VKEY_4 */, KEY_4, 0x70021, XK_4); - __CONVERT(0x35 /* VKEY_5 */, KEY_5, 0x70022, XK_5); - __CONVERT(0x36 /* VKEY_6 */, KEY_6, 0x70023, XK_6); - __CONVERT(0x37 /* VKEY_7 */, KEY_7, 0x70024, XK_7); - __CONVERT(0x38 /* VKEY_8 */, KEY_8, 0x70025, XK_8); - __CONVERT(0x39 /* VKEY_9 */, KEY_9, 0x70026, XK_9); - __CONVERT(0x41 /* VKEY_A */, KEY_A, 0x70004, XK_A); - __CONVERT(0x42 /* VKEY_B */, KEY_B, 0x70005, XK_B); - __CONVERT(0x43 /* VKEY_C */, KEY_C, 0x70006, XK_C); - __CONVERT(0x44 /* VKEY_D */, KEY_D, 0x70007, XK_D); - __CONVERT(0x45 /* VKEY_E */, KEY_E, 0x70008, XK_E); - __CONVERT(0x46 /* VKEY_F */, KEY_F, 0x70009, XK_F); - __CONVERT(0x47 /* VKEY_G */, KEY_G, 0x7000A, XK_G); - __CONVERT(0x48 /* VKEY_H */, KEY_H, 0x7000B, XK_H); - __CONVERT(0x49 /* VKEY_I */, KEY_I, 0x7000C, XK_I); - __CONVERT(0x4A /* VKEY_J */, KEY_J, 0x7000D, XK_J); - __CONVERT(0x4B /* VKEY_K */, KEY_K, 0x7000E, XK_K); - __CONVERT(0x4C /* VKEY_L */, KEY_L, 0x7000F, XK_L); - __CONVERT(0x4D /* VKEY_M */, KEY_M, 0x70010, XK_M); - __CONVERT(0x4E /* VKEY_N */, KEY_N, 0x70011, XK_N); - __CONVERT(0x4F /* VKEY_O */, KEY_O, 0x70012, XK_O); - __CONVERT(0x50 /* VKEY_P */, KEY_P, 0x70013, XK_P); - __CONVERT(0x51 /* VKEY_Q */, KEY_Q, 0x70014, XK_Q); - __CONVERT(0x52 /* VKEY_R */, KEY_R, 0x70015, XK_R); - __CONVERT(0x53 /* VKEY_S */, KEY_S, 0x70016, XK_S); - __CONVERT(0x54 /* VKEY_T */, KEY_T, 0x70017, XK_T); - __CONVERT(0x55 /* VKEY_U */, KEY_U, 0x70018, XK_U); - __CONVERT(0x56 /* VKEY_V */, KEY_V, 0x70019, XK_V); - __CONVERT(0x57 /* VKEY_W */, KEY_W, 0x7001A, XK_W); - __CONVERT(0x58 /* VKEY_X */, KEY_X, 0x7001B, XK_X); - __CONVERT(0x59 /* VKEY_Y */, KEY_Y, 0x7001C, XK_Y); - __CONVERT(0x5A /* VKEY_Z */, KEY_Z, 0x7001D, XK_Z); - __CONVERT(0x5B /* VKEY_LWIN */, KEY_LEFTMETA, 0x700E3, XK_Meta_L); - __CONVERT(0x5C /* VKEY_RWIN */, KEY_RIGHTMETA, 0x700E7, XK_Meta_R); - __CONVERT(0x5F /* VKEY_SLEEP */, KEY_SLEEP, UNKNOWN, UNKNOWN); - __CONVERT(0x60 /* VKEY_NUMPAD0 */, KEY_KP0, 0x70062, XK_KP_0); - __CONVERT(0x61 /* VKEY_NUMPAD1 */, KEY_KP1, 0x70059, XK_KP_1); - __CONVERT(0x62 /* VKEY_NUMPAD2 */, KEY_KP2, 0x7005A, XK_KP_2); - __CONVERT(0x63 /* VKEY_NUMPAD3 */, KEY_KP3, 0x7005B, XK_KP_3); - __CONVERT(0x64 /* VKEY_NUMPAD4 */, KEY_KP4, 0x7005C, XK_KP_4); - __CONVERT(0x65 /* VKEY_NUMPAD5 */, KEY_KP5, 0x7005D, XK_KP_5); - __CONVERT(0x66 /* VKEY_NUMPAD6 */, KEY_KP6, 0x7005E, XK_KP_6); - __CONVERT(0x67 /* VKEY_NUMPAD7 */, KEY_KP7, 0x7005F, XK_KP_7); - __CONVERT(0x68 /* VKEY_NUMPAD8 */, KEY_KP8, 0x70060, XK_KP_8); - __CONVERT(0x69 /* VKEY_NUMPAD9 */, KEY_KP9, 0x70061, XK_KP_9); - __CONVERT(0x6A /* VKEY_MULTIPLY */, KEY_KPASTERISK, 0x70055, XK_KP_Multiply); - __CONVERT(0x6B /* VKEY_ADD */, KEY_KPPLUS, 0x70057, XK_KP_Add); - __CONVERT(0x6C /* VKEY_SEPARATOR */, KEY_KPCOMMA, UNKNOWN, XK_KP_Separator); - __CONVERT(0x6D /* VKEY_SUBTRACT */, KEY_KPMINUS, 0x70056, XK_KP_Subtract); - __CONVERT(0x6E /* VKEY_DECIMAL */, KEY_KPDOT, 0x70063, XK_KP_Decimal); - __CONVERT(0x6F /* VKEY_DIVIDE */, KEY_KPSLASH, 0x70054, XK_KP_Divide); - __CONVERT(0x70 /* VKEY_F1 */, KEY_F1, 0x70046, XK_F1); - __CONVERT(0x71 /* VKEY_F2 */, KEY_F2, 0x70047, XK_F2); - __CONVERT(0x72 /* VKEY_F3 */, KEY_F3, 0x70048, XK_F3); - __CONVERT(0x73 /* VKEY_F4 */, KEY_F4, 0x70049, XK_F4); - __CONVERT(0x74 /* VKEY_F5 */, KEY_F5, 0x7004a, XK_F5); - __CONVERT(0x75 /* VKEY_F6 */, KEY_F6, 0x7004b, XK_F6); - __CONVERT(0x76 /* VKEY_F7 */, KEY_F7, 0x7004c, XK_F7); - __CONVERT(0x77 /* VKEY_F8 */, KEY_F8, 0x7004d, XK_F8); - __CONVERT(0x78 /* VKEY_F9 */, KEY_F9, 0x7004e, XK_F9); - __CONVERT(0x79 /* VKEY_F10 */, KEY_F10, 0x70044, XK_F10); - __CONVERT(0x7A /* VKEY_F11 */, KEY_F11, 0x70044, XK_F11); - __CONVERT(0x7B /* VKEY_F12 */, KEY_F12, 0x70045, XK_F12); - __CONVERT(0x7C /* VKEY_F13 */, KEY_F13, 0x7003a, XK_F13); - __CONVERT(0x7D /* VKEY_F14 */, KEY_F14, 0x7003b, XK_F14); - __CONVERT(0x7E /* VKEY_F15 */, KEY_F15, 0x7003c, XK_F15); - __CONVERT(0x7F /* VKEY_F16 */, KEY_F16, 0x7003d, XK_F16); - __CONVERT(0x80 /* VKEY_F17 */, KEY_F17, 0x7003e, XK_F17); - __CONVERT(0x81 /* VKEY_F18 */, KEY_F18, 0x7003f, XK_F18); - __CONVERT(0x82 /* VKEY_F19 */, KEY_F19, 0x70040, XK_F19); - __CONVERT(0x83 /* VKEY_F20 */, KEY_F20, 0x70041, XK_F20); - __CONVERT(0x84 /* VKEY_F21 */, KEY_F21, 0x70042, XK_F21); - __CONVERT(0x85 /* VKEY_F22 */, KEY_F12, 0x70043, XK_F12); - __CONVERT(0x86 /* VKEY_F23 */, KEY_F23, 0x70044, XK_F23); - __CONVERT(0x87 /* VKEY_F24 */, KEY_F24, 0x70045, XK_F24); - __CONVERT(0x90 /* VKEY_NUMLOCK */, KEY_NUMLOCK, 0x70053, XK_Num_Lock); - __CONVERT(0x91 /* VKEY_SCROLL */, KEY_SCROLLLOCK, 0x70047, XK_Scroll_Lock); - __CONVERT(0xA0 /* VKEY_LSHIFT */, KEY_LEFTSHIFT, 0x700E1, XK_Shift_L); - __CONVERT(0xA1 /* VKEY_RSHIFT */, KEY_RIGHTSHIFT, 0x700E5, XK_Shift_R); - __CONVERT(0xA2 /* VKEY_LCONTROL */, KEY_LEFTCTRL, 0x700E0, XK_Control_L); - __CONVERT(0xA3 /* VKEY_RCONTROL */, KEY_RIGHTCTRL, 0x700E4, XK_Control_R); - __CONVERT(0xA4 /* VKEY_LMENU */, KEY_LEFTALT, 0x7002E, XK_Alt_L); - __CONVERT(0xA5 /* VKEY_RMENU */, KEY_RIGHTALT, 0x700E6, XK_Alt_R); - __CONVERT(0xBA /* VKEY_OEM_1 */, KEY_SEMICOLON, 0x70033, XK_semicolon); - __CONVERT(0xBB /* VKEY_OEM_PLUS */, KEY_EQUAL, 0x7002E, XK_equal); - __CONVERT(0xBC /* VKEY_OEM_COMMA */, KEY_COMMA, 0x70036, XK_comma); - __CONVERT(0xBD /* VKEY_OEM_MINUS */, KEY_MINUS, 0x7002D, XK_minus); - __CONVERT(0xBE /* VKEY_OEM_PERIOD */, KEY_DOT, 0x70037, XK_period); - __CONVERT(0xBF /* VKEY_OEM_2 */, KEY_SLASH, 0x70038, XK_slash); - __CONVERT(0xC0 /* VKEY_OEM_3 */, KEY_GRAVE, 0x70035, XK_grave); - __CONVERT(0xDB /* VKEY_OEM_4 */, KEY_LEFTBRACE, 0x7002F, XK_braceleft); - __CONVERT(0xDC /* VKEY_OEM_5 */, KEY_BACKSLASH, 0x70031, XK_backslash); - __CONVERT(0xDD /* VKEY_OEM_6 */, KEY_RIGHTBRACE, 0x70030, XK_braceright); - __CONVERT(0xDE /* VKEY_OEM_7 */, KEY_APOSTROPHE, 0x70034, XK_apostrophe); - __CONVERT(0xE2 /* VKEY_NON_US_BACKSLASH */, KEY_102ND, 0x70064, XK_backslash); -#undef __CONVERT -#undef __CONVERT_UNSAFE - - return keycodes; - } - - static constexpr auto keycodes = init_keycodes(); - - constexpr touch_port_t target_touch_port { - 0, 0, - 19200, 12000 - }; - - static std::pair - operator*(const std::pair &l, int r) { - return { - l.first * r, - l.second * r, - }; - } - - static std::pair - operator/(const std::pair &l, int r) { - return { - l.first / r, - l.second / r, - }; - } - - static std::pair & - operator+=(std::pair &l, const std::pair &r) { - l.first += r.first; - l.second += r.second; - - return l; - } - - static inline void - print(const ff_envelope &envelope) { - BOOST_LOG(debug) - << "Envelope:"sv << std::endl - << " attack_length: " << envelope.attack_length << std::endl - << " attack_level: " << envelope.attack_level << std::endl - << " fade_length: " << envelope.fade_length << std::endl - << " fade_level: " << envelope.fade_level; - } - - static inline void - print(const ff_replay &replay) { - BOOST_LOG(debug) - << "Replay:"sv << std::endl - << " length: "sv << replay.length << std::endl - << " delay: "sv << replay.delay; - } - - static inline void - print(const ff_trigger &trigger) { - BOOST_LOG(debug) - << "Trigger:"sv << std::endl - << " button: "sv << trigger.button << std::endl - << " interval: "sv << trigger.interval; - } - - static inline void - print(const ff_effect &effect) { - BOOST_LOG(debug) - << std::endl - << std::endl - << "Received rumble effect with id: ["sv << effect.id << ']'; - - switch (effect.type) { - case FF_CONSTANT: - BOOST_LOG(debug) - << "FF_CONSTANT:"sv << std::endl - << " direction: "sv << effect.direction << std::endl - << " level: "sv << effect.u.constant.level; - - print(effect.u.constant.envelope); - break; - - case FF_PERIODIC: - BOOST_LOG(debug) - << "FF_CONSTANT:"sv << std::endl - << " direction: "sv << effect.direction << std::endl - << " waveform: "sv << effect.u.periodic.waveform << std::endl - << " period: "sv << effect.u.periodic.period << std::endl - << " magnitude: "sv << effect.u.periodic.magnitude << std::endl - << " offset: "sv << effect.u.periodic.offset << std::endl - << " phase: "sv << effect.u.periodic.phase; - - print(effect.u.periodic.envelope); - break; - - case FF_RAMP: - BOOST_LOG(debug) - << "FF_RAMP:"sv << std::endl - << " direction: "sv << effect.direction << std::endl - << " start_level:" << effect.u.ramp.start_level << std::endl - << " end_level:" << effect.u.ramp.end_level; - - print(effect.u.ramp.envelope); - break; - - case FF_RUMBLE: - BOOST_LOG(debug) - << "FF_RUMBLE:" << std::endl - << " direction: "sv << effect.direction << std::endl - << " strong_magnitude: " << effect.u.rumble.strong_magnitude << std::endl - << " weak_magnitude: " << effect.u.rumble.weak_magnitude; - break; - - case FF_SPRING: - BOOST_LOG(debug) - << "FF_SPRING:" << std::endl - << " direction: "sv << effect.direction; - break; - - case FF_FRICTION: - BOOST_LOG(debug) - << "FF_FRICTION:" << std::endl - << " direction: "sv << effect.direction; - break; - - case FF_DAMPER: - BOOST_LOG(debug) - << "FF_DAMPER:" << std::endl - << " direction: "sv << effect.direction; - break; - - case FF_INERTIA: - BOOST_LOG(debug) - << "FF_INERTIA:" << std::endl - << " direction: "sv << effect.direction; - break; - - case FF_CUSTOM: - BOOST_LOG(debug) - << "FF_CUSTOM:" << std::endl - << " direction: "sv << effect.direction; - break; - - default: - BOOST_LOG(debug) - << "FF_UNKNOWN:" << std::endl - << " direction: "sv << effect.direction; - break; - } - - print(effect.replay); - print(effect.trigger); - } - - // Emulate rumble effects - class effect_t { - public: - KITTY_DEFAULT_CONSTR_MOVE(effect_t) - - effect_t(std::uint8_t gamepadnr, uinput_t::pointer dev, feedback_queue_t &&q): - gamepadnr { gamepadnr }, dev { dev }, rumble_queue { std::move(q) }, gain { 0xFFFF }, id_to_data {} {} - - class data_t { - public: - KITTY_DEFAULT_CONSTR(data_t) - - data_t(const ff_effect &effect): - delay { effect.replay.delay }, - length { effect.replay.length }, - end_point { std::chrono::steady_clock::time_point::min() }, - envelope {}, - start {}, - end {} { - switch (effect.type) { - case FF_CONSTANT: - start.weak = effect.u.constant.level; - start.strong = effect.u.constant.level; - end.weak = effect.u.constant.level; - end.strong = effect.u.constant.level; - - envelope = effect.u.constant.envelope; - break; - case FF_PERIODIC: - start.weak = effect.u.periodic.magnitude; - start.strong = effect.u.periodic.magnitude; - end.weak = effect.u.periodic.magnitude; - end.strong = effect.u.periodic.magnitude; - - envelope = effect.u.periodic.envelope; - break; - - case FF_RAMP: - start.weak = effect.u.ramp.start_level; - start.strong = effect.u.ramp.start_level; - end.weak = effect.u.ramp.end_level; - end.strong = effect.u.ramp.end_level; - - envelope = effect.u.ramp.envelope; - break; - - case FF_RUMBLE: - start.weak = effect.u.rumble.weak_magnitude; - start.strong = effect.u.rumble.strong_magnitude; - end.weak = effect.u.rumble.weak_magnitude; - end.strong = effect.u.rumble.strong_magnitude; - break; - - default: - BOOST_LOG(warning) << "Effect type ["sv << effect.id << "] not implemented"sv; - } - } - - std::uint32_t - magnitude(std::chrono::milliseconds time_left, std::uint32_t start, std::uint32_t end) { - auto rel = end - start; - - return start + (rel * time_left.count() / length.count()); - } - - std::pair - rumble(std::chrono::steady_clock::time_point tp) { - if (end_point < tp) { - return {}; - } - - auto time_left = - std::chrono::duration_cast( - end_point - tp); - - // If it needs to be delayed' - if (time_left > length) { - return {}; - } - - auto t = length - time_left; - - auto weak = magnitude(t, start.weak, end.weak); - auto strong = magnitude(t, start.strong, end.strong); - - if (t.count() < envelope.attack_length) { - weak = (envelope.attack_level * t.count() + weak * (envelope.attack_length - t.count())) / envelope.attack_length; - strong = (envelope.attack_level * t.count() + strong * (envelope.attack_length - t.count())) / envelope.attack_length; - } - else if (time_left.count() < envelope.fade_length) { - auto dt = (t - length).count() + envelope.fade_length; - - weak = (envelope.fade_level * dt + weak * (envelope.fade_length - dt)) / envelope.fade_length; - strong = (envelope.fade_level * dt + strong * (envelope.fade_length - dt)) / envelope.fade_length; - } - - return { - weak, strong - }; - } - - void - activate() { - end_point = std::chrono::steady_clock::now() + delay + length; - } - - void - deactivate() { - end_point = std::chrono::steady_clock::time_point::min(); - } - - std::chrono::milliseconds delay; - std::chrono::milliseconds length; - - std::chrono::steady_clock::time_point end_point; - - ff_envelope envelope; - struct { - std::uint32_t weak, strong; - } start; - - struct { - std::uint32_t weak, strong; - } end; - }; - - std::pair - rumble(std::chrono::steady_clock::time_point tp) { - std::pair weak_strong {}; - for (auto &[_, data] : id_to_data) { - weak_strong += data.rumble(tp); - } - - weak_strong.first = std::clamp(weak_strong.first, 0, 0xFFFF); - weak_strong.second = std::clamp(weak_strong.second, 0, 0xFFFF); - - old_rumble = weak_strong * gain / 0xFFFF; - return old_rumble; - } - - void - upload(const ff_effect &effect) { - print(effect); - - auto it = id_to_data.find(effect.id); - - if (it == std::end(id_to_data)) { - id_to_data.emplace(effect.id, effect); - return; - } - - data_t data { effect }; - - data.end_point = it->second.end_point; - it->second = data; - } - - void - activate(int id) { - auto it = id_to_data.find(id); - - if (it != std::end(id_to_data)) { - it->second.activate(); - } - } - - void - deactivate(int id) { - auto it = id_to_data.find(id); - - if (it != std::end(id_to_data)) { - it->second.deactivate(); - } - } - - void - erase(int id) { - id_to_data.erase(id); - BOOST_LOG(debug) << "Removed rumble effect id ["sv << id << ']'; - } - - // Client-relative gamepad index for rumble notifications - std::uint8_t gamepadnr; - - // Used as ID for adding/removinf devices from evdev notifications - uinput_t::pointer dev; - - feedback_queue_t rumble_queue; - - int gain; - - // No need to send rumble data when old values equals the new values - std::pair old_rumble; - - std::unordered_map id_to_data; - }; - - struct rumble_ctx_t { - std::thread rumble_thread; - - safe::queue_t rumble_queue_queue; - }; - - void - broadcastRumble(safe::queue_t &ctx); - int - startRumble(rumble_ctx_t &ctx) { - ctx.rumble_thread = std::thread { broadcastRumble, std::ref(ctx.rumble_queue_queue) }; - - return 0; - } - - void - stopRumble(rumble_ctx_t &ctx) { - ctx.rumble_queue_queue.stop(); - - BOOST_LOG(debug) << "Waiting for Gamepad notifications to stop..."sv; - ctx.rumble_thread.join(); - BOOST_LOG(debug) << "Gamepad notifications stopped"sv; - } - - static auto notifications = safe::make_shared(startRumble, stopRumble); - - struct input_raw_t { - public: - void - clear_mouse_rel() { - std::filesystem::path mouse_path { appdata() / "sunshine_mouse_rel"sv }; - - if (std::filesystem::is_symlink(mouse_path)) { - std::filesystem::remove(mouse_path); - } - - mouse_rel_input.reset(); - } - - void - clear_keyboard() { - std::filesystem::path key_path { appdata() / "sunshine_keyboard"sv }; - - if (std::filesystem::is_symlink(key_path)) { - std::filesystem::remove(key_path); - } - - keyboard_input.reset(); - } - - void - clear_mouse_abs() { - std::filesystem::path mouse_path { appdata() / "sunshine_mouse_abs"sv }; - - if (std::filesystem::is_symlink(mouse_path)) { - std::filesystem::remove(mouse_path); - } - - mouse_abs_input.reset(); - } - - void - clear_gamepad(int nr) { - auto &[dev, _] = gamepads[nr]; - - if (!dev) { - return; - } - - // Remove this gamepad from notifications - rumble_ctx->rumble_queue_queue.raise(nr, dev.get(), nullptr, pollfd_t {}); - - std::stringstream ss; - - ss << "sunshine_gamepad_"sv << nr; - - auto gamepad_path = platf::appdata() / ss.str(); - if (std::filesystem::is_symlink(gamepad_path)) { - std::filesystem::remove(gamepad_path); - } - - gamepads[nr] = std::make_pair(uinput_t {}, gamepad_state_t {}); - } - - int - create_mouse_abs() { - int err = libevdev_uinput_create_from_device(mouse_abs_dev.get(), LIBEVDEV_UINPUT_OPEN_MANAGED, &mouse_abs_input); - - if (err) { - BOOST_LOG(error) << "Could not create Sunshine Mouse (Absolute): "sv << strerror(-err); - return -1; - } - - std::filesystem::create_symlink(libevdev_uinput_get_devnode(mouse_abs_input.get()), appdata() / "sunshine_mouse_abs"sv); - - return 0; - } - - int - create_mouse_rel() { - int err = libevdev_uinput_create_from_device(mouse_rel_dev.get(), LIBEVDEV_UINPUT_OPEN_MANAGED, &mouse_rel_input); - - if (err) { - BOOST_LOG(error) << "Could not create Sunshine Mouse (Relative): "sv << strerror(-err); - return -1; - } - - std::filesystem::create_symlink(libevdev_uinput_get_devnode(mouse_rel_input.get()), appdata() / "sunshine_mouse_rel"sv); - - return 0; - } - - int - create_keyboard() { - int err = libevdev_uinput_create_from_device(keyboard_dev.get(), LIBEVDEV_UINPUT_OPEN_MANAGED, &keyboard_input); - - if (err) { - BOOST_LOG(error) << "Could not create Sunshine Keyboard: "sv << strerror(-err); - return -1; - } - - std::filesystem::create_symlink(libevdev_uinput_get_devnode(keyboard_input.get()), appdata() / "sunshine_keyboard"sv); - - return 0; - } - - int - alloc_gamepad(const gamepad_id_t &id, const gamepad_arrival_t &metadata, feedback_queue_t &&feedback_queue) { - TUPLE_2D_REF(input, gamepad_state, gamepads[id.globalIndex]); - - int err = libevdev_uinput_create_from_device(gamepad_dev.get(), LIBEVDEV_UINPUT_OPEN_MANAGED, &input); - - gamepad_state = gamepad_state_t {}; - - if (err) { - BOOST_LOG(error) << "Could not create Sunshine Gamepad: "sv << strerror(-err); - return -1; - } - - std::stringstream ss; - ss << "sunshine_gamepad_"sv << id.globalIndex; - auto gamepad_path = platf::appdata() / ss.str(); - - if (std::filesystem::is_symlink(gamepad_path)) { - std::filesystem::remove(gamepad_path); - } - - auto dev_node = libevdev_uinput_get_devnode(input.get()); - - rumble_ctx->rumble_queue_queue.raise( - id.clientRelativeIndex, - input.get(), - std::move(feedback_queue), - pollfd_t { - dup(libevdev_uinput_get_fd(input.get())), - (std::int16_t) POLLIN, - (std::int16_t) 0, - }); - - std::filesystem::create_symlink(dev_node, gamepad_path); - return 0; - } - - void - clear() { - clear_keyboard(); - clear_mouse_abs(); - clear_mouse_rel(); - for (int x = 0; x < gamepads.size(); ++x) { - clear_gamepad(x); - } - -#ifdef SUNSHINE_BUILD_X11 - if (display) { - x11::CloseDisplay(display); - display = nullptr; - } -#endif - } - - ~input_raw_t() { - clear(); - } - - safe::shared_t::ptr_t rumble_ctx; - - std::vector> gamepads; - uinput_t mouse_rel_input; - uinput_t mouse_abs_input; - uinput_t keyboard_input; - - uint8_t mouse_rel_buttons_down = 0; - uint8_t mouse_abs_buttons_down = 0; - - uinput_t::pointer last_mouse_device_used = nullptr; - uint8_t *last_mouse_device_buttons_down = nullptr; - - evdev_t gamepad_dev; - evdev_t mouse_rel_dev; - evdev_t mouse_abs_dev; - evdev_t keyboard_dev; - evdev_t touchscreen_dev; - evdev_t pen_dev; - - int accumulated_vscroll_delta = 0; - int accumulated_hscroll_delta = 0; - -#ifdef SUNSHINE_BUILD_X11 - Display *display; -#endif - }; - - inline void - rumbleIterate(std::vector &effects, std::vector &polls, std::chrono::milliseconds to) { - std::vector polls_recv; - polls_recv.reserve(polls.size()); - for (auto &poll : polls) { - polls_recv.emplace_back(poll.el); - } - - auto res = poll(polls_recv.data(), polls_recv.size(), to.count()); - - // If timed out - if (!res) { - return; - } - - if (res < 0) { - char err_str[1024]; - BOOST_LOG(error) << "Couldn't poll Gamepad file descriptors: "sv << strerror_r(errno, err_str, 1024); - - return; - } - - for (int x = 0; x < polls.size(); ++x) { - auto poll = std::begin(polls) + x; - auto effect_it = std::begin(effects) + x; - - auto fd = (*poll)->fd; - - // TUPLE_2D_REF(dev, q, *dev_q_it); - - // on error - if (polls_recv[x].revents & (POLLHUP | POLLRDHUP | POLLERR)) { - BOOST_LOG(warning) << "Gamepad ["sv << x << "] file descriptor closed unexpectedly"sv; - - polls.erase(poll); - effects.erase(effect_it); - - --x; - continue; - } - - if (!(polls_recv[x].revents & POLLIN)) { - continue; - } - - input_event events[64]; - - // Read all available events - auto bytes = read(fd, &events, sizeof(events)); - - if (bytes < 0) { - char err_str[1024]; - - BOOST_LOG(error) << "Couldn't read evdev input ["sv << errno << "]: "sv << strerror_r(errno, err_str, 1024); - - polls.erase(poll); - effects.erase(effect_it); - - --x; - continue; - } - - if (bytes < sizeof(input_event)) { - BOOST_LOG(warning) << "Reading evdev input: Expected at least "sv << sizeof(input_event) << " bytes, got "sv << bytes << " instead"sv; - continue; - } - - auto event_count = bytes / sizeof(input_event); - - for (auto event = events; event != (events + event_count); ++event) { - switch (event->type) { - case EV_FF: - // BOOST_LOG(debug) << "EV_FF: "sv << event->value << " aka "sv << util::hex(event->value).to_string_view(); - - if (event->code == FF_GAIN) { - BOOST_LOG(debug) << "EV_FF: code [FF_GAIN]: value: "sv << event->value << " aka "sv << util::hex(event->value).to_string_view(); - effect_it->gain = std::clamp(event->value, 0, 0xFFFF); - - break; - } - - BOOST_LOG(debug) << "EV_FF: id ["sv << event->code << "]: value: "sv << event->value << " aka "sv << util::hex(event->value).to_string_view(); - - if (event->value) { - effect_it->activate(event->code); - } - else { - effect_it->deactivate(event->code); - } - break; - case EV_UINPUT: - switch (event->code) { - case UI_FF_UPLOAD: { - uinput_ff_upload upload {}; - - // *VERY* important, without this you break - // the kernel and have to reboot due to dead - // hanging process - upload.request_id = event->value; - - ioctl(fd, UI_BEGIN_FF_UPLOAD, &upload); - auto fg = util::fail_guard([&]() { - upload.retval = 0; - ioctl(fd, UI_END_FF_UPLOAD, &upload); - }); - - effect_it->upload(upload.effect); - } break; - case UI_FF_ERASE: { - uinput_ff_erase erase {}; - - // *VERY* important, without this you break - // the kernel and have to reboot due to dead - // hanging process - erase.request_id = event->value; - - ioctl(fd, UI_BEGIN_FF_ERASE, &erase); - auto fg = util::fail_guard([&]() { - erase.retval = 0; - ioctl(fd, UI_END_FF_ERASE, &erase); - }); - - effect_it->erase(erase.effect_id); - } break; - } - break; - default: - BOOST_LOG(debug) - << util::hex(event->type).to_string_view() << ": "sv - << util::hex(event->code).to_string_view() << ": "sv - << event->value << " aka "sv << util::hex(event->value).to_string_view(); - } - } - } - } - - void - broadcastRumble(safe::queue_t &rumble_queue_queue) { - std::vector effects; - std::vector polls; - - while (rumble_queue_queue.running()) { - while (rumble_queue_queue.peek()) { - auto dev_rumble_queue = rumble_queue_queue.pop(); - - if (!dev_rumble_queue) { - // rumble_queue_queue is no longer running - return; - } - - auto gamepadnr = std::get<0>(*dev_rumble_queue); - auto dev = std::get<1>(*dev_rumble_queue); - auto &rumble_queue = std::get<2>(*dev_rumble_queue); - auto &pollfd = std::get<3>(*dev_rumble_queue); - - { - auto effect_it = std::find_if(std::begin(effects), std::end(effects), [dev](auto &curr_effect) { - return dev == curr_effect.dev; - }); - - if (effect_it != std::end(effects)) { - auto poll_it = std::begin(polls) + (effect_it - std::begin(effects)); - - polls.erase(poll_it); - effects.erase(effect_it); - - BOOST_LOG(debug) << "Removed Gamepad device from notifications"sv; - - continue; - } - - // There may be an attepmt to remove, that which not exists - if (!rumble_queue) { - BOOST_LOG(warning) << "Attempting to remove a gamepad device from notifications that isn't already registered"sv; - continue; - } - } - - polls.emplace_back(std::move(pollfd)); - effects.emplace_back(gamepadnr, dev, std::move(rumble_queue)); - - BOOST_LOG(debug) << "Added Gamepad device to notifications"sv; - } - - if (polls.empty()) { - std::this_thread::sleep_for(250ms); - } - else { - rumbleIterate(effects, polls, 100ms); - - auto now = std::chrono::steady_clock::now(); - for (auto &effect : effects) { - TUPLE_2D(old_weak, old_strong, effect.old_rumble); - TUPLE_2D(weak, strong, effect.rumble(now)); - - if (old_weak != weak || old_strong != strong) { - BOOST_LOG(debug) << "Sending haptic feedback: lowfreq [0x"sv << util::hex(strong).to_string_view() << "]: highfreq [0x"sv << util::hex(weak).to_string_view() << ']'; - - effect.rumble_queue->raise(gamepad_feedback_msg_t::make_rumble(effect.gamepadnr, strong, weak)); - } - } - } - } - } - - /** - * @brief XTest absolute mouse move. - * @param input The input_t instance to use. - * @param x Absolute x position. - * @param y Absolute y position. - * @examples - * x_abs_mouse(input, 0, 0); - * @examples_end - */ - static void - x_abs_mouse(input_t &input, float x, float y) { -#ifdef SUNSHINE_BUILD_X11 - Display *xdisplay = ((input_raw_t *) input.get())->display; - if (!xdisplay) { - return; - } - x11::tst::FakeMotionEvent(xdisplay, -1, x, y, CurrentTime); - x11::Flush(xdisplay); -#endif - } - - util::point_t - get_mouse_loc(input_t &input) { -#ifdef SUNSHINE_BUILD_X11 - Display *xdisplay = ((input_raw_t *) input.get())->display; - if (!xdisplay) { - return util::point_t {}; - } - Window root, root_return, child_return; - root = DefaultRootWindow(xdisplay); - int root_x, root_y; - int win_x, win_y; - unsigned int mask_return; - - if (XQueryPointer(xdisplay, root, &root_return, &child_return, &root_x, &root_y, &win_x, &win_y, &mask_return)) { - BOOST_LOG(debug) - << "Pointer is at:"sv << std::endl - << " x: " << root_x << std::endl - << " y: " << root_y << std::endl; - - return util::point_t { (double) root_x, (double) root_y }; - } - else { - BOOST_LOG(debug) << "Unable to query x11 pointer"sv << std::endl; - } -#else - BOOST_LOG(debug) << "Unable to query wayland pointer"sv << std::endl; -#endif - return util::point_t {}; - } - - /** - * @brief Absolute mouse move. - * @param input The input_t instance to use. - * @param touch_port The touch_port instance to use. - * @param x Absolute x position. - * @param y Absolute y position. - * @examples - * abs_mouse(input, touch_port, 0, 0); - * @examples_end - */ - void - abs_mouse(input_t &input, const touch_port_t &touch_port, float x, float y) { - auto raw = (input_raw_t *) input.get(); - auto mouse_abs = raw->mouse_abs_input.get(); - if (!mouse_abs) { - x_abs_mouse(input, x, y); - return; - } - - auto scaled_x = (int) std::lround((x + touch_port.offset_x) * ((float) target_touch_port.width / (float) touch_port.width)); - auto scaled_y = (int) std::lround((y + touch_port.offset_y) * ((float) target_touch_port.height / (float) touch_port.height)); - - libevdev_uinput_write_event(mouse_abs, EV_ABS, ABS_X, scaled_x); - libevdev_uinput_write_event(mouse_abs, EV_ABS, ABS_Y, scaled_y); - libevdev_uinput_write_event(mouse_abs, EV_SYN, SYN_REPORT, 0); - - // Remember this was the last device we sent input on - raw->last_mouse_device_used = mouse_abs; - raw->last_mouse_device_buttons_down = &raw->mouse_abs_buttons_down; - } - - /** - * @brief XTest relative mouse move. - * @param input The input_t instance to use. - * @param deltaX Relative x position. - * @param deltaY Relative y position. - * @examples - * x_move_mouse(input, 10, 10); // Move mouse 10 pixels down and right - * @examples_end - */ - static void - x_move_mouse(input_t &input, int deltaX, int deltaY) { -#ifdef SUNSHINE_BUILD_X11 - Display *xdisplay = ((input_raw_t *) input.get())->display; - if (!xdisplay) { - return; - } - x11::tst::FakeRelativeMotionEvent(xdisplay, deltaX, deltaY, CurrentTime); - x11::Flush(xdisplay); -#endif - } - - /** - * @brief Relative mouse move. - * @param input The input_t instance to use. - * @param deltaX Relative x position. - * @param deltaY Relative y position. - * @examples - * move_mouse(input, 10, 10); // Move mouse 10 pixels down and right - * @examples_end - */ - void - move_mouse(input_t &input, int deltaX, int deltaY) { - auto raw = (input_raw_t *) input.get(); - auto mouse_rel = raw->mouse_rel_input.get(); - if (!mouse_rel) { - x_move_mouse(input, deltaX, deltaY); - return; - } - - if (deltaX) { - libevdev_uinput_write_event(mouse_rel, EV_REL, REL_X, deltaX); - } - - if (deltaY) { - libevdev_uinput_write_event(mouse_rel, EV_REL, REL_Y, deltaY); - } - - libevdev_uinput_write_event(mouse_rel, EV_SYN, SYN_REPORT, 0); - - // Remember this was the last device we sent input on - raw->last_mouse_device_used = mouse_rel; - raw->last_mouse_device_buttons_down = &raw->mouse_rel_buttons_down; - } - - /** - * @brief XTest mouse button press/release. - * @param input The input_t instance to use. - * @param button Which mouse button to emulate. - * @param release Whether the event was a press (false) or a release (true) - * @examples - * x_button_mouse(input, 1, false); // Press left mouse button - * @examples_end - */ - static void - x_button_mouse(input_t &input, int button, bool release) { -#ifdef SUNSHINE_BUILD_X11 - unsigned int x_button = 0; - switch (button) { - case BUTTON_LEFT: - x_button = 1; - break; - case BUTTON_MIDDLE: - x_button = 2; - break; - case BUTTON_RIGHT: - x_button = 3; - break; - default: - x_button = (button - 4) + 8; // Button 4 (Moonlight) starts at index 8 (X11) - break; - } - - if (x_button < 1 || x_button > 31) { - return; - } - - Display *xdisplay = ((input_raw_t *) input.get())->display; - if (!xdisplay) { - return; - } - x11::tst::FakeButtonEvent(xdisplay, x_button, !release, CurrentTime); - x11::Flush(xdisplay); -#endif - } - - /** - * @brief Mouse button press/release. - * @param input The input_t instance to use. - * @param button Which mouse button to emulate. - * @param release Whether the event was a press (false) or a release (true) - * @examples - * button_mouse(input, 1, false); // Press left mouse button - * @examples_end - */ - void - button_mouse(input_t &input, int button, bool release) { - auto raw = (input_raw_t *) input.get(); - - // We mimic the Linux vmmouse driver here and prefer to send buttons - // on the last mouse device we used. However, we make an exception - // if it's a release event and the button is down on the other device. - uinput_t::pointer chosen_mouse_dev = nullptr; - uint8_t *chosen_mouse_dev_buttons_down = nullptr; - if (release) { - // Prefer to send the release on the mouse with the button down - if (raw->mouse_rel_buttons_down & (1 << button)) { - chosen_mouse_dev = raw->mouse_rel_input.get(); - chosen_mouse_dev_buttons_down = &raw->mouse_rel_buttons_down; - } - else if (raw->mouse_abs_buttons_down & (1 << button)) { - chosen_mouse_dev = raw->mouse_abs_input.get(); - chosen_mouse_dev_buttons_down = &raw->mouse_abs_buttons_down; - } - } - - if (!chosen_mouse_dev) { - if (raw->last_mouse_device_used) { - // Prefer to use the last device we sent motion - chosen_mouse_dev = raw->last_mouse_device_used; - chosen_mouse_dev_buttons_down = raw->last_mouse_device_buttons_down; - } - else { - // Send on the relative device if we have no preference yet - chosen_mouse_dev = raw->mouse_rel_input.get(); - chosen_mouse_dev_buttons_down = &raw->mouse_rel_buttons_down; - } - } - - if (!chosen_mouse_dev) { - x_button_mouse(input, button, release); - return; - } - - int btn_type; - int scan; - - if (button == 1) { - btn_type = BTN_LEFT; - scan = 90001; - } - else if (button == 2) { - btn_type = BTN_MIDDLE; - scan = 90003; - } - else if (button == 3) { - btn_type = BTN_RIGHT; - scan = 90002; - } - else if (button == 4) { - btn_type = BTN_SIDE; - scan = 90004; - } - else { - btn_type = BTN_EXTRA; - scan = 90005; - } - - libevdev_uinput_write_event(chosen_mouse_dev, EV_MSC, MSC_SCAN, scan); - libevdev_uinput_write_event(chosen_mouse_dev, EV_KEY, btn_type, release ? 0 : 1); - libevdev_uinput_write_event(chosen_mouse_dev, EV_SYN, SYN_REPORT, 0); - - if (release) { - *chosen_mouse_dev_buttons_down &= ~(1 << button); - } - else { - *chosen_mouse_dev_buttons_down |= (1 << button); - } - } - - /** - * @brief XTest mouse scroll. - * @param input The input_t instance to use. - * @param distance How far to scroll. - * @param button_pos Which mouse button to emulate for positive scroll. - * @param button_neg Which mouse button to emulate for negative scroll. - * @examples - * x_scroll(input, 10, 4, 5); - * @examples_end - */ - static void - x_scroll(input_t &input, int distance, int button_pos, int button_neg) { -#ifdef SUNSHINE_BUILD_X11 - Display *xdisplay = ((input_raw_t *) input.get())->display; - if (!xdisplay) { - return; - } - - const int button = distance > 0 ? button_pos : button_neg; - for (int i = 0; i < abs(distance); i++) { - x11::tst::FakeButtonEvent(xdisplay, button, true, CurrentTime); - x11::tst::FakeButtonEvent(xdisplay, button, false, CurrentTime); - } - x11::Flush(xdisplay); -#endif - } - - /** - * @brief Vertical mouse scroll. - * @param input The input_t instance to use. - * @param high_res_distance How far to scroll. - * @examples - * scroll(input, 1200); - * @examples_end - */ - void - scroll(input_t &input, int high_res_distance) { - auto raw = ((input_raw_t *) input.get()); - - raw->accumulated_vscroll_delta += high_res_distance; - int full_ticks = raw->accumulated_vscroll_delta / 120; - - // We mimic the Linux vmmouse driver and always send scroll events - // via the relative pointing device for Xorg compatibility. - auto mouse = raw->mouse_rel_input.get(); - if (mouse) { - if (full_ticks) { - libevdev_uinput_write_event(mouse, EV_REL, REL_WHEEL, full_ticks); - } - libevdev_uinput_write_event(mouse, EV_REL, REL_WHEEL_HI_RES, high_res_distance); - libevdev_uinput_write_event(mouse, EV_SYN, SYN_REPORT, 0); - } - else if (full_ticks) { - x_scroll(input, full_ticks, 4, 5); - } - - raw->accumulated_vscroll_delta -= full_ticks * 120; - } - - /** - * @brief Horizontal mouse scroll. - * @param input The input_t instance to use. - * @param high_res_distance How far to scroll. - * @examples - * hscroll(input, 1200); - * @examples_end - */ - void - hscroll(input_t &input, int high_res_distance) { - auto raw = ((input_raw_t *) input.get()); - - raw->accumulated_hscroll_delta += high_res_distance; - int full_ticks = raw->accumulated_hscroll_delta / 120; - - // We mimic the Linux vmmouse driver and always send scroll events - // via the relative pointing device for Xorg compatibility. - auto mouse_rel = raw->mouse_rel_input.get(); - if (mouse_rel) { - if (full_ticks) { - libevdev_uinput_write_event(mouse_rel, EV_REL, REL_HWHEEL, full_ticks); - } - libevdev_uinput_write_event(mouse_rel, EV_REL, REL_HWHEEL_HI_RES, high_res_distance); - libevdev_uinput_write_event(mouse_rel, EV_SYN, SYN_REPORT, 0); - } - else if (full_ticks) { - x_scroll(input, full_ticks, 6, 7); - } - - raw->accumulated_hscroll_delta -= full_ticks * 120; - } - - static keycode_t - keysym(std::uint16_t modcode) { - if (modcode <= keycodes.size()) { - return keycodes[modcode]; - } - - return {}; - } - - /** - * @brief XTest keyboard emulation. - * @param input The input_t instance to use. - * @param modcode The moonlight key code. - * @param release Whether the event was a press (false) or a release (true). - * @param flags SS_KBE_FLAG_* values. - * @examples - * x_keyboard(input, 0x5A, false, 0); // Press Z - * @examples_end - */ - static void - x_keyboard(input_t &input, uint16_t modcode, bool release, uint8_t flags) { -#ifdef SUNSHINE_BUILD_X11 - auto keycode = keysym(modcode); - if (keycode.keysym == UNKNOWN) { - return; - } - - Display *xdisplay = ((input_raw_t *) input.get())->display; - if (!xdisplay) { - return; - } - - const auto keycode_x = XKeysymToKeycode(xdisplay, keycode.keysym); - if (keycode_x == 0) { - return; - } - - x11::tst::FakeKeyEvent(xdisplay, keycode_x, !release, CurrentTime); - x11::Flush(xdisplay); -#endif - } - - /** - * @brief Keyboard emulation. - * @param input The input_t instance to use. - * @param modcode The moonlight key code. - * @param release Whether the event was a press (false) or a release (true). - * @param flags SS_KBE_FLAG_* values. - * @examples - * keyboard(input, 0x5A, false, 0); // Press Z - * @examples_end - */ - void - keyboard_update(input_t &input, uint16_t modcode, bool release, uint8_t flags) { - auto keyboard = ((input_raw_t *) input.get())->keyboard_input.get(); - if (!keyboard) { - x_keyboard(input, modcode, release, flags); - return; - } - - auto keycode = keysym(modcode); - if (keycode.keycode == UNKNOWN) { - return; - } - - if (keycode.scancode != UNKNOWN) { - libevdev_uinput_write_event(keyboard, EV_MSC, MSC_SCAN, keycode.scancode); - } - - libevdev_uinput_write_event(keyboard, EV_KEY, keycode.keycode, release ? 0 : 1); - libevdev_uinput_write_event(keyboard, EV_SYN, SYN_REPORT, 0); - } - - void - keyboard_ev(libevdev_uinput *keyboard, int linux_code, int event_code = 1) { - libevdev_uinput_write_event(keyboard, EV_KEY, linux_code, event_code); - libevdev_uinput_write_event(keyboard, EV_SYN, SYN_REPORT, 0); - } - - /** - * Takes an UTF-32 encoded string and returns a hex string representation of the bytes (uppercase) - * - * ex: ['👱'] = "1F471" // see UTF encoding at https://www.compart.com/en/unicode/U+1F471 - * - * adapted from: https://stackoverflow.com/a/7639754 - */ - std::string - to_hex(const std::basic_string &str) { - std::stringstream ss; - ss << std::hex << std::setfill('0'); - for (const auto &ch : str) { - ss << static_cast(ch); - } - - std::string hex_unicode(ss.str()); - std::transform(hex_unicode.begin(), hex_unicode.end(), hex_unicode.begin(), ::toupper); - return hex_unicode; - } - - /** - * Here we receive a single UTF-8 encoded char at a time, - * the trick is to convert it to UTF-32 then send CTRL+SHIFT+U+{HEXCODE} in order to produce any - * unicode character, see: https://en.wikipedia.org/wiki/Unicode_input - * - * ex: - * - when receiving UTF-8 [0xF0 0x9F 0x91 0xB1] (which is '👱') - * - we'll convert it to UTF-32 [0x1F471] - * - then type: CTRL+SHIFT+U+1F471 - * see the conversion at: https://www.compart.com/en/unicode/U+1F471 - */ - void - unicode(input_t &input, char *utf8, int size) { - auto kb = ((input_raw_t *) input.get())->keyboard_input.get(); - if (!kb) { - return; - } - - /* Reading input text as UTF-8 */ - auto utf8_str = boost::locale::conv::to_utf(utf8, utf8 + size, "UTF-8"); - /* Converting to UTF-32 */ - auto utf32_str = boost::locale::conv::utf_to_utf(utf8_str); - /* To HEX string */ - auto hex_unicode = to_hex(utf32_str); - BOOST_LOG(debug) << "Unicode, typing U+"sv << hex_unicode; - - /* pressing + + U */ - keyboard_ev(kb, KEY_LEFTCTRL, 1); - keyboard_ev(kb, KEY_LEFTSHIFT, 1); - keyboard_ev(kb, KEY_U, 1); - keyboard_ev(kb, KEY_U, 0); - - /* input each HEX character */ - for (auto &ch : hex_unicode) { - auto key_str = "KEY_"s + ch; - auto keycode = libevdev_event_code_from_name(EV_KEY, key_str.c_str()); - if (keycode == -1) { - BOOST_LOG(warning) << "Unicode, unable to find keycode for: "sv << ch; - } - else { - keyboard_ev(kb, keycode, 1); - keyboard_ev(kb, keycode, 0); - } - } - - /* releasing and */ - keyboard_ev(kb, KEY_LEFTSHIFT, 0); - keyboard_ev(kb, KEY_LEFTCTRL, 0); - } - - int - alloc_gamepad(input_t &input, const gamepad_id_t &id, const gamepad_arrival_t &metadata, feedback_queue_t feedback_queue) { - return ((input_raw_t *) input.get())->alloc_gamepad(id, metadata, std::move(feedback_queue)); - } - - void - free_gamepad(input_t &input, int nr) { - ((input_raw_t *) input.get())->clear_gamepad(nr); - } - - void - gamepad_update(input_t &input, int nr, const gamepad_state_t &gamepad_state) { - TUPLE_2D_REF(uinput, gamepad_state_old, ((input_raw_t *) input.get())->gamepads[nr]); - - auto bf = gamepad_state.buttonFlags ^ gamepad_state_old.buttonFlags; - auto bf_new = gamepad_state.buttonFlags; - - if (bf) { - // up pressed == -1, down pressed == 1, else 0 - if ((DPAD_UP | DPAD_DOWN) & bf) { - int button_state = bf_new & DPAD_UP ? -1 : (bf_new & DPAD_DOWN ? 1 : 0); - - libevdev_uinput_write_event(uinput.get(), EV_ABS, ABS_HAT0Y, button_state); - } - - if ((DPAD_LEFT | DPAD_RIGHT) & bf) { - int button_state = bf_new & DPAD_LEFT ? -1 : (bf_new & DPAD_RIGHT ? 1 : 0); - - libevdev_uinput_write_event(uinput.get(), EV_ABS, ABS_HAT0X, button_state); - } - - if (START & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_START, bf_new & START ? 1 : 0); - if (BACK & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_SELECT, bf_new & BACK ? 1 : 0); - if (LEFT_STICK & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_THUMBL, bf_new & LEFT_STICK ? 1 : 0); - if (RIGHT_STICK & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_THUMBR, bf_new & RIGHT_STICK ? 1 : 0); - if (LEFT_BUTTON & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_TL, bf_new & LEFT_BUTTON ? 1 : 0); - if (RIGHT_BUTTON & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_TR, bf_new & RIGHT_BUTTON ? 1 : 0); - if ((HOME | MISC_BUTTON) & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_MODE, bf_new & (HOME | MISC_BUTTON) ? 1 : 0); - if (A & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_SOUTH, bf_new & A ? 1 : 0); - if (B & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_EAST, bf_new & B ? 1 : 0); - if (X & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_NORTH, bf_new & X ? 1 : 0); - if (Y & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_WEST, bf_new & Y ? 1 : 0); - } - - if (gamepad_state_old.lt != gamepad_state.lt) { - libevdev_uinput_write_event(uinput.get(), EV_ABS, ABS_Z, gamepad_state.lt); - } - - if (gamepad_state_old.rt != gamepad_state.rt) { - libevdev_uinput_write_event(uinput.get(), EV_ABS, ABS_RZ, gamepad_state.rt); - } - - if (gamepad_state_old.lsX != gamepad_state.lsX) { - libevdev_uinput_write_event(uinput.get(), EV_ABS, ABS_X, gamepad_state.lsX); - } - - if (gamepad_state_old.lsY != gamepad_state.lsY) { - libevdev_uinput_write_event(uinput.get(), EV_ABS, ABS_Y, -gamepad_state.lsY); - } - - if (gamepad_state_old.rsX != gamepad_state.rsX) { - libevdev_uinput_write_event(uinput.get(), EV_ABS, ABS_RX, gamepad_state.rsX); - } - - if (gamepad_state_old.rsY != gamepad_state.rsY) { - libevdev_uinput_write_event(uinput.get(), EV_ABS, ABS_RY, -gamepad_state.rsY); - } - - gamepad_state_old = gamepad_state; - libevdev_uinput_write_event(uinput.get(), EV_SYN, SYN_REPORT, 0); - } - - constexpr auto NUM_TOUCH_SLOTS = 10; - constexpr auto DISTANCE_MAX = 1024; - constexpr auto PRESSURE_MAX = 4096; - constexpr int64_t INVALID_TRACKING_ID = -1; - - // HACK: Contacts with very small pressure values get discarded by libinput, but - // we assume that the client has already excluded such errant touches. We enforce - // a minimum pressure value to prevent our touches from being discarded. - constexpr auto PRESSURE_MIN = 0.10f; - - struct client_input_raw_t: public client_input_t { - client_input_raw_t(input_t &input) { - global = (input_raw_t *) input.get(); - touch_slots.fill(INVALID_TRACKING_ID); - } - - input_raw_t *global; - - // Device state and handles for pen and touch input must be stored in the per-client - // input context, because each connected client may be sending their own independent - // pen/touch events. To maintain separation, we expose separate pen and touch devices - // for each client. - - // Mapping of ABS_MT_SLOT/ABS_MT_TRACKING_ID -> pointerId - std::array touch_slots; - uinput_t touch_input; - uinput_t pen_input; - }; - - /** - * @brief Allocates a context to store per-client input data. - * @param input The global input context. - * @return A unique pointer to a per-client input data context. - */ - std::unique_ptr - allocate_client_input_context(input_t &input) { - return std::make_unique(input); - } - - /** - * @brief Retrieves the slot index for a given pointer ID. - * @param input The client-specific input context. - * @param pointerId The pointer ID sent from the client. - * @return Slot index or -1 if not found. - */ - int - slot_index_by_pointer_id(client_input_raw_t *input, uint32_t pointerId) { - for (int i = 0; i < input->touch_slots.size(); i++) { - if (input->touch_slots[i] == pointerId) { - return i; - } - } - return -1; - } - - /** - * @brief Reserves a slot index for a new pointer ID. - * @param input The client-specific input context. - * @param pointerId The pointer ID sent from the client. - * @return Slot index or -1 if no unallocated slots remain. - */ - int - allocate_slot_index_for_pointer_id(client_input_raw_t *input, uint32_t pointerId) { - int i = slot_index_by_pointer_id(input, pointerId); - if (i >= 0) { - BOOST_LOG(warning) << "Pointer "sv << pointerId << " already down. Did the client drop an up/cancel event?"sv; - return i; - } - - for (int i = 0; i < input->touch_slots.size(); i++) { - if (input->touch_slots[i] == INVALID_TRACKING_ID) { - input->touch_slots[i] = pointerId; - return i; - } - } - - return -1; - } - - /** - * @brief Sends a touch event to the OS. - * @param input The client-specific input context. - * @param touch_port The current viewport for translating to screen coordinates. - * @param touch The touch event. - */ - void - touch_update(client_input_t *input, const touch_port_t &touch_port, const touch_input_t &touch) { - auto raw = (client_input_raw_t *) input; - - if (!raw->touch_input) { - int err = libevdev_uinput_create_from_device(raw->global->touchscreen_dev.get(), LIBEVDEV_UINPUT_OPEN_MANAGED, &raw->touch_input); - if (err) { - BOOST_LOG(error) << "Could not create Sunshine Touchscreen: "sv << strerror(-err); - return; - } - } - - auto touch_input = raw->touch_input.get(); - - float pressure = std::max(PRESSURE_MIN, touch.pressureOrDistance); - - if (touch.eventType == LI_TOUCH_EVENT_CANCEL_ALL) { - for (int i = 0; i < raw->touch_slots.size(); i++) { - libevdev_uinput_write_event(touch_input, EV_ABS, ABS_MT_SLOT, i); - libevdev_uinput_write_event(touch_input, EV_ABS, ABS_MT_TRACKING_ID, -1); - } - raw->touch_slots.fill(INVALID_TRACKING_ID); - - libevdev_uinput_write_event(touch_input, EV_KEY, BTN_TOUCH, 0); - libevdev_uinput_write_event(touch_input, EV_ABS, ABS_PRESSURE, 0); - libevdev_uinput_write_event(touch_input, EV_SYN, SYN_REPORT, 0); - return; - } - - if (touch.eventType == LI_TOUCH_EVENT_CANCEL) { - // Stop tracking this slot - auto slot_index = slot_index_by_pointer_id(raw, touch.pointerId); - if (slot_index >= 0) { - libevdev_uinput_write_event(touch_input, EV_ABS, ABS_MT_SLOT, slot_index); - libevdev_uinput_write_event(touch_input, EV_ABS, ABS_MT_TRACKING_ID, -1); - - raw->touch_slots[slot_index] = INVALID_TRACKING_ID; - - // Raise BTN_TOUCH if no touches are down - if (std::all_of(raw->touch_slots.cbegin(), raw->touch_slots.cend(), - [](uint64_t pointer_id) { return pointer_id == INVALID_TRACKING_ID; })) { - libevdev_uinput_write_event(touch_input, EV_KEY, BTN_TOUCH, 0); - - // This may have been the final slot down which was also being emulated - // through the single-touch axes. Reset ABS_PRESSURE to ensure code that - // uses ABS_PRESSURE instead of BTN_TOUCH will work properly. - libevdev_uinput_write_event(touch_input, EV_ABS, ABS_PRESSURE, 0); - } - } - } - else if (touch.eventType == LI_TOUCH_EVENT_DOWN || - touch.eventType == LI_TOUCH_EVENT_MOVE || - touch.eventType == LI_TOUCH_EVENT_UP) { - int slot_index; - if (touch.eventType == LI_TOUCH_EVENT_DOWN) { - // Allocate a new slot for this new touch - slot_index = allocate_slot_index_for_pointer_id(raw, touch.pointerId); - if (slot_index < 0) { - BOOST_LOG(error) << "No unused pointer entries! Cancelling all active touches!"sv; - - for (int i = 0; i < raw->touch_slots.size(); i++) { - libevdev_uinput_write_event(touch_input, EV_ABS, ABS_MT_SLOT, i); - libevdev_uinput_write_event(touch_input, EV_ABS, ABS_MT_TRACKING_ID, -1); - } - raw->touch_slots.fill(INVALID_TRACKING_ID); - - libevdev_uinput_write_event(touch_input, EV_KEY, BTN_TOUCH, 0); - libevdev_uinput_write_event(touch_input, EV_ABS, ABS_PRESSURE, 0); - libevdev_uinput_write_event(touch_input, EV_SYN, SYN_REPORT, 0); - - // All slots are clear, so this should never fail on the second try - slot_index = allocate_slot_index_for_pointer_id(raw, touch.pointerId); - assert(slot_index >= 0); - } - } - else { - // Lookup the slot of the previous touch with this pointer ID - slot_index = slot_index_by_pointer_id(raw, touch.pointerId); - if (slot_index < 0) { - BOOST_LOG(warning) << "Pointer "sv << touch.pointerId << " is not down. Did the client drop a down event?"sv; - return; - } - } - - libevdev_uinput_write_event(touch_input, EV_ABS, ABS_MT_SLOT, slot_index); - - if (touch.eventType == LI_TOUCH_EVENT_UP) { - // Stop tracking this touch - libevdev_uinput_write_event(touch_input, EV_ABS, ABS_MT_TRACKING_ID, -1); - raw->touch_slots[slot_index] = INVALID_TRACKING_ID; - - // Raise BTN_TOUCH if no touches are down - if (std::all_of(raw->touch_slots.cbegin(), raw->touch_slots.cend(), - [](uint64_t pointer_id) { return pointer_id == INVALID_TRACKING_ID; })) { - libevdev_uinput_write_event(touch_input, EV_KEY, BTN_TOUCH, 0); - - // This may have been the final slot down which was also being emulated - // through the single-touch axes. Reset ABS_PRESSURE to ensure code that - // uses ABS_PRESSURE instead of BTN_TOUCH will work properly. - libevdev_uinput_write_event(touch_input, EV_ABS, ABS_PRESSURE, 0); - } - } - else { - float x = touch.x * touch_port.width; - float y = touch.y * touch_port.height; - - auto scaled_x = (int) std::lround((x + touch_port.offset_x) * ((float) target_touch_port.width / (float) touch_port.width)); - auto scaled_y = (int) std::lround((y + touch_port.offset_y) * ((float) target_touch_port.height / (float) touch_port.height)); - - libevdev_uinput_write_event(touch_input, EV_ABS, ABS_MT_TRACKING_ID, slot_index); - libevdev_uinput_write_event(touch_input, EV_ABS, ABS_MT_POSITION_X, scaled_x); - libevdev_uinput_write_event(touch_input, EV_ABS, ABS_MT_POSITION_Y, scaled_y); - - if (touch.pressureOrDistance) { - libevdev_uinput_write_event(touch_input, EV_ABS, ABS_MT_PRESSURE, PRESSURE_MAX * pressure); - } - else if (touch.eventType == LI_TOUCH_EVENT_DOWN) { - // Always report some moderate pressure value when down - libevdev_uinput_write_event(touch_input, EV_ABS, ABS_MT_PRESSURE, PRESSURE_MAX / 2); - } - - if (touch.rotation != LI_ROT_UNKNOWN) { - // Convert our 0..360 range to -90..90 relative to Y axis - int adjusted_angle = touch.rotation; - - if (touch.rotation > 90 && touch.rotation < 270) { - // Lower hemisphere - adjusted_angle = 180 - adjusted_angle; - } - - // Wrap the value if it's out of range - if (adjusted_angle > 90) { - adjusted_angle -= 360; - } - else if (adjusted_angle < -90) { - adjusted_angle += 360; - } - - libevdev_uinput_write_event(touch_input, EV_ABS, ABS_MT_ORIENTATION, adjusted_angle); - } - - if (touch.contactAreaMajor) { - // Contact area comes from the input core scaled to the provided touch_port, - // however we need it rescaled to target_touch_port instead. - auto target_scaled_contact_area = input::scale_client_contact_area( - { touch.contactAreaMajor * 65535.f, touch.contactAreaMinor * 65535.f }, - touch.rotation, - { target_touch_port.width / (touch_port.width * 65535.f), - target_touch_port.height / (touch_port.height * 65535.f) }); - - libevdev_uinput_write_event(touch_input, EV_ABS, ABS_MT_TOUCH_MAJOR, target_scaled_contact_area.first); - - // scale_client_contact_area() will treat the contact area as circular (major == minor) - // if the minor axis wasn't specified, so we unconditionally report ABS_MT_TOUCH_MINOR. - libevdev_uinput_write_event(touch_input, EV_ABS, ABS_MT_TOUCH_MINOR, target_scaled_contact_area.second); - } - - // If this slot is the first active one, send our data through the single touch axes as well - for (int i = 0; i <= slot_index; i++) { - if (raw->touch_slots[i] != INVALID_TRACKING_ID) { - if (i == slot_index) { - libevdev_uinput_write_event(touch_input, EV_ABS, ABS_X, scaled_x); - libevdev_uinput_write_event(touch_input, EV_ABS, ABS_Y, scaled_y); - if (touch.pressureOrDistance) { - libevdev_uinput_write_event(touch_input, EV_ABS, ABS_PRESSURE, PRESSURE_MAX * pressure); - } - else if (touch.eventType == LI_TOUCH_EVENT_DOWN) { - libevdev_uinput_write_event(touch_input, EV_ABS, ABS_PRESSURE, PRESSURE_MAX / 2); - } - } - break; - } - } - } - - libevdev_uinput_write_event(touch_input, EV_SYN, SYN_REPORT, 0); - } - } - - /** - * @brief Sends a pen event to the OS. - * @param input The client-specific input context. - * @param touch_port The current viewport for translating to screen coordinates. - * @param pen The pen event. - */ - void - pen_update(client_input_t *input, const touch_port_t &touch_port, const pen_input_t &pen) { - auto raw = (client_input_raw_t *) input; - - if (!raw->pen_input) { - int err = libevdev_uinput_create_from_device(raw->global->pen_dev.get(), LIBEVDEV_UINPUT_OPEN_MANAGED, &raw->pen_input); - if (err) { - BOOST_LOG(error) << "Could not create Sunshine Pen: "sv << strerror(-err); - return; - } - } - - auto pen_input = raw->pen_input.get(); - - float x = pen.x * touch_port.width; - float y = pen.y * touch_port.height; - float pressure = std::max(PRESSURE_MIN, pen.pressureOrDistance); - - auto scaled_x = (int) std::lround((x + touch_port.offset_x) * ((float) target_touch_port.width / (float) touch_port.width)); - auto scaled_y = (int) std::lround((y + touch_port.offset_y) * ((float) target_touch_port.height / (float) touch_port.height)); - - // First, process location updates for applicable events - switch (pen.eventType) { - case LI_TOUCH_EVENT_HOVER: - libevdev_uinput_write_event(pen_input, EV_ABS, ABS_X, scaled_x); - libevdev_uinput_write_event(pen_input, EV_ABS, ABS_Y, scaled_y); - - libevdev_uinput_write_event(pen_input, EV_ABS, ABS_PRESSURE, 0); - if (pen.pressureOrDistance) { - libevdev_uinput_write_event(pen_input, EV_ABS, ABS_DISTANCE, DISTANCE_MAX * pen.pressureOrDistance); - } - else { - // Always report some moderate distance value when hovering to ensure hovering - // can be detected properly by code that uses ABS_DISTANCE. - libevdev_uinput_write_event(pen_input, EV_ABS, ABS_DISTANCE, DISTANCE_MAX / 2); - } - break; - - case LI_TOUCH_EVENT_DOWN: - libevdev_uinput_write_event(pen_input, EV_ABS, ABS_X, scaled_x); - libevdev_uinput_write_event(pen_input, EV_ABS, ABS_Y, scaled_y); - - libevdev_uinput_write_event(pen_input, EV_ABS, ABS_DISTANCE, 0); - libevdev_uinput_write_event(pen_input, EV_ABS, ABS_PRESSURE, PRESSURE_MAX * pressure); - break; - - case LI_TOUCH_EVENT_UP: - libevdev_uinput_write_event(pen_input, EV_ABS, ABS_X, scaled_x); - libevdev_uinput_write_event(pen_input, EV_ABS, ABS_Y, scaled_y); - - libevdev_uinput_write_event(pen_input, EV_ABS, ABS_PRESSURE, 0); - break; - - case LI_TOUCH_EVENT_MOVE: - libevdev_uinput_write_event(pen_input, EV_ABS, ABS_X, scaled_x); - libevdev_uinput_write_event(pen_input, EV_ABS, ABS_Y, scaled_y); - - // Update the pressure value if it's present, otherwise leave the default/previous value alone - if (pen.pressureOrDistance) { - libevdev_uinput_write_event(pen_input, EV_ABS, ABS_PRESSURE, PRESSURE_MAX * pressure); - } - break; - } - - if (pen.contactAreaMajor) { - // Contact area comes from the input core scaled to the provided touch_port, - // however we need it rescaled to target_touch_port instead. - auto target_scaled_contact_area = input::scale_client_contact_area( - { pen.contactAreaMajor * 65535.f, pen.contactAreaMinor * 65535.f }, - pen.rotation, - { target_touch_port.width / (touch_port.width * 65535.f), - target_touch_port.height / (touch_port.height * 65535.f) }); - - // ABS_TOOL_WIDTH assumes a circular tool, so we just report the major axis - libevdev_uinput_write_event(pen_input, EV_ABS, ABS_TOOL_WIDTH, target_scaled_contact_area.first); - } - - // We require rotation and tilt to perform the conversion to X and Y tilt angles - if (pen.tilt != LI_TILT_UNKNOWN && pen.rotation != LI_ROT_UNKNOWN) { - auto rotation_rads = pen.rotation * (M_PI / 180.f); - auto tilt_rads = pen.tilt * (M_PI / 180.f); - auto r = std::sin(tilt_rads); - auto z = std::cos(tilt_rads); - - // Convert polar coordinates into X and Y tilt angles - libevdev_uinput_write_event(pen_input, EV_ABS, ABS_TILT_X, std::atan2(std::sin(-rotation_rads) * r, z) * 180.f / M_PI); - libevdev_uinput_write_event(pen_input, EV_ABS, ABS_TILT_Y, std::atan2(std::cos(-rotation_rads) * r, z) * 180.f / M_PI); - } - - // Don't update tool type if we're cancelling or ending a touch/hover - if (pen.eventType != LI_TOUCH_EVENT_CANCEL && - pen.eventType != LI_TOUCH_EVENT_CANCEL_ALL && - pen.eventType != LI_TOUCH_EVENT_HOVER_LEAVE && - pen.eventType != LI_TOUCH_EVENT_UP) { - // Update the tool type if it is known - switch (pen.toolType) { - default: - // We need to have _some_ tool type set, otherwise there's no way to know a tool is in - // range when hovering. If we don't know the type of tool, let's assume it's a pen. - if (pen.eventType != LI_TOUCH_EVENT_DOWN && pen.eventType != LI_TOUCH_EVENT_HOVER) { - break; - } - // fall-through - case LI_TOOL_TYPE_PEN: - libevdev_uinput_write_event(pen_input, EV_KEY, BTN_TOOL_RUBBER, 0); - libevdev_uinput_write_event(pen_input, EV_KEY, BTN_TOOL_PEN, 1); - break; - case LI_TOOL_TYPE_ERASER: - libevdev_uinput_write_event(pen_input, EV_KEY, BTN_TOOL_PEN, 0); - libevdev_uinput_write_event(pen_input, EV_KEY, BTN_TOOL_RUBBER, 1); - break; - } - } - - // Next, process touch state changes - switch (pen.eventType) { - case LI_TOUCH_EVENT_CANCEL: - case LI_TOUCH_EVENT_CANCEL_ALL: - case LI_TOUCH_EVENT_HOVER_LEAVE: - case LI_TOUCH_EVENT_UP: - libevdev_uinput_write_event(pen_input, EV_KEY, BTN_TOUCH, 0); - - // Leaving hover range is detected by all BTN_TOOL_* being cleared - libevdev_uinput_write_event(pen_input, EV_KEY, BTN_TOOL_PEN, 0); - libevdev_uinput_write_event(pen_input, EV_KEY, BTN_TOOL_RUBBER, 0); - break; - - case LI_TOUCH_EVENT_DOWN: - libevdev_uinput_write_event(pen_input, EV_KEY, BTN_TOUCH, 1); - break; - } - - // Finally, process pen buttons - libevdev_uinput_write_event(pen_input, EV_KEY, BTN_STYLUS, !!(pen.penButtons & LI_PEN_BUTTON_PRIMARY)); - libevdev_uinput_write_event(pen_input, EV_KEY, BTN_STYLUS2, !!(pen.penButtons & LI_PEN_BUTTON_SECONDARY)); - libevdev_uinput_write_event(pen_input, EV_KEY, BTN_STYLUS3, !!(pen.penButtons & LI_PEN_BUTTON_TERTIARY)); - - libevdev_uinput_write_event(pen_input, EV_SYN, SYN_REPORT, 0); - } - - /** - * @brief Sends a gamepad touch event to the OS. - * @param input The global input context. - * @param touch The touch event. - */ - void - gamepad_touch(input_t &input, const gamepad_touch_t &touch) { - // Unimplemented feature - platform_caps::controller_touch - } - - /** - * @brief Sends a gamepad motion event to the OS. - * @param input The global input context. - * @param motion The motion event. - */ - void - gamepad_motion(input_t &input, const gamepad_motion_t &motion) { - // Unimplemented - } - - /** - * @brief Sends a gamepad battery event to the OS. - * @param input The global input context. - * @param battery The battery event. - */ - void - gamepad_battery(input_t &input, const gamepad_battery_t &battery) { - // Unimplemented - } - - /** - * @brief Initialize a new keyboard and return it. - * @examples - * auto my_keyboard = keyboard(); - * @examples_end - */ - evdev_t - keyboard() { - evdev_t dev { libevdev_new() }; - - libevdev_set_uniq(dev.get(), "Sunshine Keyboard"); - libevdev_set_id_product(dev.get(), 0xDEAD); - libevdev_set_id_vendor(dev.get(), 0xBEEF); - libevdev_set_id_bustype(dev.get(), 0x3); - libevdev_set_id_version(dev.get(), 0x111); - libevdev_set_name(dev.get(), "Keyboard passthrough"); - - libevdev_enable_event_type(dev.get(), EV_KEY); - for (const auto &keycode : keycodes) { - libevdev_enable_event_code(dev.get(), EV_KEY, keycode.keycode, nullptr); - } - libevdev_enable_event_type(dev.get(), EV_MSC); - libevdev_enable_event_code(dev.get(), EV_MSC, MSC_SCAN, nullptr); - - return dev; - } - - /** - * @brief Initialize a new `uinput` virtual relative mouse and return it. - * @examples - * auto my_mouse = mouse_rel(); - * @examples_end - */ - evdev_t - mouse_rel() { - evdev_t dev { libevdev_new() }; - - libevdev_set_uniq(dev.get(), "Sunshine Mouse (Rel)"); - libevdev_set_id_product(dev.get(), 0x4038); - libevdev_set_id_vendor(dev.get(), 0x46D); - libevdev_set_id_bustype(dev.get(), 0x3); - libevdev_set_id_version(dev.get(), 0x111); - libevdev_set_name(dev.get(), "Logitech Wireless Mouse PID:4038"); - - libevdev_enable_event_type(dev.get(), EV_KEY); - libevdev_enable_event_code(dev.get(), EV_KEY, BTN_LEFT, nullptr); - libevdev_enable_event_code(dev.get(), EV_KEY, BTN_RIGHT, nullptr); - libevdev_enable_event_code(dev.get(), EV_KEY, BTN_MIDDLE, nullptr); - libevdev_enable_event_code(dev.get(), EV_KEY, BTN_SIDE, nullptr); - libevdev_enable_event_code(dev.get(), EV_KEY, BTN_EXTRA, nullptr); - libevdev_enable_event_code(dev.get(), EV_KEY, BTN_FORWARD, nullptr); - libevdev_enable_event_code(dev.get(), EV_KEY, BTN_BACK, nullptr); - libevdev_enable_event_code(dev.get(), EV_KEY, BTN_TASK, nullptr); - libevdev_enable_event_code(dev.get(), EV_KEY, 280, nullptr); - libevdev_enable_event_code(dev.get(), EV_KEY, 281, nullptr); - libevdev_enable_event_code(dev.get(), EV_KEY, 282, nullptr); - libevdev_enable_event_code(dev.get(), EV_KEY, 283, nullptr); - libevdev_enable_event_code(dev.get(), EV_KEY, 284, nullptr); - libevdev_enable_event_code(dev.get(), EV_KEY, 285, nullptr); - libevdev_enable_event_code(dev.get(), EV_KEY, 286, nullptr); - libevdev_enable_event_code(dev.get(), EV_KEY, 287, nullptr); - - libevdev_enable_event_type(dev.get(), EV_REL); - libevdev_enable_event_code(dev.get(), EV_REL, REL_X, nullptr); - libevdev_enable_event_code(dev.get(), EV_REL, REL_Y, nullptr); - libevdev_enable_event_code(dev.get(), EV_REL, REL_WHEEL, nullptr); - libevdev_enable_event_code(dev.get(), EV_REL, REL_WHEEL_HI_RES, nullptr); - libevdev_enable_event_code(dev.get(), EV_REL, REL_HWHEEL, nullptr); - libevdev_enable_event_code(dev.get(), EV_REL, REL_HWHEEL_HI_RES, nullptr); - - libevdev_enable_event_type(dev.get(), EV_MSC); - libevdev_enable_event_code(dev.get(), EV_MSC, MSC_SCAN, nullptr); - - return dev; - } - - /** - * @brief Initialize a new `uinput` virtual absolute mouse and return it. - * @examples - * auto my_mouse = mouse_abs(); - * @examples_end - */ - evdev_t - mouse_abs() { - evdev_t dev { libevdev_new() }; - - libevdev_set_uniq(dev.get(), "Sunshine Mouse (Abs)"); - libevdev_set_id_product(dev.get(), 0xDEAD); - libevdev_set_id_vendor(dev.get(), 0xBEEF); - libevdev_set_id_bustype(dev.get(), 0x3); - libevdev_set_id_version(dev.get(), 0x111); - libevdev_set_name(dev.get(), "Mouse passthrough"); - - libevdev_enable_property(dev.get(), INPUT_PROP_DIRECT); - - libevdev_enable_event_type(dev.get(), EV_KEY); - libevdev_enable_event_code(dev.get(), EV_KEY, BTN_LEFT, nullptr); - libevdev_enable_event_code(dev.get(), EV_KEY, BTN_RIGHT, nullptr); - libevdev_enable_event_code(dev.get(), EV_KEY, BTN_MIDDLE, nullptr); - libevdev_enable_event_code(dev.get(), EV_KEY, BTN_SIDE, nullptr); - libevdev_enable_event_code(dev.get(), EV_KEY, BTN_EXTRA, nullptr); - - libevdev_enable_event_type(dev.get(), EV_MSC); - libevdev_enable_event_code(dev.get(), EV_MSC, MSC_SCAN, nullptr); - - input_absinfo absx { - 0, - 0, - target_touch_port.width, - 1, - 0, - 28 - }; - - input_absinfo absy { - 0, - 0, - target_touch_port.height, - 1, - 0, - 28 - }; - libevdev_enable_event_type(dev.get(), EV_ABS); - libevdev_enable_event_code(dev.get(), EV_ABS, ABS_X, &absx); - libevdev_enable_event_code(dev.get(), EV_ABS, ABS_Y, &absy); - - return dev; - } - - /** - * @brief Initialize a new `uinput` virtual touchscreen and return it. - * @examples - * auto my_touchscreen = touchscreen(); - * @examples_end - */ - evdev_t - touchscreen() { - evdev_t dev { libevdev_new() }; - - libevdev_set_uniq(dev.get(), "Sunshine Touchscreen"); - libevdev_set_id_product(dev.get(), 0xDEAD); - libevdev_set_id_vendor(dev.get(), 0xBEEF); - libevdev_set_id_bustype(dev.get(), 0x3); - libevdev_set_id_version(dev.get(), 0x111); - libevdev_set_name(dev.get(), "Touch passthrough"); - - libevdev_enable_property(dev.get(), INPUT_PROP_DIRECT); - - constexpr auto RESOLUTION = 28; - - input_absinfo abs_slot { - 0, - 0, - NUM_TOUCH_SLOTS - 1, - 0, - 0, - 0 - }; - - input_absinfo abs_tracking_id { - 0, - 0, - NUM_TOUCH_SLOTS - 1, - 0, - 0, - 0 - }; - - input_absinfo abs_x { - 0, - 0, - target_touch_port.width, - 1, - 0, - RESOLUTION - }; - - input_absinfo abs_y { - 0, - 0, - target_touch_port.height, - 1, - 0, - RESOLUTION - }; - - input_absinfo abs_pressure { - 0, - 0, - PRESSURE_MAX, - 0, - 0, - 0 - }; - - // Degrees of a half revolution - input_absinfo abs_orientation { - 0, - -90, - 90, - 0, - 0, - 0 - }; - - // Fractions of the full diagonal - input_absinfo abs_contact_area { - 0, - 0, - (__s32) std::sqrt(std::pow(target_touch_port.width, 2) + std::pow(target_touch_port.height, 2)), - 1, - 0, - RESOLUTION - }; - - libevdev_enable_event_type(dev.get(), EV_ABS); - libevdev_enable_event_code(dev.get(), EV_ABS, ABS_X, &abs_x); - libevdev_enable_event_code(dev.get(), EV_ABS, ABS_Y, &abs_y); - libevdev_enable_event_code(dev.get(), EV_ABS, ABS_PRESSURE, &abs_pressure); - libevdev_enable_event_code(dev.get(), EV_ABS, ABS_MT_SLOT, &abs_slot); - libevdev_enable_event_code(dev.get(), EV_ABS, ABS_MT_TRACKING_ID, &abs_tracking_id); - libevdev_enable_event_code(dev.get(), EV_ABS, ABS_MT_POSITION_X, &abs_x); - libevdev_enable_event_code(dev.get(), EV_ABS, ABS_MT_POSITION_Y, &abs_y); - libevdev_enable_event_code(dev.get(), EV_ABS, ABS_MT_PRESSURE, &abs_pressure); - libevdev_enable_event_code(dev.get(), EV_ABS, ABS_MT_ORIENTATION, &abs_orientation); - libevdev_enable_event_code(dev.get(), EV_ABS, ABS_MT_TOUCH_MAJOR, &abs_contact_area); - libevdev_enable_event_code(dev.get(), EV_ABS, ABS_MT_TOUCH_MINOR, &abs_contact_area); - - libevdev_enable_event_type(dev.get(), EV_KEY); - libevdev_enable_event_code(dev.get(), EV_KEY, BTN_TOUCH, nullptr); - - return dev; - } - - /** - * @brief Initialize a new `uinput` virtual pen pad and return it. - * @examples - * auto my_penpad = penpad(); - * @examples_end - */ - evdev_t - penpad() { - evdev_t dev { libevdev_new() }; - - libevdev_set_uniq(dev.get(), "Sunshine Pen"); - libevdev_set_id_product(dev.get(), 0xDEAD); - libevdev_set_id_vendor(dev.get(), 0xBEEF); - libevdev_set_id_bustype(dev.get(), 0x3); - libevdev_set_id_version(dev.get(), 0x111); - libevdev_set_name(dev.get(), "Pen passthrough"); - - libevdev_enable_property(dev.get(), INPUT_PROP_DIRECT); - - constexpr auto RESOLUTION = 28; - - input_absinfo abs_x { - 0, - 0, - target_touch_port.width, - 1, - 0, - RESOLUTION - }; - - input_absinfo abs_y { - 0, - 0, - target_touch_port.height, - 1, - 0, - RESOLUTION - }; - - input_absinfo abs_pressure { - 0, - 0, - PRESSURE_MAX, - 0, - 0, - 0 - }; - - input_absinfo abs_distance { - 0, - 0, - DISTANCE_MAX, - 0, - 0, - 0 - }; - - // Degrees of tilt - input_absinfo abs_tilt { - 0, - -90, - 90, - 0, - 0, - 0 - }; - - // Fractions of the full diagonal - input_absinfo abs_contact_area { - 0, - 0, - (__s32) std::sqrt(std::pow(target_touch_port.width, 2) + std::pow(target_touch_port.height, 2)), - 1, - 0, - RESOLUTION - }; - - libevdev_enable_event_type(dev.get(), EV_ABS); - libevdev_enable_event_code(dev.get(), EV_ABS, ABS_X, &abs_x); - libevdev_enable_event_code(dev.get(), EV_ABS, ABS_Y, &abs_y); - libevdev_enable_event_code(dev.get(), EV_ABS, ABS_PRESSURE, &abs_pressure); - libevdev_enable_event_code(dev.get(), EV_ABS, ABS_DISTANCE, &abs_distance); - libevdev_enable_event_code(dev.get(), EV_ABS, ABS_TILT_X, &abs_tilt); - libevdev_enable_event_code(dev.get(), EV_ABS, ABS_TILT_Y, &abs_tilt); - libevdev_enable_event_code(dev.get(), EV_ABS, ABS_TOOL_WIDTH, &abs_contact_area); - - libevdev_enable_event_type(dev.get(), EV_KEY); - libevdev_enable_event_code(dev.get(), EV_KEY, BTN_TOUCH, nullptr); - libevdev_enable_event_code(dev.get(), EV_KEY, BTN_TOOL_PEN, nullptr); - libevdev_enable_event_code(dev.get(), EV_KEY, BTN_TOOL_RUBBER, nullptr); - libevdev_enable_event_code(dev.get(), EV_KEY, BTN_STYLUS, nullptr); - libevdev_enable_event_code(dev.get(), EV_KEY, BTN_STYLUS2, nullptr); - libevdev_enable_event_code(dev.get(), EV_KEY, BTN_STYLUS3, nullptr); - - return dev; - } - - /** - * @brief Initialize a new `uinput` virtual X360 gamepad and return it. - * @examples - * auto my_x360 = x360(); - * @examples_end - */ - evdev_t - x360() { - evdev_t dev { libevdev_new() }; - - input_absinfo stick { - 0, - -32768, 32767, - 16, - 128, - 0 - }; - - input_absinfo trigger { - 0, - 0, 255, - 0, - 0, - 0 - }; - - input_absinfo dpad { - 0, - -1, 1, - 0, - 0, - 0 - }; - - libevdev_set_uniq(dev.get(), "Sunshine Gamepad"); - libevdev_set_id_product(dev.get(), 0x28E); - libevdev_set_id_vendor(dev.get(), 0x45E); - libevdev_set_id_bustype(dev.get(), 0x3); - libevdev_set_id_version(dev.get(), 0x110); - libevdev_set_name(dev.get(), "Microsoft X-Box 360 pad"); - - libevdev_enable_event_type(dev.get(), EV_KEY); - libevdev_enable_event_code(dev.get(), EV_KEY, BTN_WEST, nullptr); - libevdev_enable_event_code(dev.get(), EV_KEY, BTN_EAST, nullptr); - libevdev_enable_event_code(dev.get(), EV_KEY, BTN_NORTH, nullptr); - libevdev_enable_event_code(dev.get(), EV_KEY, BTN_SOUTH, nullptr); - libevdev_enable_event_code(dev.get(), EV_KEY, BTN_THUMBL, nullptr); - libevdev_enable_event_code(dev.get(), EV_KEY, BTN_THUMBR, nullptr); - libevdev_enable_event_code(dev.get(), EV_KEY, BTN_TR, nullptr); - libevdev_enable_event_code(dev.get(), EV_KEY, BTN_TL, nullptr); - libevdev_enable_event_code(dev.get(), EV_KEY, BTN_SELECT, nullptr); - libevdev_enable_event_code(dev.get(), EV_KEY, BTN_MODE, nullptr); - libevdev_enable_event_code(dev.get(), EV_KEY, BTN_START, nullptr); - - libevdev_enable_event_type(dev.get(), EV_ABS); - libevdev_enable_event_code(dev.get(), EV_ABS, ABS_HAT0Y, &dpad); - libevdev_enable_event_code(dev.get(), EV_ABS, ABS_HAT0X, &dpad); - libevdev_enable_event_code(dev.get(), EV_ABS, ABS_Z, &trigger); - libevdev_enable_event_code(dev.get(), EV_ABS, ABS_RZ, &trigger); - libevdev_enable_event_code(dev.get(), EV_ABS, ABS_X, &stick); - libevdev_enable_event_code(dev.get(), EV_ABS, ABS_RX, &stick); - libevdev_enable_event_code(dev.get(), EV_ABS, ABS_Y, &stick); - libevdev_enable_event_code(dev.get(), EV_ABS, ABS_RY, &stick); - - libevdev_enable_event_type(dev.get(), EV_FF); - libevdev_enable_event_code(dev.get(), EV_FF, FF_RUMBLE, nullptr); - libevdev_enable_event_code(dev.get(), EV_FF, FF_CONSTANT, nullptr); - libevdev_enable_event_code(dev.get(), EV_FF, FF_PERIODIC, nullptr); - libevdev_enable_event_code(dev.get(), EV_FF, FF_SINE, nullptr); - libevdev_enable_event_code(dev.get(), EV_FF, FF_RAMP, nullptr); - libevdev_enable_event_code(dev.get(), EV_FF, FF_GAIN, nullptr); - - return dev; - } - - /** - * @brief Initialize the input system and return it. - * @examples - * auto my_input = input(); - * @examples_end - */ - input_t - input() { - input_t result { new input_raw_t() }; - auto &gp = *(input_raw_t *) result.get(); - - gp.rumble_ctx = notifications.ref(); - - gp.gamepads.resize(MAX_GAMEPADS); - - // Ensure starting from clean slate - gp.clear(); - gp.keyboard_dev = keyboard(); - gp.mouse_rel_dev = mouse_rel(); - gp.mouse_abs_dev = mouse_abs(); - gp.touchscreen_dev = touchscreen(); - gp.pen_dev = penpad(); - gp.gamepad_dev = x360(); - - gp.create_mouse_rel(); - gp.create_mouse_abs(); - gp.create_keyboard(); - - // If we do not have a keyboard or mouse, fall back to XTest - if (!gp.mouse_rel_input || !gp.mouse_abs_input || !gp.keyboard_input) { -#ifdef SUNSHINE_BUILD_X11 - if (x11::init() || x11::tst::init()) { - BOOST_LOG(fatal) << "Unable to create virtual input devices or use XTest fallback! Are you a member of the 'input' group?"sv; - } - else { - BOOST_LOG(error) << "Falling back to XTest for virtual input! Are you a member of the 'input' group?"sv; - x11::InitThreads(); - gp.display = x11::OpenDisplay(NULL); - } -#else - BOOST_LOG(fatal) << "Unable to create virtual input devices! Are you a member of the 'input' group?"sv; -#endif - } - else { - has_uinput = true; - } - - return result; - } - - void - freeInput(void *p) { - auto *input = (input_raw_t *) p; - delete input; - } - - std::vector & - supported_gamepads(input_t *input) { - static std::vector gamepads { - supported_gamepad_t { "x360", true, "" }, - }; - - return gamepads; - } - - /** - * @brief Returns the supported platform capabilities to advertise to the client. - * @return Capability flags. - */ - platform_caps::caps_t - get_capabilities() { - platform_caps::caps_t caps = 0; - - // Pen and touch emulation requires uinput - if (has_uinput && config::input.native_pen_touch) { - caps |= platform_caps::pen_touch; - } - - return caps; - } -} // namespace platf diff --git a/src/platform/linux/kmsgrab.cpp b/src/platform/linux/kmsgrab.cpp index 5b77d6064c1..3db74899930 100644 --- a/src/platform/linux/kmsgrab.cpp +++ b/src/platform/linux/kmsgrab.cpp @@ -2,28 +2,30 @@ * @file src/platform/linux/kmsgrab.cpp * @brief Definitions for KMS screen capture. */ -#include +// standard includes #include #include +#include +#include +#include + +// platform includes +#include #include #include #include -#include #include #include -#include -#include - +// local includes +#include "cuda.h" +#include "graphics.h" #include "src/config.h" #include "src/logging.h" #include "src/platform/common.h" #include "src/round_robin.h" #include "src/utility.h" #include "src/video.h" - -#include "cuda.h" -#include "graphics.h" #include "vaapi.h" #include "wayland.h" @@ -59,7 +61,10 @@ namespace platf { class wrapper_fb { public: wrapper_fb(drmModeFB *fb): - fb { fb }, fb_id { fb->fb_id }, width { fb->width }, height { fb->height } { + fb {fb}, + fb_id {fb->fb_id}, + width {fb->width}, + height {fb->height} { pixel_format = DRM_FORMAT_XRGB8888; modifier = DRM_FORMAT_MOD_INVALID; std::fill_n(handles, 4, 0); @@ -70,7 +75,10 @@ namespace platf { } wrapper_fb(drmModeFB2 *fb2): - fb2 { fb2 }, fb_id { fb2->fb_id }, width { fb2->width }, height { fb2->height } { + fb2 {fb2}, + fb_id {fb2->fb_id}, + width {fb2->width}, + height {fb2->height} { pixel_format = fb2->pixel_format; modifier = (fb2->flags & DRM_MODE_FB_MODIFIERS) ? fb2->modifier : DRM_FORMAT_MOD_INVALID; @@ -82,8 +90,7 @@ namespace platf { ~wrapper_fb() { if (fb) { drmModeFreeFB(fb); - } - else if (fb2) { + } else if (fb2) { drmModeFreeFB2(fb2); } } @@ -116,8 +123,7 @@ namespace platf { static int env_width; static int env_height; - std::string_view - plane_type(std::uint64_t val) { + std::string_view plane_type(std::uint64_t val) { switch (val) { case DRM_PLANE_TYPE_OVERLAY: return "DRM_PLANE_TYPE_OVERLAY"sv; @@ -165,10 +171,10 @@ namespace platf { static std::vector card_descriptors; - static std::uint32_t - from_view(const std::string_view &string) { + static std::uint32_t from_view(const std::string_view &string) { #define _CONVERT(x, y) \ - if (string == x) return DRM_MODE_CONNECTOR_##y + if (string == x) \ + return DRM_MODE_CONNECTOR_##y // This list was created from the following sources: // https://gitlab.freedesktop.org/mesa/drm/-/blob/main/xf86drmMode.c (drmModeGetConnectorTypeName) @@ -212,7 +218,7 @@ namespace platf { // value appended to the string. Let's try to read it. if (string.find("Unknown"sv) == 0) { std::uint32_t type; - std::string null_terminated_string { string }; + std::string null_terminated_string {string}; if (std::sscanf(null_terminated_string.c_str(), "Unknown%u", &type) == 1) { return type; } @@ -225,15 +231,19 @@ namespace platf { class plane_it_t: public round_robin_util::it_wrap_t { public: plane_it_t(int fd, std::uint32_t *plane_p, std::uint32_t *end): - fd { fd }, plane_p { plane_p }, end { end } { + fd {fd}, + plane_p {plane_p}, + end {end} { load_next_valid_plane(); } plane_it_t(int fd, std::uint32_t *end): - fd { fd }, plane_p { end }, end { end } {} + fd {fd}, + plane_p {end}, + end {end} { + } - void - load_next_valid_plane() { + void load_next_valid_plane() { this->plane.reset(); for (; plane_p != end; ++plane_p) { @@ -248,19 +258,16 @@ namespace platf { } } - void - inc() { + void inc() { ++plane_p; load_next_valid_plane(); } - bool - eq(const plane_it_t &other) const { + bool eq(const plane_it_t &other) const { return plane_p == other.plane_p; } - plane_t::pointer - get() { + plane_t::pointer get() { return plane.get(); } @@ -289,8 +296,7 @@ namespace platf { public: using connector_interal_t = util::safe_ptr; - int - init(const char *path) { + int init(const char *path) { cap_sys_admin admin; fd.el = open(path, O_RDWR); @@ -299,7 +305,7 @@ namespace platf { return -1; } - version_t ver { drmGetVersion(fd.el) }; + version_t ver {drmGetVersion(fd.el)}; BOOST_LOG(info) << path << " -> "sv << ((ver && ver->name) ? ver->name : "UNKNOWN"); // Open the render node for this card to share with libva. @@ -313,8 +319,7 @@ namespace platf { render_fd.el = dup(fd.el); } free(rendernode_path); - } - else { + } else { BOOST_LOG(warning) << "No render device name for: "sv << path; render_fd.el = dup(fd.el); } @@ -346,8 +351,7 @@ namespace platf { return 0; } - fb_t - fb(plane_t::pointer plane) { + fb_t fb(plane_t::pointer plane) { cap_sys_admin admin; auto fb2 = drmModeGetFB2(fd.el, plane->fb_id); @@ -363,36 +367,30 @@ namespace platf { return nullptr; } - crtc_t - crtc(std::uint32_t id) { + crtc_t crtc(std::uint32_t id) { return drmModeGetCrtc(fd.el, id); } - encoder_t - encoder(std::uint32_t id) { + encoder_t encoder(std::uint32_t id) { return drmModeGetEncoder(fd.el, id); } - res_t - res() { + res_t res() { return drmModeGetResources(fd.el); } - bool - is_nvidia() { - version_t ver { drmGetVersion(fd.el) }; + bool is_nvidia() { + version_t ver {drmGetVersion(fd.el)}; return ver && ver->name && strncmp(ver->name, "nvidia-drm", 10) == 0; } - bool - is_cursor(std::uint32_t plane_id) { + bool is_cursor(std::uint32_t plane_id) { auto props = plane_props(plane_id); for (auto &[prop, val] : props) { if (prop->name == "type"sv) { if (val == DRM_PLANE_TYPE_CURSOR) { return true; - } - else { + } else { return false; } } @@ -401,8 +399,7 @@ namespace platf { return false; } - std::optional - prop_value_by_name(const std::vector> &props, std::string_view name) { + std::optional prop_value_by_name(const std::vector> &props, std::string_view name) { for (auto &[prop, val] : props) { if (prop->name == name) { return val; @@ -411,8 +408,7 @@ namespace platf { return std::nullopt; } - std::uint32_t - get_panel_orientation(std::uint32_t plane_id) { + std::uint32_t get_panel_orientation(std::uint32_t plane_id) { auto props = plane_props(plane_id); auto value = prop_value_by_name(props, "rotation"sv); if (value) { @@ -423,8 +419,7 @@ namespace platf { return DRM_MODE_ROTATE_0; } - int - get_crtc_index_by_id(std::uint32_t crtc_id) { + int get_crtc_index_by_id(std::uint32_t crtc_id) { auto resources = res(); for (int i = 0; i < resources->count_crtcs; i++) { if (resources->crtcs[i] == crtc_id) { @@ -434,13 +429,11 @@ namespace platf { return -1; } - connector_interal_t - connector(std::uint32_t id) { + connector_interal_t connector(std::uint32_t id) { return drmModeGetConnector(fd.el, id); } - std::vector - monitors(conn_type_count_t &conn_type_count) { + std::vector monitors(conn_type_count_t &conn_type_count) { auto resources = res(); if (!resources) { BOOST_LOG(error) << "Couldn't get connector resources"sv; @@ -474,8 +467,7 @@ namespace platf { return monitors; } - file_t - handleFD(std::uint32_t handle) { + file_t handleFD(std::uint32_t handle) { file_t fb_fd; auto status = drmPrimeHandleToFD(fd.el, handle, 0 /* flags */, &fb_fd.el); @@ -486,8 +478,7 @@ namespace platf { return fb_fd; } - std::vector> - props(std::uint32_t id, std::uint32_t type) { + std::vector> props(std::uint32_t id, std::uint32_t type) { obj_prop_t obj_prop = drmModeObjectGetProperties(fd.el, id, type); if (!obj_prop) { return {}; @@ -503,39 +494,32 @@ namespace platf { return props; } - std::vector> - plane_props(std::uint32_t id) { + std::vector> plane_props(std::uint32_t id) { return props(id, DRM_MODE_OBJECT_PLANE); } - std::vector> - crtc_props(std::uint32_t id) { + std::vector> crtc_props(std::uint32_t id) { return props(id, DRM_MODE_OBJECT_CRTC); } - std::vector> - connector_props(std::uint32_t id) { + std::vector> connector_props(std::uint32_t id) { return props(id, DRM_MODE_OBJECT_CONNECTOR); } - plane_t - operator[](std::uint32_t index) { + plane_t operator[](std::uint32_t index) { return drmModeGetPlane(fd.el, plane_res->planes[index]); } - std::uint32_t - count() { + std::uint32_t count() { return plane_res->count_planes; } - plane_it_t - begin() const { - return plane_it_t { fd.el, plane_res->planes, plane_res->planes + plane_res->count_planes }; + plane_it_t begin() const { + return plane_it_t {fd.el, plane_res->planes, plane_res->planes + plane_res->count_planes}; } - plane_it_t - end() const { - return plane_it_t { fd.el, plane_res->planes + plane_res->count_planes }; + plane_it_t end() const { + return plane_it_t {fd.el, plane_res->planes + plane_res->count_planes}; } file_t fd; @@ -543,16 +527,14 @@ namespace platf { plane_res_t plane_res; }; - std::map - map_crtc_to_monitor(const std::vector &connectors) { + std::map map_crtc_to_monitor(const std::vector &connectors) { std::map result; for (auto &connector : connectors) { - result.emplace(connector.crtc_id, - monitor_t { - connector.type, - connector.index, - }); + result.emplace(connector.crtc_id, monitor_t { + connector.type, + connector.index, + }); } return result; @@ -565,8 +547,7 @@ namespace platf { } }; - void - print(plane_t::pointer plane, fb_t::pointer fb, crtc_t::pointer crtc) { + void print(plane_t::pointer plane, fb_t::pointer fb, crtc_t::pointer crtc) { if (crtc) { BOOST_LOG(debug) << "crtc("sv << crtc->x << ", "sv << crtc->y << ')'; BOOST_LOG(debug) << "crtc("sv << crtc->width << ", "sv << crtc->height << ')'; @@ -601,21 +582,22 @@ namespace platf { class display_t: public platf::display_t { public: display_t(mem_type_e mem_type): - platf::display_t(), mem_type { mem_type } {} + platf::display_t(), + mem_type {mem_type} { + } - int - init(const std::string &display_name, const ::video::config_t &config) { - delay = std::chrono::nanoseconds { 1s } / config.framerate; + int init(const std::string &display_name, const ::video::config_t &config) { + delay = std::chrono::nanoseconds {1s} / config.framerate; int monitor_index = util::from_view(display_name); int monitor = 0; - fs::path card_dir { "/dev/dri"sv }; - for (auto &entry : fs::directory_iterator { card_dir }) { + fs::path card_dir {"/dev/dri"sv}; + for (auto &entry : fs::directory_iterator {card_dir}) { auto file = entry.path().filename(); auto filestring = file.generic_string(); - if (filestring.size() < 4 || std::string_view { filestring }.substr(0, 4) != "card"sv) { + if (filestring.size() < 4 || std::string_view {filestring}.substr(0, 4) != "card"sv) { continue; } @@ -779,8 +761,7 @@ namespace platf { if (!(plane->possible_crtcs & (1 << crtc_index))) { // Skip cursor planes for other CRTCs continue; - } - else if (plane->possible_crtcs != (1 << crtc_index)) { + } else if (plane->possible_crtcs != (1 << crtc_index)) { // We assume a 1:1 mapping between cursor planes and CRTCs, which seems to // match the behavior of drivers in the real world. If it's violated, we'll // proceed anyway but print a warning in the log. @@ -799,8 +780,7 @@ namespace platf { return 0; } - bool - is_hdr() { + bool is_hdr() { if (!hdr_metadata_blob_id || *hdr_metadata_blob_id == 0) { return false; } @@ -846,8 +826,7 @@ namespace platf { } } - bool - get_hdr_metadata(SS_HDR_METADATA &metadata) { + bool get_hdr_metadata(SS_HDR_METADATA &metadata) { // This performs all the metadata validation if (!is_hdr()) { return false; @@ -876,8 +855,7 @@ namespace platf { return true; } - void - update_cursor() { + void update_cursor() { if (cursor_plane_id < 0) { return; } @@ -898,26 +876,19 @@ namespace platf { for (auto &[prop, val] : props) { if (prop->name == "CRTC_X"sv) { prop_crtc_x = val; - } - else if (prop->name == "CRTC_Y"sv) { + } else if (prop->name == "CRTC_Y"sv) { prop_crtc_y = val; - } - else if (prop->name == "CRTC_W"sv) { + } else if (prop->name == "CRTC_W"sv) { prop_crtc_w = val; - } - else if (prop->name == "CRTC_H"sv) { + } else if (prop->name == "CRTC_H"sv) { prop_crtc_h = val; - } - else if (prop->name == "SRC_X"sv) { + } else if (prop->name == "SRC_X"sv) { prop_src_x = val; - } - else if (prop->name == "SRC_Y"sv) { + } else if (prop->name == "SRC_Y"sv) { prop_src_y = val; - } - else if (prop->name == "SRC_W"sv) { + } else if (prop->name == "SRC_W"sv) { prop_src_w = val; - } - else if (prop->name == "SRC_H"sv) { + } else if (prop->name == "SRC_H"sv) { prop_src_h = val; } } @@ -951,15 +922,13 @@ namespace platf { if (!plane->fb_id) { captured_cursor.visible = false; captured_cursor.fb_id = 0; - } - else if (plane->fb_id != captured_cursor.fb_id) { + } else if (plane->fb_id != captured_cursor.fb_id) { BOOST_LOG(debug) << "Refreshing cursor image after FB changed"sv; cursor_dirty = true; - } - else if (*prop_src_x != captured_cursor.prop_src_x || - *prop_src_y != captured_cursor.prop_src_y || - *prop_src_w != captured_cursor.prop_src_w || - *prop_src_h != captured_cursor.prop_src_h) { + } else if (*prop_src_x != captured_cursor.prop_src_x || + *prop_src_y != captured_cursor.prop_src_y || + *prop_src_w != captured_cursor.prop_src_w || + *prop_src_h != captured_cursor.prop_src_h) { BOOST_LOG(debug) << "Refreshing cursor image after source dimensions changed"sv; cursor_dirty = true; } @@ -1041,8 +1010,7 @@ namespace platf { // If the image is tightly packed, copy it in one shot if (fb->pitches[0] == src_w * 4 && src_x == 0) { memcpy(captured_cursor.pixels.data(), &((std::uint8_t *) mapped_data)[src_y * fb->pitches[0]], src_h * fb->pitches[0]); - } - else { + } else { // Copy row by row to deal with mismatched pitch or an X offset auto pixel_dst = captured_cursor.pixels.data(); for (int y = 0; y < src_h; y++) { @@ -1068,8 +1036,7 @@ namespace platf { } } - inline capture_e - refresh(file_t *file, egl::surface_descriptor_t *sd, std::optional &frame_timestamp) { + inline capture_e refresh(file_t *file, egl::surface_descriptor_t *sd, std::optional &frame_timestamp) { // Check for a change in HDR metadata if (connector_id) { auto connector_props = card.connector_props(*connector_id); @@ -1123,7 +1090,8 @@ namespace platf { if ( fb->width != img_width || - fb->height != img_height) { + fb->height != img_height + ) { return capture_e::reinit; } @@ -1155,10 +1123,10 @@ namespace platf { class display_ram_t: public display_t { public: display_ram_t(mem_type_e mem_type): - display_t(mem_type) {} + display_t(mem_type) { + } - int - init(const std::string &display_name, const ::video::config_t &config) { + int init(const std::string &display_name, const ::video::config_t &config) { if (!gbm::create_device) { BOOST_LOG(warning) << "libgbm not initialized"sv; return -1; @@ -1189,8 +1157,7 @@ namespace platf { return 0; } - capture_e - capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override { + capture_e capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override { auto next_frame = std::chrono::steady_clock::now(); sleep_overshoot_logger.reset(); @@ -1235,8 +1202,7 @@ namespace platf { return capture_e::ok; } - std::unique_ptr - make_avcodec_encode_device(pix_fmt_e pix_fmt) override { + std::unique_ptr make_avcodec_encode_device(pix_fmt_e pix_fmt) override { #ifdef SUNSHINE_BUILD_VAAPI if (mem_type == mem_type_e::vaapi) { return va::make_avcodec_encode_device(width, height, false); @@ -1252,8 +1218,7 @@ namespace platf { return std::make_unique(); } - void - blend_cursor(img_t &img) { + void blend_cursor(img_t &img) { // TODO: Cursor scaling is not supported in this codepath. // We always draw the cursor at the source size. auto pixels = (int *) img.data; @@ -1290,8 +1255,7 @@ namespace platf { auto alpha = (*(uint *) &cursor_pixel) >> 24u; if (alpha == 255) { *pixels_begin = cursor_pixel; - } - else { + } else { auto colors_out = (uint8_t *) &cursor_pixel; colors_in[0] = colors_out[0] + (colors_in[0] * (255 - alpha) + 255 / 2) / 255; colors_in[1] = colors_out[1] + (colors_in[1] * (255 - alpha) + 255 / 2) / 255; @@ -1302,8 +1266,7 @@ namespace platf { } } - capture_e - snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor) { + capture_e snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor) { file_t fb_fd[4]; egl::surface_descriptor_t sd; @@ -1345,8 +1308,7 @@ namespace platf { return capture_e::ok; } - std::shared_ptr - alloc_img() override { + std::shared_ptr alloc_img() override { auto img = std::make_shared(); img->width = width; img->height = height; @@ -1357,8 +1319,7 @@ namespace platf { return img; } - int - dummy_img(platf::img_t *img) override { + int dummy_img(platf::img_t *img) override { return 0; } @@ -1370,10 +1331,10 @@ namespace platf { class display_vram_t: public display_t { public: display_vram_t(mem_type_e mem_type): - display_t(mem_type) {} + display_t(mem_type) { + } - std::unique_ptr - make_avcodec_encode_device(pix_fmt_e pix_fmt) override { + std::unique_ptr make_avcodec_encode_device(pix_fmt_e pix_fmt) override { #ifdef SUNSHINE_BUILD_VAAPI if (mem_type == mem_type_e::vaapi) { return va::make_avcodec_encode_device(width, height, dup(card.render_fd.el), img_offset_x, img_offset_y, true); @@ -1390,8 +1351,7 @@ namespace platf { return nullptr; } - std::shared_ptr - alloc_img() override { + std::shared_ptr alloc_img() override { auto img = std::make_shared(); img->width = width; @@ -1406,14 +1366,12 @@ namespace platf { return img; } - int - dummy_img(platf::img_t *img) override { + int dummy_img(platf::img_t *img) override { // Empty images are recognized as dummies by the zero sequence number return 0; } - capture_e - capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) { + capture_e capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) { auto next_frame = std::chrono::steady_clock::now(); sleep_overshoot_logger.reset(); @@ -1458,8 +1416,7 @@ namespace platf { return capture_e::ok; } - capture_e - snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds /* timeout */, bool cursor) { + capture_e snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds /* timeout */, bool cursor) { file_t fb_fd[4]; if (!pull_free_image_cb(img_out)) { @@ -1491,8 +1448,7 @@ namespace platf { img->pixel_pitch = 4; img->row_pitch = img->pixel_pitch * img->width; img->data = img->buffer.data(); - } - else { + } else { img->data = nullptr; } @@ -1502,8 +1458,7 @@ namespace platf { return capture_e::ok; } - int - init(const std::string &display_name, const ::video::config_t &config) { + int init(const std::string &display_name, const ::video::config_t &config) { if (display_t::init(display_name, config)) { return -1; } @@ -1530,8 +1485,7 @@ namespace platf { } // namespace kms - std::shared_ptr - kms_display(mem_type_e hwdevice_type, const std::string &display_name, const ::video::config_t &config) { + std::shared_ptr kms_display(mem_type_e hwdevice_type, const std::string &display_name, const ::video::config_t &config) { if (hwdevice_type == mem_type_e::vaapi || hwdevice_type == mem_type_e::cuda) { auto disp = std::make_shared(hwdevice_type); @@ -1561,8 +1515,7 @@ namespace platf { * * This is an ugly hack :( */ - void - correlate_to_wayland(std::vector &cds) { + void correlate_to_wayland(std::vector &cds) { auto monitors = wl::monitors(); BOOST_LOG(info) << "-------- Start of KMS monitor list --------"sv; @@ -1578,8 +1531,7 @@ namespace platf { std::uint32_t index; if (index_begin == std::string_view::npos) { index = 1; - } - else { + } else { index = std::max(1, util::from_view(name.substr(index_begin + 1))); } @@ -1594,7 +1546,8 @@ namespace platf { // A sanity check, it's guesswork after all. if ( monitor_descriptor.viewport.width != monitor->viewport.width || - monitor_descriptor.viewport.height != monitor->viewport.height) { + monitor_descriptor.viewport.height != monitor->viewport.height + ) { BOOST_LOG(warning) << "Mismatch on expected Resolution compared to actual resolution: "sv << monitor_descriptor.viewport.width << 'x' << monitor_descriptor.viewport.height @@ -1616,8 +1569,7 @@ namespace platf { } // A list of names of displays accepted as display_name - std::vector - kms_display_names(mem_type_e hwdevice_type) { + std::vector kms_display_names(mem_type_e hwdevice_type) { int count = 0; if (!fs::exists("/dev/dri")) { @@ -1635,12 +1587,12 @@ namespace platf { std::vector cds; std::vector display_names; - fs::path card_dir { "/dev/dri"sv }; - for (auto &entry : fs::directory_iterator { card_dir }) { + fs::path card_dir {"/dev/dri"sv}; + for (auto &entry : fs::directory_iterator {card_dir}) { auto file = entry.path().filename(); auto filestring = file.generic_string(); - if (std::string_view { filestring }.substr(0, 4) != "card"sv) { + if (std::string_view {filestring}.substr(0, 4) != "card"sv) { continue; } @@ -1655,8 +1607,7 @@ namespace platf { BOOST_LOG(debug) << file << " is not a CUDA device"sv; if (config::video.encoder == "nvenc") { BOOST_LOG(warning) << "Using NVENC with your display connected to a different GPU may not work properly!"sv; - } - else { + } else { continue; } } @@ -1685,7 +1636,7 @@ namespace platf { BOOST_LOG((window_system != window_system_e::X11 || config::video.capture == "kms") ? fatal : error) << "You must run [sudo setcap cap_sys_admin+p $(readlink -f $(which sunshine))] for KMS display capture to work!\n"sv << "If you installed from AppImage or Flatpak, please refer to the official documentation:\n"sv - << "https://docs.lizardbyte.dev/projects/sunshine/en/latest/about/setup.html#install"sv; + << "https://docs.lizardbyte.dev/projects/sunshine/latest/md_docs_2getting__started.html#linux"sv; break; } diff --git a/src/platform/linux/misc.cpp b/src/platform/linux/misc.cpp index 05c5a264e76..81c08ed4bc5 100644 --- a/src/platform/linux/misc.cpp +++ b/src/platform/linux/misc.cpp @@ -12,16 +12,18 @@ #include #include -// lib includes +// platform includes #include -#include -#include -#include #include -#include #include #include #include + +// lib includes +#include +#include +#include +#include #include // local includes @@ -46,8 +48,7 @@ namespace bp = boost::process; window_system_e window_system; namespace dyn { - void * - handle(const std::vector &libs) { + void *handle(const std::vector &libs) { void *handle; for (auto lib : libs) { @@ -70,8 +71,7 @@ namespace dyn { return nullptr; } - int - load(void *handle, const std::vector> &funcs, bool strict) { + int load(void *handle, const std::vector> &funcs, bool strict) { int err = 0; for (auto &func : funcs) { TUPLE_2D_REF(fn, name, func); @@ -88,16 +88,16 @@ namespace dyn { return err; } } // namespace dyn + namespace platf { using ifaddr_t = util::safe_ptr; - ifaddr_t - get_ifaddrs() { - ifaddrs *p { nullptr }; + ifaddr_t get_ifaddrs() { + ifaddrs *p {nullptr}; getifaddrs(&p); - return ifaddr_t { p }; + return ifaddr_t {p}; } /** @@ -105,8 +105,7 @@ namespace platf { * @details This is used for the log directory, so it cannot invoke Boost logging! * @return The path of the appdata directory that should be used. */ - fs::path - appdata() { + fs::path appdata() { static std::once_flag migration_flag; static fs::path config_path; @@ -172,8 +171,7 @@ namespace platf { std::cerr << "Migration failed: " << ec.message() << std::endl; config_path = old_config_path; } - } - else { + } else { // We cannot use Boost logging because it hasn't been initialized yet! std::cerr << "Config exists in both "sv << old_config_path << " and "sv << config_path << ". Using "sv << config_path << " for config" << std::endl; std::cerr << "It is recommended to remove "sv << old_config_path << std::endl; @@ -185,45 +183,36 @@ namespace platf { return config_path; } - std::string - from_sockaddr(const sockaddr *const ip_addr) { + std::string from_sockaddr(const sockaddr *const ip_addr) { char data[INET6_ADDRSTRLEN] = {}; auto family = ip_addr->sa_family; if (family == AF_INET6) { - inet_ntop(AF_INET6, &((sockaddr_in6 *) ip_addr)->sin6_addr, data, - INET6_ADDRSTRLEN); - } - else if (family == AF_INET) { - inet_ntop(AF_INET, &((sockaddr_in *) ip_addr)->sin_addr, data, - INET_ADDRSTRLEN); + inet_ntop(AF_INET6, &((sockaddr_in6 *) ip_addr)->sin6_addr, data, INET6_ADDRSTRLEN); + } else if (family == AF_INET) { + inet_ntop(AF_INET, &((sockaddr_in *) ip_addr)->sin_addr, data, INET_ADDRSTRLEN); } - return std::string { data }; + return std::string {data}; } - std::pair - from_sockaddr_ex(const sockaddr *const ip_addr) { + std::pair from_sockaddr_ex(const sockaddr *const ip_addr) { char data[INET6_ADDRSTRLEN] = {}; auto family = ip_addr->sa_family; std::uint16_t port = 0; if (family == AF_INET6) { - inet_ntop(AF_INET6, &((sockaddr_in6 *) ip_addr)->sin6_addr, data, - INET6_ADDRSTRLEN); + inet_ntop(AF_INET6, &((sockaddr_in6 *) ip_addr)->sin6_addr, data, INET6_ADDRSTRLEN); port = ((sockaddr_in6 *) ip_addr)->sin6_port; - } - else if (family == AF_INET) { - inet_ntop(AF_INET, &((sockaddr_in *) ip_addr)->sin_addr, data, - INET_ADDRSTRLEN); + } else if (family == AF_INET) { + inet_ntop(AF_INET, &((sockaddr_in *) ip_addr)->sin_addr, data, INET_ADDRSTRLEN); port = ((sockaddr_in *) ip_addr)->sin_port; } - return { port, std::string { data } }; + return {port, std::string {data}}; } - std::string - get_mac_address(const std::string_view &address) { + std::string get_mac_address(const std::string_view &address) { auto ifaddrs = get_ifaddrs(); for (auto pos = ifaddrs.get(); pos != nullptr; pos = pos->ifa_next) { if (pos->ifa_addr && address == from_sockaddr(pos->ifa_addr)) { @@ -240,8 +229,7 @@ namespace platf { return "00:00:00:00:00:00"s; } - bp::child - run_command(bool elevated, bool interactive, const std::string &cmd, boost::filesystem::path &working_dir, const bp::environment &env, FILE *file, std::error_code &ec, bp::group *group) { + bp::child run_command(bool elevated, bool interactive, const std::string &cmd, boost::filesystem::path &working_dir, const bp::environment &env, FILE *file, std::error_code &ec, bp::group *group) { // clang-format off if (!group) { if (!file) { @@ -266,8 +254,7 @@ namespace platf { * @brief Open a url in the default web browser. * @param url The url to open. */ - void - open_url(const std::string &url) { + void open_url(const std::string &url) { // set working dir to user home directory auto working_dir = boost::filesystem::path(std::getenv("HOME")); std::string cmd = R"(xdg-open ")" + url + R"(")"; @@ -277,30 +264,25 @@ namespace platf { auto child = run_command(false, false, cmd, working_dir, _env, nullptr, ec, nullptr); if (ec) { BOOST_LOG(warning) << "Couldn't open url ["sv << url << "]: System: "sv << ec.message(); - } - else { + } else { BOOST_LOG(info) << "Opened url ["sv << url << "]"sv; child.detach(); } } - void - adjust_thread_priority(thread_priority_e priority) { + void adjust_thread_priority(thread_priority_e priority) { // Unimplemented } - void - streaming_will_start() { + void streaming_will_start() { // Nothing to do } - void - streaming_will_stop() { + void streaming_will_stop() { // Nothing to do } - void - restart_on_exit() { + void restart_on_exit() { char executable[PATH_MAX]; ssize_t len = readlink("/proc/self/exe", executable, PATH_MAX - 1); if (len == -1) { @@ -322,42 +304,35 @@ namespace platf { } } - void - restart() { + void restart() { // Gracefully clean up and restart ourselves instead of exiting atexit(restart_on_exit); lifetime::exit_sunshine(0, true); } - int - set_env(const std::string &name, const std::string &value) { + int set_env(const std::string &name, const std::string &value) { return setenv(name.c_str(), value.c_str(), 1); } - int - unset_env(const std::string &name) { + int unset_env(const std::string &name) { return unsetenv(name.c_str()); } - bool - request_process_group_exit(std::uintptr_t native_handle) { + bool request_process_group_exit(std::uintptr_t native_handle) { if (kill(-((pid_t) native_handle), SIGTERM) == 0 || errno == ESRCH) { BOOST_LOG(debug) << "Successfully sent SIGTERM to process group: "sv << native_handle; return true; - } - else { + } else { BOOST_LOG(warning) << "Unable to send SIGTERM to process group ["sv << native_handle << "]: "sv << errno; return false; } } - bool - process_group_running(std::uintptr_t native_handle) { + bool process_group_running(std::uintptr_t native_handle) { return waitpid(-((pid_t) native_handle), nullptr, WNOHANG) >= 0; } - struct sockaddr_in - to_sockaddr(boost::asio::ip::address_v4 address, uint16_t port) { + struct sockaddr_in to_sockaddr(boost::asio::ip::address_v4 address, uint16_t port) { struct sockaddr_in saddr_v4 = {}; saddr_v4.sin_family = AF_INET; @@ -369,8 +344,7 @@ namespace platf { return saddr_v4; } - struct sockaddr_in6 - to_sockaddr(boost::asio::ip::address_v6 address, uint16_t port) { + struct sockaddr_in6 to_sockaddr(boost::asio::ip::address_v6 address, uint16_t port) { struct sockaddr_in6 saddr_v6 = {}; saddr_v6.sin6_family = AF_INET6; @@ -383,8 +357,7 @@ namespace platf { return saddr_v6; } - bool - send_batch(batched_send_info_t &send_info) { + bool send_batch(batched_send_info_t &send_info) { auto sockfd = (int) send_info.native_socket; struct msghdr msg = {}; @@ -396,8 +369,7 @@ namespace platf { msg.msg_name = (struct sockaddr *) &taddr_v6; msg.msg_namelen = sizeof(taddr_v6); - } - else { + } else { taddr_v4 = to_sockaddr(send_info.target_address.to_v4(), send_info.target_port); msg.msg_name = (struct sockaddr *) &taddr_v4; @@ -405,10 +377,10 @@ namespace platf { } union { - char buf[CMSG_SPACE(sizeof(uint16_t)) + - std::max(CMSG_SPACE(sizeof(struct in_pktinfo)), CMSG_SPACE(sizeof(struct in6_pktinfo)))]; + char buf[CMSG_SPACE(sizeof(uint16_t)) + std::max(CMSG_SPACE(sizeof(struct in_pktinfo)), CMSG_SPACE(sizeof(struct in6_pktinfo)))]; struct cmsghdr alignment; } cmbuf = {}; // Must be zeroed for CMSG_NXTHDR() + socklen_t cmbuflen = 0; msg.msg_control = cmbuf.buf; @@ -430,8 +402,7 @@ namespace platf { pktinfo_cm->cmsg_type = IPV6_PKTINFO; pktinfo_cm->cmsg_len = CMSG_LEN(sizeof(pktInfo)); memcpy(CMSG_DATA(pktinfo_cm), &pktInfo, sizeof(pktInfo)); - } - else { + } else { struct in_pktinfo pktInfo; struct sockaddr_in saddr_v4 = to_sockaddr(send_info.source_address.to_v4(), 0); @@ -469,8 +440,7 @@ namespace platf { iovs[iovlen].iov_len = send_info.payload_size; iovlen++; } - } - else { + } else { // Translate buffer descriptors into iovs auto payload_offset = (send_info.block_offset + seg_index) * send_info.payload_size; auto payload_length = payload_offset + (segs_in_batch * send_info.payload_size); @@ -496,8 +466,7 @@ namespace platf { cm->cmsg_type = UDP_SEGMENT; cm->cmsg_len = CMSG_LEN(sizeof(uint16_t)); *((uint16_t *) CMSG_DATA(cm)) = msg_size; - } - else { + } else { msg.msg_controllen = cmbuflen; } @@ -593,8 +562,7 @@ namespace platf { } } - bool - send(send_info_t &send_info) { + bool send(send_info_t &send_info) { auto sockfd = (int) send_info.native_socket; struct msghdr msg = {}; @@ -606,8 +574,7 @@ namespace platf { msg.msg_name = (struct sockaddr *) &taddr_v6; msg.msg_namelen = sizeof(taddr_v6); - } - else { + } else { taddr_v4 = to_sockaddr(send_info.target_address.to_v4(), send_info.target_port); msg.msg_name = (struct sockaddr *) &taddr_v4; @@ -618,6 +585,7 @@ namespace platf { char buf[std::max(CMSG_SPACE(sizeof(struct in_pktinfo)), CMSG_SPACE(sizeof(struct in6_pktinfo)))]; struct cmsghdr alignment; } cmbuf; + socklen_t cmbuflen = 0; msg.msg_control = cmbuf.buf; @@ -637,8 +605,7 @@ namespace platf { pktinfo_cm->cmsg_type = IPV6_PKTINFO; pktinfo_cm->cmsg_len = CMSG_LEN(sizeof(pktInfo)); memcpy(CMSG_DATA(pktinfo_cm), &pktInfo, sizeof(pktInfo)); - } - else { + } else { struct in_pktinfo pktInfo; struct sockaddr_in saddr_v4 = to_sockaddr(send_info.source_address.to_v4(), 0); @@ -703,7 +670,8 @@ namespace platf { class qos_t: public deinit_t { public: qos_t(int sockfd, std::vector> options): - sockfd(sockfd), options(options) { + sockfd(sockfd), + options(options) { qos_ref_count++; } @@ -731,8 +699,7 @@ namespace platf { * @param data_type The type of traffic sent on this socket. * @param dscp_tagging Specifies whether to enable DSCP tagging on outgoing traffic. */ - std::unique_ptr - enable_socket_qos(uintptr_t native_socket, boost::asio::ip::address &address, uint16_t port, qos_data_type_e data_type, bool dscp_tagging) { + std::unique_ptr enable_socket_qos(uintptr_t native_socket, boost::asio::ip::address &address, uint16_t port, qos_data_type_e data_type, bool dscp_tagging) { int sockfd = (int) native_socket; std::vector> reset_options; @@ -745,8 +712,7 @@ namespace platf { if (address.is_v6() && !address.to_v6().is_v4_mapped()) { level = SOL_IPV6; option = IPV6_TCLASS; - } - else { + } else { level = SOL_IP; option = IP_TOS; } @@ -773,8 +739,7 @@ namespace platf { if (setsockopt(sockfd, level, option, &dscp, sizeof(dscp)) == 0) { // Reset TOS to -1 when QoS is disabled reset_options.emplace_back(std::make_tuple(level, option, -1)); - } - else { + } else { BOOST_LOG(error) << "Failed to set TOS/TCLASS: "sv << errno; } } @@ -790,20 +755,17 @@ namespace platf { if (setsockopt(sockfd, SOL_SOCKET, SO_PRIORITY, &priority, sizeof(priority)) == 0) { // Reset SO_PRIORITY to 0 when QoS is disabled reset_options.emplace_back(std::make_tuple(SOL_SOCKET, SO_PRIORITY, 0)); - } - else { + } else { BOOST_LOG(error) << "Failed to set SO_PRIORITY: "sv << errno; } return std::make_unique(sockfd, reset_options); } - std::string - get_host_name() { + std::string get_host_name() { try { return boost::asio::ip::host_name(); - } - catch (boost::system::system_error &err) { + } catch (boost::system::system_error &err) { BOOST_LOG(error) << "Failed to get hostname: "sv << err.what(); return "Sunshine"s; } @@ -830,67 +792,62 @@ namespace platf { static std::bitset sources; #ifdef SUNSHINE_BUILD_CUDA - std::vector - nvfbc_display_names(); - std::shared_ptr - nvfbc_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config); + std::vector nvfbc_display_names(); + std::shared_ptr nvfbc_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config); - bool - verify_nvfbc() { + bool verify_nvfbc() { return !nvfbc_display_names().empty(); } #endif #ifdef SUNSHINE_BUILD_WAYLAND - std::vector - wl_display_names(); - std::shared_ptr - wl_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config); + std::vector wl_display_names(); + std::shared_ptr wl_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config); - bool - verify_wl() { + bool verify_wl() { return window_system == window_system_e::WAYLAND && !wl_display_names().empty(); } #endif #ifdef SUNSHINE_BUILD_DRM - std::vector - kms_display_names(mem_type_e hwdevice_type); - std::shared_ptr - kms_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config); + std::vector kms_display_names(mem_type_e hwdevice_type); + std::shared_ptr kms_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config); - bool - verify_kms() { + bool verify_kms() { return !kms_display_names(mem_type_e::unknown).empty(); } #endif #ifdef SUNSHINE_BUILD_X11 - std::vector - x11_display_names(); - std::shared_ptr - x11_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config); + std::vector x11_display_names(); + std::shared_ptr x11_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config); - bool - verify_x11() { + bool verify_x11() { return window_system == window_system_e::X11 && !x11_display_names().empty(); } #endif - std::vector - display_names(mem_type_e hwdevice_type) { + std::vector display_names(mem_type_e hwdevice_type) { #ifdef SUNSHINE_BUILD_CUDA // display using NvFBC only supports mem_type_e::cuda - if (sources[source::NVFBC] && hwdevice_type == mem_type_e::cuda) return nvfbc_display_names(); + if (sources[source::NVFBC] && hwdevice_type == mem_type_e::cuda) { + return nvfbc_display_names(); + } #endif #ifdef SUNSHINE_BUILD_WAYLAND - if (sources[source::WAYLAND]) return wl_display_names(); + if (sources[source::WAYLAND]) { + return wl_display_names(); + } #endif #ifdef SUNSHINE_BUILD_DRM - if (sources[source::KMS]) return kms_display_names(hwdevice_type); + if (sources[source::KMS]) { + return kms_display_names(hwdevice_type); + } #endif #ifdef SUNSHINE_BUILD_X11 - if (sources[source::X11]) return x11_display_names(); + if (sources[source::X11]) { + return x11_display_names(); + } #endif return {}; } @@ -899,14 +856,12 @@ namespace platf { * @brief Returns if GPUs/drivers have changed since the last call to this function. * @return `true` if a change has occurred or if it is unknown whether a change occurred. */ - bool - needs_encoder_reenumeration() { + bool needs_encoder_reenumeration() { // We don't track GPU state, so we will always reenumerate. Fortunately, it is fast on Linux. return true; } - std::shared_ptr - display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config) { + std::shared_ptr display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config) { #ifdef SUNSHINE_BUILD_CUDA if (sources[source::NVFBC] && hwdevice_type == mem_type_e::cuda) { BOOST_LOG(info) << "Screencasting with NvFBC"sv; @@ -935,8 +890,7 @@ namespace platf { return nullptr; } - std::unique_ptr - init() { + std::unique_ptr init() { // enable low latency mode for AMD // https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/30039 set_env("AMD_DEBUG", "lowlatencyenc"); @@ -1005,8 +959,7 @@ namespace platf { class linux_high_precision_timer: public high_precision_timer { public: - void - sleep_for(const std::chrono::nanoseconds &duration) override { + void sleep_for(const std::chrono::nanoseconds &duration) override { std::this_thread::sleep_for(duration); } @@ -1015,8 +968,7 @@ namespace platf { } }; - std::unique_ptr - create_high_precision_timer() { + std::unique_ptr create_high_precision_timer() { return std::make_unique(); } } // namespace platf diff --git a/src/platform/linux/misc.h b/src/platform/linux/misc.h index b084c9853f3..c9f98f44de2 100644 --- a/src/platform/linux/misc.h +++ b/src/platform/linux/misc.h @@ -4,9 +4,11 @@ */ #pragma once +// standard includes #include #include +// local includes #include "src/utility.h" KITTY_USING_MOVE_T(file_t, int, -1, { @@ -26,9 +28,7 @@ extern window_system_e window_system; namespace dyn { typedef void (*apiproc)(void); - int - load(void *handle, const std::vector> &funcs, bool strict = true); - void * - handle(const std::vector &libs); + int load(void *handle, const std::vector> &funcs, bool strict = true); + void *handle(const std::vector &libs); } // namespace dyn diff --git a/src/platform/linux/publish.cpp b/src/platform/linux/publish.cpp index 9b7dbbc542d..a2bac72f820 100644 --- a/src/platform/linux/publish.cpp +++ b/src/platform/linux/publish.cpp @@ -3,8 +3,10 @@ * @brief Definitions for publishing services on Linux. * @note Adapted from https://www.avahi.org/doxygen/html/client-publish-service_8c-example.html */ +// standard includes #include +// local includes #include "misc.h" #include "src/logging.h" #include "src/network.h" @@ -84,6 +86,7 @@ namespace avahi { }; constexpr auto IF_UNSPEC = -1; + enum proto { PROTO_INET = 0, ///< IPv4 PROTO_INET6 = 1, ///< IPv6 @@ -164,7 +167,8 @@ namespace avahi { const char *domain, const char *host, uint16_t port, - ...); + ... + ); typedef int (*entry_group_is_empty_fn)(EntryGroup *); typedef int (*entry_group_reset_fn)(EntryGroup *); @@ -199,30 +203,31 @@ namespace avahi { simple_poll_new_fn simple_poll_new; simple_poll_free_fn simple_poll_free; - int - init_common() { - static void *handle { nullptr }; + int init_common() { + static void *handle {nullptr}; static bool funcs_loaded = false; - if (funcs_loaded) return 0; + if (funcs_loaded) { + return 0; + } if (!handle) { - handle = dyn::handle({ "libavahi-common.so.3", "libavahi-common.so" }); + handle = dyn::handle({"libavahi-common.so.3", "libavahi-common.so"}); if (!handle) { return -1; } } std::vector> funcs { - { (dyn::apiproc *) &alternative_service_name, "avahi_alternative_service_name" }, - { (dyn::apiproc *) &free, "avahi_free" }, - { (dyn::apiproc *) &strdup, "avahi_strdup" }, - { (dyn::apiproc *) &strerror, "avahi_strerror" }, - { (dyn::apiproc *) &simple_poll_get, "avahi_simple_poll_get" }, - { (dyn::apiproc *) &simple_poll_loop, "avahi_simple_poll_loop" }, - { (dyn::apiproc *) &simple_poll_quit, "avahi_simple_poll_quit" }, - { (dyn::apiproc *) &simple_poll_new, "avahi_simple_poll_new" }, - { (dyn::apiproc *) &simple_poll_free, "avahi_simple_poll_free" }, + {(dyn::apiproc *) &alternative_service_name, "avahi_alternative_service_name"}, + {(dyn::apiproc *) &free, "avahi_free"}, + {(dyn::apiproc *) &strdup, "avahi_strdup"}, + {(dyn::apiproc *) &strerror, "avahi_strerror"}, + {(dyn::apiproc *) &simple_poll_get, "avahi_simple_poll_get"}, + {(dyn::apiproc *) &simple_poll_loop, "avahi_simple_poll_loop"}, + {(dyn::apiproc *) &simple_poll_quit, "avahi_simple_poll_quit"}, + {(dyn::apiproc *) &simple_poll_new, "avahi_simple_poll_new"}, + {(dyn::apiproc *) &simple_poll_free, "avahi_simple_poll_free"}, }; if (dyn::load(handle, funcs)) { @@ -233,34 +238,35 @@ namespace avahi { return 0; } - int - init_client() { + int init_client() { if (init_common()) { return -1; } - static void *handle { nullptr }; + static void *handle {nullptr}; static bool funcs_loaded = false; - if (funcs_loaded) return 0; + if (funcs_loaded) { + return 0; + } if (!handle) { - handle = dyn::handle({ "libavahi-client.so.3", "libavahi-client.so" }); + handle = dyn::handle({"libavahi-client.so.3", "libavahi-client.so"}); if (!handle) { return -1; } } std::vector> funcs { - { (dyn::apiproc *) &client_new, "avahi_client_new" }, - { (dyn::apiproc *) &client_free, "avahi_client_free" }, - { (dyn::apiproc *) &entry_group_get_client, "avahi_entry_group_get_client" }, - { (dyn::apiproc *) &entry_group_new, "avahi_entry_group_new" }, - { (dyn::apiproc *) &entry_group_add_service, "avahi_entry_group_add_service" }, - { (dyn::apiproc *) &entry_group_is_empty, "avahi_entry_group_is_empty" }, - { (dyn::apiproc *) &entry_group_reset, "avahi_entry_group_reset" }, - { (dyn::apiproc *) &entry_group_commit, "avahi_entry_group_commit" }, - { (dyn::apiproc *) &client_errno, "avahi_client_errno" }, + {(dyn::apiproc *) &client_new, "avahi_client_new"}, + {(dyn::apiproc *) &client_free, "avahi_client_free"}, + {(dyn::apiproc *) &entry_group_get_client, "avahi_entry_group_get_client"}, + {(dyn::apiproc *) &entry_group_new, "avahi_entry_group_new"}, + {(dyn::apiproc *) &entry_group_add_service, "avahi_entry_group_add_service"}, + {(dyn::apiproc *) &entry_group_is_empty, "avahi_entry_group_is_empty"}, + {(dyn::apiproc *) &entry_group_reset, "avahi_entry_group_reset"}, + {(dyn::apiproc *) &entry_group_commit, "avahi_entry_group_commit"}, + {(dyn::apiproc *) &client_errno, "avahi_client_errno"}, }; if (dyn::load(handle, funcs)) { @@ -274,13 +280,12 @@ namespace avahi { namespace platf::publish { - template - void - free(T *p) { + template + void free(T *p) { avahi::free(p); } - template + template using ptr_t = util::safe_ptr>; using client_t = util::dyn_safe_ptr; using poll_t = util::dyn_safe_ptr; @@ -292,11 +297,9 @@ namespace platf::publish { ptr_t name; - void - create_services(avahi::Client *c); + void create_services(avahi::Client *c); - void - entry_group_callback(avahi::EntryGroup *g, avahi::EntryGroupState state, void *) { + void entry_group_callback(avahi::EntryGroup *g, avahi::EntryGroupState state, void *) { group = g; switch (state) { @@ -319,8 +322,7 @@ namespace platf::publish { } } - void - create_services(avahi::Client *c) { + void create_services(avahi::Client *c) { int ret; auto fg = util::fail_guard([]() { @@ -339,13 +341,16 @@ namespace platf::publish { ret = avahi::entry_group_add_service( group, - avahi::IF_UNSPEC, avahi::PROTO_UNSPEC, + avahi::IF_UNSPEC, + avahi::PROTO_UNSPEC, avahi::PublishFlags(0), name.get(), SERVICE_TYPE, - nullptr, nullptr, + nullptr, + nullptr, net::map_port(nvhttp::PORT_HTTP), - nullptr); + nullptr + ); if (ret < 0) { if (ret == avahi::ERR_COLLISION) { @@ -375,8 +380,7 @@ namespace platf::publish { fg.disable(); } - void - client_callback(avahi::Client *c, avahi::ClientState state, void *) { + void client_callback(avahi::Client *c, avahi::ClientState state, void *) { switch (state) { case avahi::CLIENT_S_RUNNING: create_services(c); @@ -387,8 +391,9 @@ namespace platf::publish { break; case avahi::CLIENT_S_COLLISION: case avahi::CLIENT_S_REGISTERING: - if (group) + if (group) { avahi::entry_group_reset(group); + } break; case avahi::CLIENT_CONNECTING:; } @@ -399,7 +404,8 @@ namespace platf::publish { std::thread poll_thread; deinit_t(std::thread poll_thread): - poll_thread { std::move(poll_thread) } {} + poll_thread {std::move(poll_thread)} { + } ~deinit_t() override { if (avahi::simple_poll_quit && poll) { @@ -412,8 +418,7 @@ namespace platf::publish { } }; - [[nodiscard]] std::unique_ptr<::platf::deinit_t> - start() { + [[nodiscard]] std::unique_ptr<::platf::deinit_t> start() { if (avahi::init_client()) { return nullptr; } @@ -430,13 +435,14 @@ namespace platf::publish { name.reset(avahi::strdup(instance_name.c_str())); client.reset( - avahi::client_new(avahi::simple_poll_get(poll.get()), avahi::ClientFlags(0), client_callback, nullptr, &avhi_error)); + avahi::client_new(avahi::simple_poll_get(poll.get()), avahi::ClientFlags(0), client_callback, nullptr, &avhi_error) + ); if (!client) { BOOST_LOG(error) << "Failed to create client: "sv << avahi::strerror(avhi_error); return nullptr; } - return std::make_unique(std::thread { avahi::simple_poll_loop, poll.get() }); + return std::make_unique(std::thread {avahi::simple_poll_loop, poll.get()}); } } // namespace platf::publish diff --git a/src/platform/linux/vaapi.cpp b/src/platform/linux/vaapi.cpp index 909ade10548..88444d10cbc 100644 --- a/src/platform/linux/vaapi.cpp +++ b/src/platform/linux/vaapi.cpp @@ -2,28 +2,30 @@ * @file src/platform/linux/vaapi.cpp * @brief Definitions for VA-API hardware accelerated capture. */ +// standard includes +#include #include #include -#include - extern "C" { #include #include #include #include #if !VA_CHECK_VERSION(1, 9, 0) -// vaSyncBuffer stub allows Sunshine built against libva <2.9.0 to link against ffmpeg on libva 2.9.0 or later -VAStatus -vaSyncBuffer( - VADisplay dpy, - VABufferID buf_id, - uint64_t timeout_ns) { - return VA_STATUS_ERROR_UNIMPLEMENTED; -} + // vaSyncBuffer stub allows Sunshine built against libva <2.9.0 to link against ffmpeg on libva 2.9.0 or later + VAStatus + vaSyncBuffer( + VADisplay dpy, + VABufferID buf_id, + uint64_t timeout_ns + ) { + return VA_STATUS_ERROR_UNIMPLEMENTED; + } #endif } +// local includes #include "graphics.h" #include "misc.h" #include "src/config.h" @@ -69,6 +71,7 @@ namespace va { // Number of layers making up the surface. uint32_t num_layers; + struct { // DRM format fourcc of this layer (DRM_FOURCC_*). uint32_t drm_format; @@ -89,13 +92,11 @@ namespace va { using display_t = util::safe_ptr_v2; - int - vaapi_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *encode_device, AVBufferRef **hw_device_buf); + int vaapi_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *encode_device, AVBufferRef **hw_device_buf); class va_t: public platf::avcodec_encode_device_t { public: - int - init(int in_width, int in_height, file_t &&render_device) { + int init(int in_width, int in_height, file_t &&render_device) { file = std::move(render_device); if (!gbm::create_device) { @@ -135,8 +136,7 @@ namespace va { * @param profile The profile to match. * @return A valid encoding entrypoint or 0 on failure. */ - VAEntrypoint - select_va_entrypoint(VAProfile profile) { + VAEntrypoint select_va_entrypoint(VAProfile profile) { std::vector entrypoints(vaMaxNumEntrypoints(va_display)); int num_eps; auto status = vaQueryConfigEntrypoints(va_display, profile, entrypoints.data(), &num_eps); @@ -166,8 +166,7 @@ namespace va { * @param profile The profile to match. * @return Boolean value indicating if the profile is supported. */ - bool - is_va_profile_supported(VAProfile profile) { + bool is_va_profile_supported(VAProfile profile) { std::vector profiles(vaMaxNumProfiles(va_display)); int num_profs; auto status = vaQueryConfigProfiles(va_display, profiles.data(), &num_profs); @@ -185,13 +184,11 @@ namespace va { * @param ctx The FFmpeg codec context. * @return The matching VA profile or `VAProfileNone` on failure. */ - VAProfile - get_va_profile(AVCodecContext *ctx) { + VAProfile get_va_profile(AVCodecContext *ctx) { if (ctx->codec_id == AV_CODEC_ID_H264) { // There's no VAAPI profile for H.264 4:4:4 return VAProfileH264High; - } - else if (ctx->codec_id == AV_CODEC_ID_HEVC) { + } else if (ctx->codec_id == AV_CODEC_ID_HEVC) { switch (ctx->profile) { case FF_PROFILE_HEVC_REXT: switch (av_pix_fmt_desc_get(ctx->sw_pix_fmt)->comp[0].depth) { @@ -206,8 +203,7 @@ namespace va { case FF_PROFILE_HEVC_MAIN: return VAProfileHEVCMain; } - } - else if (ctx->codec_id == AV_CODEC_ID_AV1) { + } else if (ctx->codec_id == AV_CODEC_ID_AV1) { switch (ctx->profile) { case FF_PROFILE_AV1_HIGH: return VAProfileAV1Profile1; @@ -220,8 +216,7 @@ namespace va { return VAProfileNone; } - void - init_codec_options(AVCodecContext *ctx, AVDictionary **options) override { + void init_codec_options(AVCodecContext *ctx, AVDictionary **options) override { auto va_profile = get_va_profile(ctx); if (va_profile == VAProfileNone || !is_va_profile_supported(va_profile)) { // Don't bother doing anything if the profile isn't supported @@ -239,19 +234,18 @@ namespace va { if (va_entrypoint == VAEntrypointEncSliceLP) { BOOST_LOG(info) << "Using LP encoding mode"sv; av_dict_set_int(options, "low_power", 1, 0); - } - else { + } else { BOOST_LOG(info) << "Using normal encoding mode"sv; } - VAConfigAttrib rc_attr = { VAConfigAttribRateControl }; + VAConfigAttrib rc_attr = {VAConfigAttribRateControl}; auto status = vaGetConfigAttributes(va_display, va_profile, va_entrypoint, &rc_attr, 1); if (status != VA_STATUS_SUCCESS) { // Stick to the default rate control (CQP) rc_attr.value = 0; } - VAConfigAttrib slice_attr = { VAConfigAttribEncMaxSlices }; + VAConfigAttrib slice_attr = {VAConfigAttribEncMaxSlices}; status = vaGetConfigAttributes(va_display, va_profile, va_entrypoint, &slice_attr, 1); if (status != VA_STATUS_SUCCESS) { // Assume only a single slice is supported @@ -281,27 +275,22 @@ namespace va { if (rc_attr.value & VA_RC_VBR) { BOOST_LOG(info) << "Using VBR with single frame VBV size"sv; av_dict_set(options, "rc_mode", "VBR", 0); - } - else if (rc_attr.value & VA_RC_CBR) { + } else if (rc_attr.value & VA_RC_CBR) { BOOST_LOG(info) << "Using CBR with single frame VBV size"sv; av_dict_set(options, "rc_mode", "CBR", 0); - } - else { + } else { BOOST_LOG(warning) << "Using CQP with single frame VBV size"sv; av_dict_set_int(options, "qp", config::video.qp, 0); } - } - else if (!(rc_attr.value & (VA_RC_CBR | VA_RC_VBR))) { + } else if (!(rc_attr.value & (VA_RC_CBR | VA_RC_VBR))) { BOOST_LOG(warning) << "Using CQP rate control"sv; av_dict_set_int(options, "qp", config::video.qp, 0); - } - else { + } else { BOOST_LOG(info) << "Using default rate control"sv; } } - int - set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx_buf) override { + int set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx_buf) override { this->hwframe.reset(frame); this->frame = frame; @@ -321,7 +310,8 @@ namespace va { surface, va::SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2, va::EXPORT_SURFACE_WRITE_ONLY | va::EXPORT_SURFACE_SEPARATE_LAYERS, - &prime); + &prime + ); if (status) { BOOST_LOG(error) << "Couldn't export va surface handle: ["sv << (int) surface << "]: "sv << vaErrorStr(status); @@ -377,8 +367,7 @@ namespace va { return 0; } - void - apply_colorspace() override { + void apply_colorspace() override { sws.apply_colorspace(colorspace); } @@ -401,8 +390,7 @@ namespace va { class va_ram_t: public va_t { public: - int - convert(platf::img_t &img) override { + int convert(platf::img_t &img) override { sws.load_ram(img); sws.convert(nv12->buf); @@ -412,15 +400,13 @@ namespace va { class va_vram_t: public va_t { public: - int - convert(platf::img_t &img) override { + int convert(platf::img_t &img) override { auto &descriptor = (egl::img_descriptor_t &) img; if (descriptor.sequence == 0) { // For dummy images, use a blank RGB texture instead of importing a DMA-BUF rgb = egl::create_blank(img); - } - else if (descriptor.sequence > sequence) { + } else if (descriptor.sequence > sequence) { sequence = descriptor.sequence; rgb = egl::rgb_t {}; @@ -440,8 +426,7 @@ namespace va { return 0; } - int - init(int in_width, int in_height, file_t &&render_device, int offset_x, int offset_y) { + int init(int in_width, int in_height, file_t &&render_device, int offset_x, int offset_y) { if (va_t::init(in_width, in_height, std::move(render_device))) { return -1; } @@ -471,6 +456,7 @@ namespace va { void *xdisplay; int fd; } drm; + int drm_fd; } VAAPIDevicePriv; @@ -494,13 +480,11 @@ namespace va { unsigned int driver_quirks; } AVVAAPIDeviceContext; - static void - __log(void *level, const char *msg) { + static void __log(void *level, const char *msg) { BOOST_LOG(*(boost::log::sources::severity_logger *) level) << msg; } - static void - vaapi_hwdevice_ctx_free(AVHWDeviceContext *ctx) { + static void vaapi_hwdevice_ctx_free(AVHWDeviceContext *ctx) { auto hwctx = (AVVAAPIDeviceContext *) ctx->hwctx; auto priv = (VAAPIDevicePriv *) ctx->user_opaque; @@ -509,8 +493,7 @@ namespace va { av_freep(&priv); } - int - vaapi_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *base, AVBufferRef **hw_device_buf) { + int vaapi_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *base, AVBufferRef **hw_device_buf) { auto va = (va::va_t *) base; auto fd = dup(va->file.el); @@ -522,7 +505,7 @@ namespace va { av_free(priv); }); - va::display_t display { vaGetDisplayDRM(fd) }; + va::display_t display {vaGetDisplayDRM(fd)}; if (!display) { auto render_device = config::video.adapter_name.empty() ? "/dev/dri/renderD128" : config::video.adapter_name.c_str(); @@ -556,7 +539,7 @@ namespace va { auto err = av_hwdevice_ctx_init(*hw_device_buf); if (err) { - char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 }; + char err_str[AV_ERROR_MAX_STRING_SIZE] {0}; BOOST_LOG(error) << "Failed to create FFMpeg hardware device context: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err); return err; @@ -565,8 +548,7 @@ namespace va { return 0; } - static bool - query(display_t::pointer display, VAProfile profile) { + static bool query(display_t::pointer display, VAProfile profile) { std::vector entrypoints; entrypoints.resize(vaMaxNumEntrypoints(display)); @@ -587,15 +569,14 @@ namespace va { return false; } - bool - validate(int fd) { - va::display_t display { vaGetDisplayDRM(fd) }; + bool validate(int fd) { + va::display_t display {vaGetDisplayDRM(fd)}; if (!display) { char string[1024]; auto bytes = readlink(("/proc/self/fd/" + std::to_string(fd)).c_str(), string, sizeof(string)); - std::string_view render_device { string, (std::size_t) bytes }; + std::string_view render_device {string, (std::size_t) bytes}; BOOST_LOG(error) << "Couldn't open a va display from DRM with device: "sv << render_device; return false; @@ -623,8 +604,7 @@ namespace va { return true; } - std::unique_ptr - make_avcodec_encode_device(int width, int height, file_t &&card, int offset_x, int offset_y, bool vram) { + std::unique_ptr make_avcodec_encode_device(int width, int height, file_t &&card, int offset_x, int offset_y, bool vram) { if (vram) { auto egl = std::make_unique(); if (egl->init(width, height, std::move(card), offset_x, offset_y)) { @@ -644,8 +624,7 @@ namespace va { } } - std::unique_ptr - make_avcodec_encode_device(int width, int height, int offset_x, int offset_y, bool vram) { + std::unique_ptr make_avcodec_encode_device(int width, int height, int offset_x, int offset_y, bool vram) { auto render_device = config::video.adapter_name.empty() ? "/dev/dri/renderD128" : config::video.adapter_name.c_str(); file_t file = open(render_device, O_RDWR); @@ -659,8 +638,7 @@ namespace va { return make_avcodec_encode_device(width, height, std::move(file), offset_x, offset_y, vram); } - std::unique_ptr - make_avcodec_encode_device(int width, int height, bool vram) { + std::unique_ptr make_avcodec_encode_device(int width, int height, bool vram) { return make_avcodec_encode_device(width, height, 0, 0, vram); } } // namespace va diff --git a/src/platform/linux/vaapi.h b/src/platform/linux/vaapi.h index de1ea7021aa..490df1bccea 100644 --- a/src/platform/linux/vaapi.h +++ b/src/platform/linux/vaapi.h @@ -4,12 +4,14 @@ */ #pragma once +// local includes #include "misc.h" #include "src/platform/common.h" namespace egl { struct surface_descriptor_t; } + namespace va { /** * Width --> Width of the image @@ -18,14 +20,10 @@ namespace va { * offset_y --> Vertical offset of the image in the texture * file_t card --> The file descriptor of the render device used for encoding */ - std::unique_ptr - make_avcodec_encode_device(int width, int height, bool vram); - std::unique_ptr - make_avcodec_encode_device(int width, int height, int offset_x, int offset_y, bool vram); - std::unique_ptr - make_avcodec_encode_device(int width, int height, file_t &&card, int offset_x, int offset_y, bool vram); + std::unique_ptr make_avcodec_encode_device(int width, int height, bool vram); + std::unique_ptr make_avcodec_encode_device(int width, int height, int offset_x, int offset_y, bool vram); + std::unique_ptr make_avcodec_encode_device(int width, int height, file_t &&card, int offset_x, int offset_y, bool vram); // Ensure the render device pointed to by fd is capable of encoding h264 with the hevc_mode configured - bool - validate(int fd); + bool validate(int fd); } // namespace va diff --git a/src/platform/linux/wayland.cpp b/src/platform/linux/wayland.cpp index a6dba44e6ee..4fa05a277dc 100644 --- a/src/platform/linux/wayland.cpp +++ b/src/platform/linux/wayland.cpp @@ -2,12 +2,20 @@ * @file src/platform/linux/wayland.cpp * @brief Definitions for Wayland capture. */ +// standard includes +#include + +// platform includes +#include +#include +#include #include +#include #include #include +#include -#include - +// local includes #include "graphics.h" #include "src/logging.h" #include "src/platform/common.h" @@ -27,16 +35,20 @@ using namespace std::literals; namespace wl { // Helper to call C++ method from wayland C callback - template - static auto - classCall(void *data, Params... params) -> decltype(((*reinterpret_cast(data)).*m)(params...)) { + template + static auto classCall(void *data, Params... params) -> decltype(((*reinterpret_cast(data)).*m)(params...)) { return ((*reinterpret_cast(data)).*m)(params...); } #define CLASS_CALL(c, m) classCall - int - display_t::init(const char *display_name) { + // Define buffer params listener + static const struct zwp_linux_buffer_params_v1_listener params_listener = { + .created = dmabuf_t::buffer_params_created, + .failed = dmabuf_t::buffer_params_failed + }; + + int display_t::init(const char *display_name) { if (!display_name) { display_name = std::getenv("WAYLAND_DISPLAY"); } @@ -57,8 +69,7 @@ namespace wl { return 0; } - void - display_t::roundtrip() { + void display_t::roundtrip() { wl_display_roundtrip(display_internal.get()); } @@ -67,8 +78,7 @@ namespace wl { * @param timeout The timeout in milliseconds. * @return `true` if new events were dispatched or `false` if the timeout expired. */ - bool - display_t::dispatch(std::chrono::milliseconds timeout) { + bool display_t::dispatch(std::chrono::milliseconds timeout) { // Check if any events are queued already. If not, flush // outgoing events, and prepare to wait for readability. if (wl_display_prepare_read(display_internal.get()) == 0) { @@ -81,8 +91,7 @@ namespace wl { if (poll(&pfd, 1, timeout.count()) == 1 && (pfd.revents & POLLIN)) { // Read the new event(s) wl_display_read_events(display_internal.get()); - } - else { + } else { // We timed out, so unlock the queue now wl_display_cancel_read(display_internal.get()); return false; @@ -94,13 +103,12 @@ namespace wl { return true; } - wl_registry * - display_t::registry() { + wl_registry *display_t::registry() { return wl_display_get_registry(display_internal.get()); } inline monitor_t::monitor_t(wl_output *output): - output { output }, + output {output}, wl_listener { &CLASS_CALL(monitor_t, wl_geometry), &CLASS_CALL(monitor_t, wl_mode), @@ -113,46 +121,46 @@ namespace wl { &CLASS_CALL(monitor_t, xdg_done), &CLASS_CALL(monitor_t, xdg_name), &CLASS_CALL(monitor_t, xdg_description) - } {} + } { + } - inline void - monitor_t::xdg_name(zxdg_output_v1 *, const char *name) { + inline void monitor_t::xdg_name(zxdg_output_v1 *, const char *name) { this->name = name; BOOST_LOG(info) << "Name: "sv << this->name; } - void - monitor_t::xdg_description(zxdg_output_v1 *, const char *description) { + void monitor_t::xdg_description(zxdg_output_v1 *, const char *description) { this->description = description; BOOST_LOG(info) << "Found monitor: "sv << this->description; } - void - monitor_t::xdg_position(zxdg_output_v1 *, std::int32_t x, std::int32_t y) { + void monitor_t::xdg_position(zxdg_output_v1 *, std::int32_t x, std::int32_t y) { viewport.offset_x = x; viewport.offset_y = y; BOOST_LOG(info) << "Offset: "sv << x << 'x' << y; } - void - monitor_t::xdg_size(zxdg_output_v1 *, std::int32_t width, std::int32_t height) { + void monitor_t::xdg_size(zxdg_output_v1 *, std::int32_t width, std::int32_t height) { BOOST_LOG(info) << "Logical size: "sv << width << 'x' << height; } - void - monitor_t::wl_mode(wl_output *wl_output, std::uint32_t flags, - std::int32_t width, std::int32_t height, std::int32_t refresh) { + void monitor_t::wl_mode( + wl_output *wl_output, + std::uint32_t flags, + std::int32_t width, + std::int32_t height, + std::int32_t refresh + ) { viewport.width = width; viewport.height = height; BOOST_LOG(info) << "Resolution: "sv << width << 'x' << height; } - void - monitor_t::listen(zxdg_output_manager_v1 *output_manager) { + void monitor_t::listen(zxdg_output_manager_v1 *output_manager) { auto xdg_output = zxdg_output_manager_v1_get_xdg_output(output_manager, output); zxdg_output_v1_add_listener(xdg_output, &xdg_listener, this); wl_output_add_listener(output, &wl_listener, this); @@ -160,128 +168,357 @@ namespace wl { interface_t::interface_t() noexcept : - output_manager { nullptr }, + screencopy_manager {nullptr}, + dmabuf_interface {nullptr}, + output_manager {nullptr}, listener { &CLASS_CALL(interface_t, add_interface), &CLASS_CALL(interface_t, del_interface) - } {} + } { + } - void - interface_t::listen(wl_registry *registry) { + void interface_t::listen(wl_registry *registry) { wl_registry_add_listener(registry, &listener, this); } - void - interface_t::add_interface(wl_registry *registry, std::uint32_t id, const char *interface, std::uint32_t version) { + void interface_t::add_interface( + wl_registry *registry, + std::uint32_t id, + const char *interface, + std::uint32_t version + ) { BOOST_LOG(debug) << "Available interface: "sv << interface << '(' << id << ") version "sv << version; if (!std::strcmp(interface, wl_output_interface.name)) { BOOST_LOG(info) << "Found interface: "sv << interface << '(' << id << ") version "sv << version; monitors.emplace_back( std::make_unique( - (wl_output *) wl_registry_bind(registry, id, &wl_output_interface, 2))); - } - else if (!std::strcmp(interface, zxdg_output_manager_v1_interface.name)) { + (wl_output *) wl_registry_bind(registry, id, &wl_output_interface, 2) + ) + ); + } else if (!std::strcmp(interface, zxdg_output_manager_v1_interface.name)) { BOOST_LOG(info) << "Found interface: "sv << interface << '(' << id << ") version "sv << version; output_manager = (zxdg_output_manager_v1 *) wl_registry_bind(registry, id, &zxdg_output_manager_v1_interface, version); this->interface[XDG_OUTPUT] = true; - } - else if (!std::strcmp(interface, zwlr_export_dmabuf_manager_v1_interface.name)) { + } else if (!std::strcmp(interface, zwlr_screencopy_manager_v1_interface.name)) { BOOST_LOG(info) << "Found interface: "sv << interface << '(' << id << ") version "sv << version; - dmabuf_manager = (zwlr_export_dmabuf_manager_v1 *) wl_registry_bind(registry, id, &zwlr_export_dmabuf_manager_v1_interface, version); + screencopy_manager = (zwlr_screencopy_manager_v1 *) wl_registry_bind(registry, id, &zwlr_screencopy_manager_v1_interface, version); this->interface[WLR_EXPORT_DMABUF] = true; + } else if (!std::strcmp(interface, zwp_linux_dmabuf_v1_interface.name)) { + BOOST_LOG(info) << "Found interface: "sv << interface << '(' << id << ") version "sv << version; + dmabuf_interface = (zwp_linux_dmabuf_v1 *) wl_registry_bind(registry, id, &zwp_linux_dmabuf_v1_interface, version); + + this->interface[LINUX_DMABUF] = true; } } - void - interface_t::del_interface(wl_registry *registry, uint32_t id) { + void interface_t::del_interface(wl_registry *registry, uint32_t id) { BOOST_LOG(info) << "Delete: "sv << id; } + // Initialize GBM + bool dmabuf_t::init_gbm() { + if (gbm_device) { + return true; + } + + // Find render node + drmDevice *devices[16]; + int n = drmGetDevices2(0, devices, 16); + if (n <= 0) { + BOOST_LOG(error) << "No DRM devices found"sv; + return false; + } + + int drm_fd = -1; + for (int i = 0; i < n; i++) { + if (devices[i]->available_nodes & (1 << DRM_NODE_RENDER)) { + drm_fd = open(devices[i]->nodes[DRM_NODE_RENDER], O_RDWR); + if (drm_fd >= 0) { + break; + } + } + } + drmFreeDevices(devices, n); + + if (drm_fd < 0) { + BOOST_LOG(error) << "Failed to open DRM render node"sv; + return false; + } + + gbm_device = gbm_create_device(drm_fd); + if (!gbm_device) { + close(drm_fd); + BOOST_LOG(error) << "Failed to create GBM device"sv; + return false; + } + + return true; + } + + // Cleanup GBM + void dmabuf_t::cleanup_gbm() { + if (current_bo) { + gbm_bo_destroy(current_bo); + current_bo = nullptr; + } + + if (current_wl_buffer) { + wl_buffer_destroy(current_wl_buffer); + current_wl_buffer = nullptr; + } + } + dmabuf_t::dmabuf_t(): - status { READY }, frames {}, current_frame { &frames[0] }, listener { - &CLASS_CALL(dmabuf_t, frame), - &CLASS_CALL(dmabuf_t, object), + status {READY}, + frames {}, + current_frame {&frames[0]}, + listener { + &CLASS_CALL(dmabuf_t, buffer), + &CLASS_CALL(dmabuf_t, flags), &CLASS_CALL(dmabuf_t, ready), - &CLASS_CALL(dmabuf_t, cancel) + &CLASS_CALL(dmabuf_t, failed), + &CLASS_CALL(dmabuf_t, damage), + &CLASS_CALL(dmabuf_t, linux_dmabuf), + &CLASS_CALL(dmabuf_t, buffer_done), } { } - void - dmabuf_t::listen(zwlr_export_dmabuf_manager_v1 *dmabuf_manager, wl_output *output, bool blend_cursor) { - auto frame = zwlr_export_dmabuf_manager_v1_capture_output(dmabuf_manager, blend_cursor, output); - zwlr_export_dmabuf_frame_v1_add_listener(frame, &listener, this); + // Start capture + void dmabuf_t::listen( + zwlr_screencopy_manager_v1 *screencopy_manager, + zwp_linux_dmabuf_v1 *dmabuf_interface, + wl_output *output, + bool blend_cursor + ) { + this->dmabuf_interface = dmabuf_interface; + // Reset state + shm_info.supported = false; + dmabuf_info.supported = false; + + // Create new frame + auto frame = zwlr_screencopy_manager_v1_capture_output( + screencopy_manager, + blend_cursor ? 1 : 0, + output + ); + + // Store frame data pointer for callbacks + zwlr_screencopy_frame_v1_set_user_data(frame, this); + + // Add listener + zwlr_screencopy_frame_v1_add_listener(frame, &listener, this); status = WAITING; } dmabuf_t::~dmabuf_t() { + cleanup_gbm(); + for (auto &frame : frames) { frame.destroy(); } + + if (gbm_device) { + // We should close the DRM FD, but it's owned by GBM + gbm_device_destroy(gbm_device); + gbm_device = nullptr; + } } - void - dmabuf_t::frame( - zwlr_export_dmabuf_frame_v1 *frame, - std::uint32_t width, std::uint32_t height, - std::uint32_t x, std::uint32_t y, - std::uint32_t buffer_flags, std::uint32_t flags, + // Buffer format callback + void dmabuf_t::buffer( + zwlr_screencopy_frame_v1 *frame, + uint32_t format, + uint32_t width, + uint32_t height, + uint32_t stride + ) { + shm_info.supported = true; + shm_info.format = format; + shm_info.width = width; + shm_info.height = height; + shm_info.stride = stride; + + BOOST_LOG(debug) << "Screencopy supports SHM format: "sv << format; + } + + // DMA-BUF format callback + void dmabuf_t::linux_dmabuf( + zwlr_screencopy_frame_v1 *frame, std::uint32_t format, - std::uint32_t high, std::uint32_t low, - std::uint32_t obj_count) { + std::uint32_t width, + std::uint32_t height + ) { + dmabuf_info.supported = true; + dmabuf_info.format = format; + dmabuf_info.width = width; + dmabuf_info.height = height; + + BOOST_LOG(debug) << "Screencopy supports DMA-BUF format: "sv << format; + } + + // Flags callback + void dmabuf_t::flags(zwlr_screencopy_frame_v1 *frame, std::uint32_t flags) { + y_invert = flags & ZWLR_SCREENCOPY_FRAME_V1_FLAGS_Y_INVERT; + BOOST_LOG(debug) << "Frame flags: "sv << flags << (y_invert ? " (y_invert)" : ""); + } + + // DMA-BUF creation helper + void dmabuf_t::create_and_copy_dmabuf(zwlr_screencopy_frame_v1 *frame) { + if (!init_gbm()) { + BOOST_LOG(error) << "Failed to initialize GBM"sv; + zwlr_screencopy_frame_v1_destroy(frame); + status = REINIT; + return; + } + + // Create GBM buffer + current_bo = gbm_bo_create(gbm_device, dmabuf_info.width, dmabuf_info.height, dmabuf_info.format, GBM_BO_USE_RENDERING); + if (!current_bo) { + BOOST_LOG(error) << "Failed to create GBM buffer"sv; + zwlr_screencopy_frame_v1_destroy(frame); + status = REINIT; + return; + } + + // Get buffer info + int fd = gbm_bo_get_fd(current_bo); + if (fd < 0) { + BOOST_LOG(error) << "Failed to get buffer FD"sv; + gbm_bo_destroy(current_bo); + current_bo = nullptr; + zwlr_screencopy_frame_v1_destroy(frame); + status = REINIT; + return; + } + + uint32_t stride = gbm_bo_get_stride(current_bo); + uint64_t modifier = gbm_bo_get_modifier(current_bo); + + // Store in surface descriptor for later use auto next_frame = get_next_frame(); + next_frame->sd.fds[0] = fd; + next_frame->sd.pitches[0] = stride; + next_frame->sd.offsets[0] = 0; + next_frame->sd.modifier = modifier; - next_frame->sd.fourcc = format; - next_frame->sd.width = width; - next_frame->sd.height = height; - next_frame->sd.modifier = (((std::uint64_t) high) << 32) | low; - } - - void - dmabuf_t::object( - zwlr_export_dmabuf_frame_v1 *frame, - std::uint32_t index, - std::int32_t fd, - std::uint32_t size, - std::uint32_t offset, - std::uint32_t stride, - std::uint32_t plane_index) { + // Create linux-dmabuf buffer + auto params = zwp_linux_dmabuf_v1_create_params(dmabuf_interface); + zwp_linux_buffer_params_v1_add(params, fd, 0, 0, stride, modifier >> 32, modifier & 0xffffffff); + + // Add listener for buffer creation + zwp_linux_buffer_params_v1_add_listener(params, ¶ms_listener, frame); + + // Create Wayland buffer (async - callback will handle copy) + zwp_linux_buffer_params_v1_create(params, dmabuf_info.width, dmabuf_info.height, dmabuf_info.format, 0); + } + + // Buffer done callback - time to create buffer + void dmabuf_t::buffer_done(zwlr_screencopy_frame_v1 *frame) { auto next_frame = get_next_frame(); - next_frame->sd.fds[plane_index] = fd; - next_frame->sd.pitches[plane_index] = stride; - next_frame->sd.offsets[plane_index] = offset; + // Prefer DMA-BUF if supported + if (dmabuf_info.supported && dmabuf_interface) { + // Store format info first + next_frame->sd.fourcc = dmabuf_info.format; + next_frame->sd.width = dmabuf_info.width; + next_frame->sd.height = dmabuf_info.height; + + // Create and start copy + create_and_copy_dmabuf(frame); + } else if (shm_info.supported) { + // SHM fallback would go here + BOOST_LOG(warning) << "SHM capture not implemented"sv; + zwlr_screencopy_frame_v1_destroy(frame); + status = REINIT; + } else { + BOOST_LOG(error) << "No supported buffer types"sv; + zwlr_screencopy_frame_v1_destroy(frame); + status = REINIT; + } + } + + // Buffer params created callback + void dmabuf_t::buffer_params_created( + void *data, + struct zwp_linux_buffer_params_v1 *params, + struct wl_buffer *buffer + ) { + auto frame = static_cast(data); + auto self = static_cast(zwlr_screencopy_frame_v1_get_user_data(frame)); + + // Store for cleanup + self->current_wl_buffer = buffer; + + // Start the actual copy + zwlr_screencopy_frame_v1_copy(frame, buffer); } - void - dmabuf_t::ready( - zwlr_export_dmabuf_frame_v1 *frame, - std::uint32_t tv_sec_hi, std::uint32_t tv_sec_lo, std::uint32_t tv_nsec) { - zwlr_export_dmabuf_frame_v1_destroy(frame); + // Buffer params failed callback + void dmabuf_t::buffer_params_failed( + void *data, + struct zwp_linux_buffer_params_v1 *params + ) { + auto frame = static_cast(data); + auto self = static_cast(zwlr_screencopy_frame_v1_get_user_data(frame)); + BOOST_LOG(error) << "Failed to create buffer from params"sv; + self->cleanup_gbm(); + + zwlr_screencopy_frame_v1_destroy(frame); + self->status = REINIT; + } + + // Ready callback + void dmabuf_t::ready( + zwlr_screencopy_frame_v1 *frame, + std::uint32_t tv_sec_hi, + std::uint32_t tv_sec_lo, + std::uint32_t tv_nsec + ) { + BOOST_LOG(debug) << "Frame ready"sv; + + // Frame is ready for use, GBM buffer now contains screen content current_frame->destroy(); current_frame = get_next_frame(); + // Keep the GBM buffer alive but destroy the Wayland objects + if (current_wl_buffer) { + wl_buffer_destroy(current_wl_buffer); + current_wl_buffer = nullptr; + } + + cleanup_gbm(); + + zwlr_screencopy_frame_v1_destroy(frame); status = READY; } - void - dmabuf_t::cancel( - zwlr_export_dmabuf_frame_v1 *frame, - std::uint32_t reason) { - zwlr_export_dmabuf_frame_v1_destroy(frame); + // Failed callback + void dmabuf_t::failed(zwlr_screencopy_frame_v1 *frame) { + BOOST_LOG(error) << "Frame capture failed"sv; + // Clean up resources + cleanup_gbm(); auto next_frame = get_next_frame(); next_frame->destroy(); + zwlr_screencopy_frame_v1_destroy(frame); status = REINIT; } - void - frame_t::destroy() { + void dmabuf_t::damage( + zwlr_screencopy_frame_v1 *frame, + std::uint32_t x, + std::uint32_t y, + std::uint32_t width, + std::uint32_t height + ) {}; + + void frame_t::destroy() { for (auto x = 0; x < 4; ++x) { if (sd.fds[x] >= 0) { close(sd.fds[x]); @@ -296,8 +533,7 @@ namespace wl { std::fill_n(sd.fds, 4, -1); }; - std::vector> - monitors(const char *display_name) { + std::vector> monitors(const char *display_name) { display_t display; if (display.init(display_name)) { @@ -323,15 +559,13 @@ namespace wl { return std::move(interface.monitors); } - static bool - validate() { + static bool validate() { display_t display; return display.init() == 0; } - int - init() { + int init() { static bool validated = validate(); return !validated; @@ -339,4 +573,4 @@ namespace wl { } // namespace wl -#pragma GCC diagnostic pop \ No newline at end of file +#pragma GCC diagnostic pop diff --git a/src/platform/linux/wayland.h b/src/platform/linux/wayland.h index ece9d3bdaf6..08d5acbd543 100644 --- a/src/platform/linux/wayland.h +++ b/src/platform/linux/wayland.h @@ -4,13 +4,16 @@ */ #pragma once +// standard includes #include #ifdef SUNSHINE_BUILD_WAYLAND - #include + #include + #include #include #endif +// local includes #include "graphics.h" /** @@ -25,10 +28,9 @@ namespace wl { class frame_t { public: frame_t(); - egl::surface_descriptor_t sd; + void destroy(); - void - destroy(); + egl::surface_descriptor_t sd; }; class dmabuf_t { @@ -39,109 +41,91 @@ namespace wl { REINIT, ///< Reinitialize the frame }; - dmabuf_t(dmabuf_t &&) = delete; - dmabuf_t(const dmabuf_t &) = delete; - - dmabuf_t & - operator=(const dmabuf_t &) = delete; - dmabuf_t & - operator=(dmabuf_t &&) = delete; - dmabuf_t(); - - void - listen(zwlr_export_dmabuf_manager_v1 *dmabuf_manager, wl_output *output, bool blend_cursor = false); - ~dmabuf_t(); - void - frame( - zwlr_export_dmabuf_frame_v1 *frame, - std::uint32_t width, std::uint32_t height, - std::uint32_t x, std::uint32_t y, - std::uint32_t buffer_flags, std::uint32_t flags, - std::uint32_t format, - std::uint32_t high, std::uint32_t low, - std::uint32_t obj_count); - - void - object( - zwlr_export_dmabuf_frame_v1 *frame, - std::uint32_t index, - std::int32_t fd, - std::uint32_t size, - std::uint32_t offset, - std::uint32_t stride, - std::uint32_t plane_index); - - void - ready( - zwlr_export_dmabuf_frame_v1 *frame, - std::uint32_t tv_sec_hi, std::uint32_t tv_sec_lo, std::uint32_t tv_nsec); - - void - cancel( - zwlr_export_dmabuf_frame_v1 *frame, - std::uint32_t reason); - - inline frame_t * - get_next_frame() { + dmabuf_t(dmabuf_t &&) = delete; + dmabuf_t(const dmabuf_t &) = delete; + dmabuf_t &operator=(const dmabuf_t &) = delete; + dmabuf_t &operator=(dmabuf_t &&) = delete; + + void listen(zwlr_screencopy_manager_v1 *screencopy_manager, zwp_linux_dmabuf_v1 *dmabuf_interface, wl_output *output, bool blend_cursor = false); + static void buffer_params_created(void *data, struct zwp_linux_buffer_params_v1 *params, struct wl_buffer *wl_buffer); + static void buffer_params_failed(void *data, struct zwp_linux_buffer_params_v1 *params); + void buffer(zwlr_screencopy_frame_v1 *frame, std::uint32_t format, std::uint32_t width, std::uint32_t height, std::uint32_t stride); + void linux_dmabuf(zwlr_screencopy_frame_v1 *frame, std::uint32_t format, std::uint32_t width, std::uint32_t height); + void buffer_done(zwlr_screencopy_frame_v1 *frame); + void flags(zwlr_screencopy_frame_v1 *frame, std::uint32_t flags); + void damage(zwlr_screencopy_frame_v1 *frame, std::uint32_t x, std::uint32_t y, std::uint32_t width, std::uint32_t height); + void ready(zwlr_screencopy_frame_v1 *frame, std::uint32_t tv_sec_hi, std::uint32_t tv_sec_lo, std::uint32_t tv_nsec); + void failed(zwlr_screencopy_frame_v1 *frame); + + frame_t *get_next_frame() { return current_frame == &frames[0] ? &frames[1] : &frames[0]; } status_e status; - std::array frames; frame_t *current_frame; + zwlr_screencopy_frame_v1_listener listener; - zwlr_export_dmabuf_frame_v1_listener listener; + private: + bool init_gbm(); + void cleanup_gbm(); + void create_and_copy_dmabuf(zwlr_screencopy_frame_v1 *frame); + + zwp_linux_dmabuf_v1 *dmabuf_interface {nullptr}; + + struct { + bool supported {false}; + std::uint32_t format; + std::uint32_t width; + std::uint32_t height; + std::uint32_t stride; + } shm_info; + + struct { + bool supported {false}; + std::uint32_t format; + std::uint32_t width; + std::uint32_t height; + } dmabuf_info; + + struct gbm_device *gbm_device {nullptr}; + struct gbm_bo *current_bo {nullptr}; + struct wl_buffer *current_wl_buffer {nullptr}; + bool y_invert {false}; }; class monitor_t { public: + explicit monitor_t(wl_output *output); + monitor_t(monitor_t &&) = delete; monitor_t(const monitor_t &) = delete; + monitor_t &operator=(const monitor_t &) = delete; + monitor_t &operator=(monitor_t &&) = delete; - monitor_t & - operator=(const monitor_t &) = delete; - monitor_t & - operator=(monitor_t &&) = delete; + void listen(zxdg_output_manager_v1 *output_manager); + void xdg_name(zxdg_output_v1 *, const char *name); + void xdg_description(zxdg_output_v1 *, const char *description); + void xdg_position(zxdg_output_v1 *, std::int32_t x, std::int32_t y); + void xdg_size(zxdg_output_v1 *, std::int32_t width, std::int32_t height); - monitor_t(wl_output *output); + void xdg_done(zxdg_output_v1 *) {} - void - xdg_name(zxdg_output_v1 *, const char *name); - void - xdg_description(zxdg_output_v1 *, const char *description); - void - xdg_position(zxdg_output_v1 *, std::int32_t x, std::int32_t y); - void - xdg_size(zxdg_output_v1 *, std::int32_t width, std::int32_t height); - void - xdg_done(zxdg_output_v1 *) {} - - void - wl_geometry(wl_output *wl_output, std::int32_t x, std::int32_t y, - std::int32_t physical_width, std::int32_t physical_height, std::int32_t subpixel, - const char *make, const char *model, std::int32_t transform) {} - void - wl_mode(wl_output *wl_output, std::uint32_t flags, - std::int32_t width, std::int32_t height, std::int32_t refresh); - void - wl_done(wl_output *wl_output) {} - void - wl_scale(wl_output *wl_output, std::int32_t factor) {} - - void - listen(zxdg_output_manager_v1 *output_manager); + void wl_geometry(wl_output *wl_output, std::int32_t x, std::int32_t y, std::int32_t physical_width, std::int32_t physical_height, std::int32_t subpixel, const char *make, const char *model, std::int32_t transform) {} - wl_output *output; + void wl_mode(wl_output *wl_output, std::uint32_t flags, std::int32_t width, std::int32_t height, std::int32_t refresh); + void wl_done(wl_output *wl_output) {} + + void wl_scale(wl_output *wl_output, std::int32_t factor) {} + + wl_output *output; std::string name; std::string description; - platf::touch_port_t viewport; - wl_output_listener wl_listener; zxdg_output_v1_listener xdg_listener; }; @@ -155,41 +139,34 @@ namespace wl { public: enum interface_e { XDG_OUTPUT, ///< xdg-output - WLR_EXPORT_DMABUF, ///< Export dmabuf + WLR_EXPORT_DMABUF, ///< screencopy manager + LINUX_DMABUF, ///< linux-dmabuf protocol MAX_INTERFACES, ///< Maximum number of interfaces }; - interface_t(interface_t &&) = delete; - interface_t(const interface_t &) = delete; - - interface_t & - operator=(const interface_t &) = delete; - interface_t & - operator=(interface_t &&) = delete; - interface_t() noexcept; - void - listen(wl_registry *registry); - - std::vector> monitors; + interface_t(interface_t &&) = delete; + interface_t(const interface_t &) = delete; + interface_t &operator=(const interface_t &) = delete; + interface_t &operator=(interface_t &&) = delete; - zwlr_export_dmabuf_manager_v1 *dmabuf_manager; - zxdg_output_manager_v1 *output_manager; + void listen(wl_registry *registry); - bool - operator[](interface_e bit) const { + bool operator[](interface_e bit) const { return interface[bit]; } + std::vector> monitors; + zwlr_screencopy_manager_v1 *screencopy_manager {nullptr}; + zwp_linux_dmabuf_v1 *dmabuf_interface {nullptr}; + zxdg_output_manager_v1 *output_manager {nullptr}; + private: - void - add_interface(wl_registry *registry, std::uint32_t id, const char *interface, std::uint32_t version); - void - del_interface(wl_registry *registry, uint32_t id); + void add_interface(wl_registry *registry, std::uint32_t id, const char *interface, std::uint32_t version); + void del_interface(wl_registry *registry, uint32_t id); std::bitset interface; - wl_registry_listener listener; }; @@ -201,24 +178,19 @@ namespace wl { * @param display_name The name of the display. * @return 0 on success, -1 on failure. */ - int - init(const char *display_name = nullptr); + int init(const char *display_name = nullptr); // Roundtrip with Wayland connection - void - roundtrip(); + void roundtrip(); // Wait up to the timeout to read and dispatch new events - bool - dispatch(std::chrono::milliseconds timeout); + bool dispatch(std::chrono::milliseconds timeout); // Get the registry associated with the display // No need to manually free the registry - wl_registry * - registry(); + wl_registry *registry(); - inline display_internal_t::pointer - get() { + inline display_internal_t::pointer get() { return display_internal.get(); } @@ -226,11 +198,8 @@ namespace wl { display_internal_t display_internal; }; - std::vector> - monitors(const char *display_name = nullptr); - - int - init(); + std::vector> monitors(const char *display_name = nullptr); + int init(); } // namespace wl #else @@ -240,31 +209,27 @@ struct zxdg_output_manager_v1; namespace wl { class monitor_t { public: + monitor_t(wl_output *output); + monitor_t(monitor_t &&) = delete; monitor_t(const monitor_t &) = delete; + monitor_t &operator=(const monitor_t &) = delete; + monitor_t &operator=(monitor_t &&) = delete; - monitor_t & - operator=(const monitor_t &) = delete; - monitor_t & - operator=(monitor_t &&) = delete; - - monitor_t(wl_output *output); - - void - listen(zxdg_output_manager_v1 *output_manager); + void listen(zxdg_output_manager_v1 *output_manager); wl_output *output; - std::string name; std::string description; - platf::touch_port_t viewport; }; - inline std::vector> - monitors(const char *display_name = nullptr) { return {}; } + inline std::vector> monitors(const char *display_name = nullptr) { + return {}; + } - inline int - init() { return -1; } + inline int init() { + return -1; + } } // namespace wl #endif diff --git a/src/platform/linux/wlgrab.cpp b/src/platform/linux/wlgrab.cpp index 4c5946c84d3..9ef3e09f9c6 100644 --- a/src/platform/linux/wlgrab.cpp +++ b/src/platform/linux/wlgrab.cpp @@ -2,18 +2,19 @@ * @file src/platform/linux/wlgrab.cpp * @brief Definitions for wlgrab capture. */ +// standard includes #include -#include "src/platform/common.h" - +// local includes +#include "cuda.h" #include "src/logging.h" +#include "src/platform/common.h" #include "src/video.h" - -#include "cuda.h" #include "vaapi.h" #include "wayland.h" using namespace std::literals; + namespace wl { static int env_width; static int env_height; @@ -27,9 +28,8 @@ namespace wl { class wlr_t: public platf::display_t { public: - int - init(platf::mem_type_e hwdevice_type, const std::string &display_name, const ::video::config_t &config) { - delay = std::chrono::nanoseconds { 1s } / config.framerate; + int init(platf::mem_type_e hwdevice_type, const std::string &display_name, const ::video::config_t &config) { + delay = std::chrono::nanoseconds {1s} / config.framerate; mem_type = hwdevice_type; if (display.init()) { @@ -82,17 +82,15 @@ namespace wl { return 0; } - int - dummy_img(platf::img_t *img) override { + int dummy_img(platf::img_t *img) override { return 0; } - inline platf::capture_e - snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor) { + inline platf::capture_e snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor) { auto to = std::chrono::steady_clock::now() + timeout; // Dispatch events until we get a new frame or the timeout expires - dmabuf.listen(interface.dmabuf_manager, output, cursor); + dmabuf.listen(interface.screencopy_manager, interface.dmabuf_interface, output, cursor); do { auto remaining_time_ms = std::chrono::duration_cast(to - std::chrono::steady_clock::now()); if (remaining_time_ms.count() < 0 || !display.dispatch(remaining_time_ms)) { @@ -105,7 +103,8 @@ namespace wl { if ( dmabuf.status == dmabuf_t::REINIT || current_frame->sd.width != width || - current_frame->sd.height != height) { + current_frame->sd.height != height + ) { return platf::capture_e::reinit; } @@ -125,8 +124,7 @@ namespace wl { class wlr_ram_t: public wlr_t { public: - platf::capture_e - capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override { + platf::capture_e capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override { auto next_frame = std::chrono::steady_clock::now(); sleep_overshoot_logger.reset(); @@ -171,8 +169,7 @@ namespace wl { return platf::capture_e::ok; } - platf::capture_e - snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor) { + platf::capture_e snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor) { auto status = wlr_t::snapshot(pull_free_image_cb, img_out, timeout, cursor); if (status != platf::capture_e::ok) { return status; @@ -204,8 +201,7 @@ namespace wl { return platf::capture_e::ok; } - int - init(platf::mem_type_e hwdevice_type, const std::string &display_name, const ::video::config_t &config) { + int init(platf::mem_type_e hwdevice_type, const std::string &display_name, const ::video::config_t &config) { if (wlr_t::init(hwdevice_type, display_name, config)) { return -1; } @@ -225,8 +221,7 @@ namespace wl { return 0; } - std::unique_ptr - make_avcodec_encode_device(platf::pix_fmt_e pix_fmt) override { + std::unique_ptr make_avcodec_encode_device(platf::pix_fmt_e pix_fmt) override { #ifdef SUNSHINE_BUILD_VAAPI if (mem_type == platf::mem_type_e::vaapi) { return va::make_avcodec_encode_device(width, height, false); @@ -242,8 +237,7 @@ namespace wl { return std::make_unique(); } - std::shared_ptr - alloc_img() override { + std::shared_ptr alloc_img() override { auto img = std::make_shared(); img->width = width; img->height = height; @@ -260,8 +254,7 @@ namespace wl { class wlr_vram_t: public wlr_t { public: - platf::capture_e - capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override { + platf::capture_e capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override { auto next_frame = std::chrono::steady_clock::now(); sleep_overshoot_logger.reset(); @@ -306,8 +299,7 @@ namespace wl { return platf::capture_e::ok; } - platf::capture_e - snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor) { + platf::capture_e snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor) { auto status = wlr_t::snapshot(pull_free_image_cb, img_out, timeout, cursor); if (status != platf::capture_e::ok) { return status; @@ -332,8 +324,7 @@ namespace wl { return platf::capture_e::ok; } - std::shared_ptr - alloc_img() override { + std::shared_ptr alloc_img() override { auto img = std::make_shared(); img->width = width; @@ -348,8 +339,7 @@ namespace wl { return img; } - std::unique_ptr - make_avcodec_encode_device(platf::pix_fmt_e pix_fmt) override { + std::unique_ptr make_avcodec_encode_device(platf::pix_fmt_e pix_fmt) override { #ifdef SUNSHINE_BUILD_VAAPI if (mem_type == platf::mem_type_e::vaapi) { return va::make_avcodec_encode_device(width, height, 0, 0, true); @@ -365,8 +355,7 @@ namespace wl { return std::make_unique(); } - int - dummy_img(platf::img_t *img) override { + int dummy_img(platf::img_t *img) override { // Empty images are recognized as dummies by the zero sequence number return 0; } @@ -377,8 +366,7 @@ namespace wl { } // namespace wl namespace platf { - std::shared_ptr - wl_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config) { + std::shared_ptr wl_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config) { if (hwdevice_type != platf::mem_type_e::system && hwdevice_type != platf::mem_type_e::vaapi && hwdevice_type != platf::mem_type_e::cuda) { BOOST_LOG(error) << "Could not initialize display with the given hw device type."sv; return nullptr; @@ -401,8 +389,7 @@ namespace platf { return wlr; } - std::vector - wl_display_names() { + std::vector wl_display_names() { std::vector display_names; wl::display_t display; diff --git a/src/platform/linux/x11grab.cpp b/src/platform/linux/x11grab.cpp index 44c37609387..6eef5a81abc 100644 --- a/src/platform/linux/x11grab.cpp +++ b/src/platform/linux/x11grab.cpp @@ -2,61 +2,49 @@ * @file src/platform/linux/x11grab.cpp * @brief Definitions for x11 capture. */ -#include "src/platform/common.h" - +// standard includes #include #include +// plaform includes +#include +#include +#include +#include #include #include #include -#include -#include -#include -#include #include #include +// local includes +#include "cuda.h" +#include "graphics.h" +#include "misc.h" #include "src/config.h" #include "src/globals.h" #include "src/logging.h" +#include "src/platform/common.h" #include "src/task_pool.h" #include "src/video.h" - -#include "cuda.h" -#include "graphics.h" -#include "misc.h" #include "vaapi.h" #include "x11grab.h" using namespace std::literals; namespace platf { - int - load_xcb(); - int - load_x11(); + int load_xcb(); + int load_x11(); namespace x11 { -#define _FN(x, ret, args) \ +#define _FN(x, ret, args) \ typedef ret(*x##_fn) args; \ static x##_fn x - _FN(GetImage, XImage *, - ( - Display * display, - Drawable d, - int x, int y, - unsigned int width, unsigned int height, - unsigned long plane_mask, - int format)); + _FN(GetImage, XImage *, (Display * display, Drawable d, int x, int y, unsigned int width, unsigned int height, unsigned long plane_mask, int format)); _FN(OpenDisplay, Display *, (_Xconst char *display_name)); - _FN(GetWindowAttributes, Status, - ( - Display * display, - Window w, - XWindowAttributes *window_attributes_return)); + _FN(GetWindowAttributes, Status, (Display * display, Window w, XWindowAttributes *window_attributes_return)); _FN(CloseDisplay, int, (Display * display)); _FN(Free, int, (void *data)); @@ -70,27 +58,28 @@ namespace platf { _FN(FreeOutputInfo, void, (XRROutputInfo * outputInfo)); _FN(FreeCrtcInfo, void, (XRRCrtcInfo * crtcInfo)); - static int - init() { - static void *handle { nullptr }; + static int init() { + static void *handle {nullptr}; static bool funcs_loaded = false; - if (funcs_loaded) return 0; + if (funcs_loaded) { + return 0; + } if (!handle) { - handle = dyn::handle({ "libXrandr.so.2", "libXrandr.so" }); + handle = dyn::handle({"libXrandr.so.2", "libXrandr.so"}); if (!handle) { return -1; } } std::vector> funcs { - { (dyn::apiproc *) &GetScreenResources, "XRRGetScreenResources" }, - { (dyn::apiproc *) &GetOutputInfo, "XRRGetOutputInfo" }, - { (dyn::apiproc *) &GetCrtcInfo, "XRRGetCrtcInfo" }, - { (dyn::apiproc *) &FreeScreenResources, "XRRFreeScreenResources" }, - { (dyn::apiproc *) &FreeOutputInfo, "XRRFreeOutputInfo" }, - { (dyn::apiproc *) &FreeCrtcInfo, "XRRFreeCrtcInfo" }, + {(dyn::apiproc *) &GetScreenResources, "XRRGetScreenResources"}, + {(dyn::apiproc *) &GetOutputInfo, "XRRGetOutputInfo"}, + {(dyn::apiproc *) &GetCrtcInfo, "XRRGetCrtcInfo"}, + {(dyn::apiproc *) &FreeScreenResources, "XRRFreeScreenResources"}, + {(dyn::apiproc *) &FreeOutputInfo, "XRRFreeOutputInfo"}, + {(dyn::apiproc *) &FreeCrtcInfo, "XRRFreeCrtcInfo"}, }; if (dyn::load(handle, funcs)) { @@ -102,25 +91,27 @@ namespace platf { } } // namespace rr + namespace fix { _FN(GetCursorImage, XFixesCursorImage *, (Display * dpy)); - static int - init() { - static void *handle { nullptr }; + static int init() { + static void *handle {nullptr}; static bool funcs_loaded = false; - if (funcs_loaded) return 0; + if (funcs_loaded) { + return 0; + } if (!handle) { - handle = dyn::handle({ "libXfixes.so.3", "libXfixes.so" }); + handle = dyn::handle({"libXfixes.so.3", "libXfixes.so"}); if (!handle) { return -1; } } std::vector> funcs { - { (dyn::apiproc *) &GetCursorImage, "XFixesGetCursorImage" }, + {(dyn::apiproc *) &GetCursorImage, "XFixesGetCursorImage"}, }; if (dyn::load(handle, funcs)) { @@ -132,27 +123,28 @@ namespace platf { } } // namespace fix - static int - init() { - static void *handle { nullptr }; + static int init() { + static void *handle {nullptr}; static bool funcs_loaded = false; - if (funcs_loaded) return 0; + if (funcs_loaded) { + return 0; + } if (!handle) { - handle = dyn::handle({ "libX11.so.6", "libX11.so" }); + handle = dyn::handle({"libX11.so.6", "libX11.so"}); if (!handle) { return -1; } } std::vector> funcs { - { (dyn::apiproc *) &GetImage, "XGetImage" }, - { (dyn::apiproc *) &OpenDisplay, "XOpenDisplay" }, - { (dyn::apiproc *) &GetWindowAttributes, "XGetWindowAttributes" }, - { (dyn::apiproc *) &Free, "XFree" }, - { (dyn::apiproc *) &CloseDisplay, "XCloseDisplay" }, - { (dyn::apiproc *) &InitThreads, "XInitThreads" }, + {(dyn::apiproc *) &GetImage, "XGetImage"}, + {(dyn::apiproc *) &OpenDisplay, "XOpenDisplay"}, + {(dyn::apiproc *) &GetWindowAttributes, "XGetWindowAttributes"}, + {(dyn::apiproc *) &Free, "XFree"}, + {(dyn::apiproc *) &CloseDisplay, "XCloseDisplay"}, + {(dyn::apiproc *) &InitThreads, "XInitThreads"}, }; if (dyn::load(handle, funcs)) { @@ -167,31 +159,13 @@ namespace platf { namespace xcb { static xcb_extension_t *shm_id; - _FN(shm_get_image_reply, xcb_shm_get_image_reply_t *, - ( - xcb_connection_t * c, - xcb_shm_get_image_cookie_t cookie, - xcb_generic_error_t **e)); - - _FN(shm_get_image_unchecked, xcb_shm_get_image_cookie_t, - ( - xcb_connection_t * c, - xcb_drawable_t drawable, - int16_t x, int16_t y, - uint16_t width, uint16_t height, - uint32_t plane_mask, - uint8_t format, - xcb_shm_seg_t shmseg, - uint32_t offset)); - - _FN(shm_attach, xcb_void_cookie_t, - (xcb_connection_t * c, - xcb_shm_seg_t shmseg, - uint32_t shmid, - uint8_t read_only)); - - _FN(get_extension_data, xcb_query_extension_reply_t *, - (xcb_connection_t * c, xcb_extension_t *ext)); + _FN(shm_get_image_reply, xcb_shm_get_image_reply_t *, (xcb_connection_t * c, xcb_shm_get_image_cookie_t cookie, xcb_generic_error_t **e)); + + _FN(shm_get_image_unchecked, xcb_shm_get_image_cookie_t, (xcb_connection_t * c, xcb_drawable_t drawable, int16_t x, int16_t y, uint16_t width, uint16_t height, uint32_t plane_mask, uint8_t format, xcb_shm_seg_t shmseg, uint32_t offset)); + + _FN(shm_attach, xcb_void_cookie_t, (xcb_connection_t * c, xcb_shm_seg_t shmseg, uint32_t shmid, uint8_t read_only)); + + _FN(get_extension_data, xcb_query_extension_reply_t *, (xcb_connection_t * c, xcb_extension_t *ext)); _FN(get_setup, xcb_setup_t *, (xcb_connection_t * c)); _FN(disconnect, void, (xcb_connection_t * c)); @@ -200,25 +174,26 @@ namespace platf { _FN(setup_roots_iterator, xcb_screen_iterator_t, (const xcb_setup_t *R)); _FN(generate_id, std::uint32_t, (xcb_connection_t * c)); - int - init_shm() { - static void *handle { nullptr }; + int init_shm() { + static void *handle {nullptr}; static bool funcs_loaded = false; - if (funcs_loaded) return 0; + if (funcs_loaded) { + return 0; + } if (!handle) { - handle = dyn::handle({ "libxcb-shm.so.0", "libxcb-shm.so" }); + handle = dyn::handle({"libxcb-shm.so.0", "libxcb-shm.so"}); if (!handle) { return -1; } } std::vector> funcs { - { (dyn::apiproc *) &shm_id, "xcb_shm_id" }, - { (dyn::apiproc *) &shm_get_image_reply, "xcb_shm_get_image_reply" }, - { (dyn::apiproc *) &shm_get_image_unchecked, "xcb_shm_get_image_unchecked" }, - { (dyn::apiproc *) &shm_attach, "xcb_shm_attach" }, + {(dyn::apiproc *) &shm_id, "xcb_shm_id"}, + {(dyn::apiproc *) &shm_get_image_reply, "xcb_shm_get_image_reply"}, + {(dyn::apiproc *) &shm_get_image_unchecked, "xcb_shm_get_image_unchecked"}, + {(dyn::apiproc *) &shm_attach, "xcb_shm_attach"}, }; if (dyn::load(handle, funcs)) { @@ -229,28 +204,29 @@ namespace platf { return 0; } - int - init() { - static void *handle { nullptr }; + int init() { + static void *handle {nullptr}; static bool funcs_loaded = false; - if (funcs_loaded) return 0; + if (funcs_loaded) { + return 0; + } if (!handle) { - handle = dyn::handle({ "libxcb.so.1", "libxcb.so" }); + handle = dyn::handle({"libxcb.so.1", "libxcb.so"}); if (!handle) { return -1; } } std::vector> funcs { - { (dyn::apiproc *) &get_extension_data, "xcb_get_extension_data" }, - { (dyn::apiproc *) &get_setup, "xcb_get_setup" }, - { (dyn::apiproc *) &disconnect, "xcb_disconnect" }, - { (dyn::apiproc *) &connection_has_error, "xcb_connection_has_error" }, - { (dyn::apiproc *) &connect, "xcb_connect" }, - { (dyn::apiproc *) &setup_roots_iterator, "xcb_setup_roots_iterator" }, - { (dyn::apiproc *) &generate_id, "xcb_generate_id" }, + {(dyn::apiproc *) &get_extension_data, "xcb_get_extension_data"}, + {(dyn::apiproc *) &get_setup, "xcb_get_setup"}, + {(dyn::apiproc *) &disconnect, "xcb_disconnect"}, + {(dyn::apiproc *) &connection_has_error, "xcb_connection_has_error"}, + {(dyn::apiproc *) &connect, "xcb_connect"}, + {(dyn::apiproc *) &setup_roots_iterator, "xcb_setup_roots_iterator"}, + {(dyn::apiproc *) &generate_id, "xcb_generate_id"}, }; if (dyn::load(handle, funcs)) { @@ -264,10 +240,8 @@ namespace platf { #undef _FN } // namespace xcb - void - freeImage(XImage *); - void - freeX(XFixesCursorImage *); + void freeImage(XImage *); + void freeX(XFixesCursorImage *); using xcb_connect_t = util::dyn_safe_ptr; using xcb_img_t = util::c_ptr; @@ -282,9 +256,13 @@ namespace platf { class shm_id_t { public: shm_id_t(): - id { -1 } {} + id {-1} { + } + shm_id_t(int id): - id { id } {} + id {id} { + } + shm_id_t(shm_id_t &&other) noexcept: id(other.id) { other.id = -1; @@ -296,15 +274,19 @@ namespace platf { id = -1; } } + int id; }; class shm_data_t { public: shm_data_t(): - data { (void *) -1 } {} + data {(void *) -1} { + } + shm_data_t(void *data): - data { data } {} + data {data} { + } shm_data_t(shm_data_t &&other) noexcept: data(other.data) { @@ -331,9 +313,8 @@ namespace platf { } }; - static void - blend_cursor(Display *display, img_t &img, int offsetX, int offsetY) { - xcursor_t overlay { x11::fix::GetCursorImage(display) }; + static void blend_cursor(Display *display, img_t &img, int offsetX, int offsetY) { + xcursor_t overlay {x11::fix::GetCursorImage(display)}; if (!overlay) { BOOST_LOG(error) << "Couldn't get cursor from XFixesGetCursorImage"sv; @@ -370,8 +351,7 @@ namespace platf { auto alpha = (*(uint *) pixel_p) >> 24u; if (alpha == 255) { *pixels_begin = *pixel_p; - } - else { + } else { auto colors_out = (uint8_t *) pixel_p; colors_in[0] = colors_out[0] + (colors_in[0] * (255 - alpha) + 255 / 2) / 255; colors_in[1] = colors_out[1] + (colors_in[1] * (255 - alpha) + 255 / 2) / 255; @@ -398,18 +378,20 @@ namespace platf { // int env_width, env_height; x11_attr_t(mem_type_e mem_type): - xdisplay { x11::OpenDisplay(nullptr) }, xwindow {}, xattr {}, mem_type { mem_type } { + xdisplay {x11::OpenDisplay(nullptr)}, + xwindow {}, + xattr {}, + mem_type {mem_type} { x11::InitThreads(); } - int - init(const std::string &display_name, const ::video::config_t &config) { + int init(const std::string &display_name, const ::video::config_t &config) { if (!xdisplay) { BOOST_LOG(error) << "Could not open X11 display"sv; return -1; } - delay = std::chrono::nanoseconds { 1s } / config.framerate; + delay = std::chrono::nanoseconds {1s} / config.framerate; xwindow = DefaultRootWindow(xdisplay.get()); @@ -422,13 +404,13 @@ namespace platf { if (streamedMonitor != -1) { BOOST_LOG(info) << "Configuring selected display ("sv << streamedMonitor << ") to stream"sv; - screen_res_t screenr { x11::rr::GetScreenResources(xdisplay.get(), xwindow) }; + screen_res_t screenr {x11::rr::GetScreenResources(xdisplay.get(), xwindow)}; int output = screenr->noutput; output_info_t result; int monitor = 0; for (int x = 0; x < output; ++x) { - output_info_t out_info { x11::rr::GetOutputInfo(xdisplay.get(), screenr.get(), screenr->outputs[x]) }; + output_info_t out_info {x11::rr::GetOutputInfo(xdisplay.get(), screenr.get(), screenr->outputs[x])}; if (out_info) { if (monitor++ == streamedMonitor) { result = std::move(out_info); @@ -443,7 +425,7 @@ namespace platf { } if (result->crtc) { - crtc_info_t crt_info { x11::rr::GetCrtcInfo(xdisplay.get(), screenr.get(), result->crtc) }; + crtc_info_t crt_info {x11::rr::GetCrtcInfo(xdisplay.get(), screenr.get(), result->crtc)}; BOOST_LOG(info) << "Streaming display: "sv << result->name << " with res "sv << crt_info->width << 'x' << crt_info->height << " offset by "sv << crt_info->x << 'x' << crt_info->y; @@ -451,14 +433,12 @@ namespace platf { height = crt_info->height; offset_x = crt_info->x; offset_y = crt_info->y; - } - else { + } else { BOOST_LOG(warning) << "Couldn't get requested display info, defaulting to recording entire virtual desktop"sv; width = xattr.width; height = xattr.height; } - } - else { + } else { width = xattr.width; height = xattr.height; } @@ -472,13 +452,11 @@ namespace platf { /** * Called when the display attributes should change. */ - void - refresh() { + void refresh() { x11::GetWindowAttributes(xdisplay.get(), xwindow, &xattr); // Update xattr's } - capture_e - capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override { + capture_e capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override { auto next_frame = std::chrono::steady_clock::now(); sleep_overshoot_logger.reset(); @@ -523,8 +501,7 @@ namespace platf { return capture_e::ok; } - capture_e - snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor) { + capture_e snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor) { refresh(); // The whole X server changed, so we must reinit everything @@ -538,7 +515,7 @@ namespace platf { } auto img = (x11_img_t *) img_out.get(); - XImage *x_img { x11::GetImage(xdisplay.get(), xwindow, offset_x, offset_y, width, height, AllPlanes, ZPixmap) }; + XImage *x_img {x11::GetImage(xdisplay.get(), xwindow, offset_x, offset_y, width, height, AllPlanes, ZPixmap)}; img->frame_timestamp = std::chrono::steady_clock::now(); img->width = x_img->width; @@ -555,13 +532,11 @@ namespace platf { return capture_e::ok; } - std::shared_ptr - alloc_img() override { + std::shared_ptr alloc_img() override { return std::make_shared(); } - std::unique_ptr - make_avcodec_encode_device(pix_fmt_e pix_fmt) override { + std::unique_ptr make_avcodec_encode_device(pix_fmt_e pix_fmt) override { #ifdef SUNSHINE_BUILD_VAAPI if (mem_type == mem_type_e::vaapi) { return va::make_avcodec_encode_device(width, height, false); @@ -577,8 +552,7 @@ namespace platf { return std::make_unique(); } - int - dummy_img(img_t *img) override { + int dummy_img(img_t *img) override { // TODO: stop cheating and give black image if (!img) { return -1; @@ -605,15 +579,15 @@ namespace platf { task_pool_util::TaskPool::task_id_t refresh_task_id; - void - delayed_refresh() { + void delayed_refresh() { refresh(); refresh_task_id = task_pool.pushDelayed(&shm_attr_t::delayed_refresh, 2s, this).task_id; } shm_attr_t(mem_type_e mem_type): - x11_attr_t(mem_type), shm_xdisplay { x11::OpenDisplay(nullptr) } { + x11_attr_t(mem_type), + shm_xdisplay {x11::OpenDisplay(nullptr)} { refresh_task_id = task_pool.pushDelayed(&shm_attr_t::delayed_refresh, 2s, this).task_id; } @@ -621,8 +595,7 @@ namespace platf { while (!task_pool.cancel(refresh_task_id)); } - capture_e - capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override { + capture_e capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override { auto next_frame = std::chrono::steady_clock::now(); sleep_overshoot_logger.reset(); @@ -667,18 +640,16 @@ namespace platf { return capture_e::ok; } - capture_e - snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor) { + capture_e snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor) { // The whole X server changed, so we must reinit everything if (xattr.width != env_width || xattr.height != env_height) { BOOST_LOG(warning) << "X dimensions changed in SHM mode, request reinit"sv; return capture_e::reinit; - } - else { + } else { auto img_cookie = xcb::shm_get_image_unchecked(xcb.get(), display->root, offset_x, offset_y, width, height, ~0, XCB_IMAGE_FORMAT_Z_PIXMAP, seg, 0); auto frame_timestamp = std::chrono::steady_clock::now(); - xcb_img_t img_reply { xcb::shm_get_image_reply(xcb.get(), img_cookie, nullptr) }; + xcb_img_t img_reply {xcb::shm_get_image_reply(xcb.get(), img_cookie, nullptr)}; if (!img_reply) { BOOST_LOG(error) << "Could not get image reply"sv; return capture_e::reinit; @@ -699,8 +670,7 @@ namespace platf { } } - std::shared_ptr - alloc_img() override { + std::shared_ptr alloc_img() override { auto img = std::make_shared(); img->width = width; img->height = height; @@ -711,13 +681,11 @@ namespace platf { return img; } - int - dummy_img(platf::img_t *img) override { + int dummy_img(platf::img_t *img) override { return 0; } - int - init(const std::string &display_name, const ::video::config_t &config) { + int init(const std::string &display_name, const ::video::config_t &config) { if (x11_attr_t::init(display_name, config)) { return 1; } @@ -756,14 +724,12 @@ namespace platf { return 0; } - std::uint32_t - frame_size() { + std::uint32_t frame_size() { return width * height * 4; } }; - std::shared_ptr - x11_display(platf::mem_type_e hwdevice_type, const std::string &display_name, const ::video::config_t &config) { + std::shared_ptr x11_display(platf::mem_type_e hwdevice_type, const std::string &display_name, const ::video::config_t &config) { if (hwdevice_type != platf::mem_type_e::system && hwdevice_type != platf::mem_type_e::vaapi && hwdevice_type != platf::mem_type_e::cuda) { BOOST_LOG(error) << "Could not initialize x11 display with the given hw device type"sv; return nullptr; @@ -797,8 +763,7 @@ namespace platf { return x11_disp; } - std::vector - x11_display_names() { + std::vector x11_display_names() { if (load_x11() || load_xcb()) { BOOST_LOG(error) << "Couldn't init x11 libraries"sv; @@ -807,18 +772,18 @@ namespace platf { BOOST_LOG(info) << "Detecting displays"sv; - x11::xdisplay_t xdisplay { x11::OpenDisplay(nullptr) }; + x11::xdisplay_t xdisplay {x11::OpenDisplay(nullptr)}; if (!xdisplay) { return {}; } auto xwindow = DefaultRootWindow(xdisplay.get()); - screen_res_t screenr { x11::rr::GetScreenResources(xdisplay.get(), xwindow) }; + screen_res_t screenr {x11::rr::GetScreenResources(xdisplay.get(), xwindow)}; int output = screenr->noutput; int monitor = 0; for (int x = 0; x < output; ++x) { - output_info_t out_info { x11::rr::GetOutputInfo(xdisplay.get(), screenr.get(), screenr->outputs[x]) }; + output_info_t out_info {x11::rr::GetOutputInfo(xdisplay.get(), screenr.get(), screenr->outputs[x])}; if (out_info) { BOOST_LOG(info) << "Detected display: "sv << out_info->name << " (id: "sv << monitor << ")"sv << out_info->name << " connected: "sv << (out_info->connection == RR_Connected); ++monitor; @@ -835,25 +800,22 @@ namespace platf { return names; } - void - freeImage(XImage *p) { + void freeImage(XImage *p) { XDestroyImage(p); } - void - freeX(XFixesCursorImage *p) { + + void freeX(XFixesCursorImage *p) { x11::Free(p); } - int - load_xcb() { + int load_xcb() { // This will be called once only static int xcb_status = xcb::init_shm() || xcb::init(); return xcb_status; } - int - load_x11() { + int load_x11() { // This will be called once only static int x11_status = window_system == window_system_e::NONE || @@ -863,8 +825,7 @@ namespace platf { } namespace x11 { - std::optional - cursor_t::make() { + std::optional cursor_t::make() { if (load_x11()) { return std::nullopt; } @@ -876,8 +837,7 @@ namespace platf { return cursor; } - void - cursor_t::capture(egl::cursor_t &img) { + void cursor_t::capture(egl::cursor_t &img) { auto display = (xdisplay_t::pointer) ctx.get(); xcursor_t xcursor = fix::GetCursorImage(display); @@ -904,23 +864,19 @@ namespace platf { img.serial = xcursor->cursor_serial; } - void - cursor_t::blend(img_t &img, int offsetX, int offsetY) { + void cursor_t::blend(img_t &img, int offsetX, int offsetY) { blend_cursor((xdisplay_t::pointer) ctx.get(), img, offsetX, offsetY); } - xdisplay_t - make_display() { + xdisplay_t make_display() { return OpenDisplay(nullptr); } - void - freeDisplay(_XDisplay *xdisplay) { + void freeDisplay(_XDisplay *xdisplay) { CloseDisplay(xdisplay); } - void - freeCursorCtx(cursor_ctx_t::pointer ctx) { + void freeCursorCtx(cursor_ctx_t::pointer ctx) { CloseDisplay((xdisplay_t::pointer) ctx); } } // namespace x11 diff --git a/src/platform/linux/x11grab.h b/src/platform/linux/x11grab.h index 1322949019a..9d0a1864722 100644 --- a/src/platform/linux/x11grab.h +++ b/src/platform/linux/x11grab.h @@ -4,8 +4,10 @@ */ #pragma once +// standard includes #include +// local includes #include "src/platform/common.h" #include "src/utility.h" @@ -18,21 +20,17 @@ namespace egl { namespace platf::x11 { struct cursor_ctx_raw_t; - void - freeCursorCtx(cursor_ctx_raw_t *ctx); - void - freeDisplay(_XDisplay *xdisplay); + void freeCursorCtx(cursor_ctx_raw_t *ctx); + void freeDisplay(_XDisplay *xdisplay); using cursor_ctx_t = util::safe_ptr; using xdisplay_t = util::safe_ptr<_XDisplay, freeDisplay>; class cursor_t { public: - static std::optional - make(); + static std::optional make(); - void - capture(egl::cursor_t &img); + void capture(egl::cursor_t &img); /** * Capture and blend the cursor into the image @@ -40,12 +38,10 @@ namespace platf::x11 { * img <-- destination image * offsetX, offsetY <--- Top left corner of the virtual screen */ - void - blend(img_t &img, int offsetX, int offsetY); + void blend(img_t &img, int offsetX, int offsetY); cursor_ctx_t ctx; }; - xdisplay_t - make_display(); + xdisplay_t make_display(); } // namespace platf::x11 diff --git a/src/platform/macos/av_audio.h b/src/platform/macos/av_audio.h index bf5b13b3d29..9ef1cca2918 100644 --- a/src/platform/macos/av_audio.h +++ b/src/platform/macos/av_audio.h @@ -4,8 +4,10 @@ */ #pragma once +// platform includes #import +// lib includes #include "third-party/TPCircularBuffer/TPCircularBuffer.h" #define kBufferLength 4096 diff --git a/src/platform/macos/av_audio.m b/src/platform/macos/av_audio.m index f0e631ef311..a274cd6c1ff 100644 --- a/src/platform/macos/av_audio.m +++ b/src/platform/macos/av_audio.m @@ -2,12 +2,13 @@ * @file src/platform/macos/av_audio.m * @brief Definitions for audio capture on macOS. */ +// local includes #import "av_audio.h" @implementation AVAudio + (NSArray *)microphones { - if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:((NSOperatingSystemVersion) { 10, 15, 0 })]) { + if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:((NSOperatingSystemVersion) {10, 15, 0})]) { // This will generate a warning about AVCaptureDeviceDiscoverySession being // unavailable before macOS 10.15, but we have a guard to prevent it from // being called on those earlier systems. @@ -16,14 +17,12 @@ @implementation AVAudio // a different method. #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunguarded-availability-new" - AVCaptureDeviceDiscoverySession *discoverySession = [AVCaptureDeviceDiscoverySession discoverySessionWithDeviceTypes:@[AVCaptureDeviceTypeBuiltInMicrophone, - AVCaptureDeviceTypeExternalUnknown] + AVCaptureDeviceDiscoverySession *discoverySession = [AVCaptureDeviceDiscoverySession discoverySessionWithDeviceTypes:@[AVCaptureDeviceTypeBuiltInMicrophone, AVCaptureDeviceTypeExternalUnknown] mediaType:AVMediaTypeAudio position:AVCaptureDevicePositionUnspecified]; return discoverySession.devices; #pragma clang diagnostic pop - } - else { + } else { // We're intentionally using a deprecated API here specifically for versions // of macOS where it's not deprecated, so we can ignore any deprecation // warnings: @@ -75,8 +74,7 @@ - (int)setupMicrophone:(AVCaptureDevice *)device sampleRate:(UInt32)sampleRate f if ([self.audioCaptureSession canAddInput:audioInput]) { [self.audioCaptureSession addInput:audioInput]; - } - else { + } else { [audioInput dealloc]; return -1; } @@ -92,17 +90,14 @@ - (int)setupMicrophone:(AVCaptureDevice *)device sampleRate:(UInt32)sampleRate f (NSString *) AVLinearPCMIsNonInterleaved: @NO }]; - dispatch_queue_attr_t qos = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_CONCURRENT, - QOS_CLASS_USER_INITIATED, - DISPATCH_QUEUE_PRIORITY_HIGH); + dispatch_queue_attr_t qos = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_CONCURRENT, QOS_CLASS_USER_INITIATED, DISPATCH_QUEUE_PRIORITY_HIGH); dispatch_queue_t recordingQueue = dispatch_queue_create("audioSamplingQueue", qos); [audioOutput setSampleBufferDelegate:self queue:recordingQueue]; if ([self.audioCaptureSession canAddOutput:audioOutput]) { [self.audioCaptureSession addOutput:audioOutput]; - } - else { + } else { [audioInput release]; [audioOutput release]; return -1; diff --git a/src/platform/macos/av_img_t.h b/src/platform/macos/av_img_t.h index 202aa5f8b91..50896c4c568 100644 --- a/src/platform/macos/av_img_t.h +++ b/src/platform/macos/av_img_t.h @@ -4,17 +4,20 @@ */ #pragma once -#include "src/platform/common.h" - +// platform includes #include #include +// local includes +#include "src/platform/common.h" + namespace platf { struct av_sample_buf_t { CMSampleBufferRef buf; explicit av_sample_buf_t(CMSampleBufferRef buf): - buf((CMSampleBufferRef) CFRetain(buf)) {} + buf((CMSampleBufferRef) CFRetain(buf)) { + } ~av_sample_buf_t() { if (buf != nullptr) { @@ -29,12 +32,12 @@ namespace platf { // Constructor explicit av_pixel_buf_t(CMSampleBufferRef sb): buf( - CMSampleBufferGetImageBuffer(sb)) { + CMSampleBufferGetImageBuffer(sb) + ) { CVPixelBufferLockBaseAddress(buf, kCVPixelBufferLock_ReadOnly); } - [[nodiscard]] uint8_t * - data() const { + [[nodiscard]] uint8_t *data() const { return static_cast(CVPixelBufferGetBaseAddress(buf)); } @@ -59,8 +62,11 @@ namespace platf { temp_retain_av_img_t( std::shared_ptr sb, std::shared_ptr pb, - uint8_t *dt): + uint8_t *dt + ): sample_buffer(std::move(sb)), - pixel_buffer(std::move(pb)), data(dt) {} + pixel_buffer(std::move(pb)), + data(dt) { + } }; } // namespace platf diff --git a/src/platform/macos/av_video.h b/src/platform/macos/av_video.h index 1525372d110..4ff24609fef 100644 --- a/src/platform/macos/av_video.h +++ b/src/platform/macos/av_video.h @@ -4,8 +4,9 @@ */ #pragma once -#import +// platform includes #import +#import struct CaptureSession { AVCaptureVideoDataOutput *output; diff --git a/src/platform/macos/av_video.m b/src/platform/macos/av_video.m index 3b58bff55f5..34d70a0c500 100644 --- a/src/platform/macos/av_video.m +++ b/src/platform/macos/av_video.m @@ -2,6 +2,7 @@ * @file src/platform/macos/av_video.m * @brief Definitions for video capture on macOS. */ +// local includes #import "av_video.h" @implementation AVVideo @@ -32,8 +33,7 @@ @implementation AVVideo } + (NSString *)getDisplayName:(CGDirectDisplayID)displayID { - NSScreen *screens = [NSScreen screens]; - for (NSScreen *screen in screens) { + for (NSScreen *screen in [NSScreen screens]) { if (screen.deviceDescription[@"NSScreenNumber"] == [NSNumber numberWithUnsignedInt:displayID]) { return screen.localizedName; } @@ -63,8 +63,7 @@ - (id)initWithDisplay:(CGDirectDisplayID)displayID frameRate:(int)frameRate { if ([self.session canAddInput:screenInput]) { [self.session addInput:screenInput]; - } - else { + } else { [screenInput release]; return nil; } @@ -98,9 +97,7 @@ - (dispatch_semaphore_t)capture:(FrameCallbackBlock)frameCallback { (NSString *) AVVideoScalingModeKey: AVVideoScalingModeResizeAspect, }]; - dispatch_queue_attr_t qos = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, - QOS_CLASS_USER_INITIATED, - DISPATCH_QUEUE_PRIORITY_HIGH); + dispatch_queue_attr_t qos = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INITIATED, DISPATCH_QUEUE_PRIORITY_HIGH); dispatch_queue_t recordingQueue = dispatch_queue_create("videoCaptureQueue", qos); [videoOutput setSampleBufferDelegate:self queue:recordingQueue]; @@ -108,8 +105,7 @@ - (dispatch_semaphore_t)capture:(FrameCallbackBlock)frameCallback { if ([self.session canAddOutput:videoOutput]) { [self.session addOutput:videoOutput]; - } - else { + } else { [videoOutput release]; return nil; } diff --git a/src/platform/macos/display.mm b/src/platform/macos/display.mm index 5220c68db72..be124b2d331 100644 --- a/src/platform/macos/display.mm +++ b/src/platform/macos/display.mm @@ -2,15 +2,15 @@ * @file src/platform/macos/display.mm * @brief Definitions for display capture on macOS. */ +// local includes +#include "src/config.h" +#include "src/logging.h" #include "src/platform/common.h" #include "src/platform/macos/av_img_t.h" #include "src/platform/macos/av_video.h" #include "src/platform/macos/misc.h" #include "src/platform/macos/nv12_zero_device.h" -#include "src/config.h" -#include "src/logging.h" - // Avoid conflict between AVFoundation and libavutil both defining AVMediaType #define AVMediaType AVMediaType_FFmpeg #include "src/video.h" @@ -29,8 +29,7 @@ [av_capture release]; } - capture_e - capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override { + capture_e capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override { auto signal = [av_capture capture:^(CMSampleBufferRef sampleBuffer) { auto new_sample_buffer = std::make_shared(sampleBuffer); auto new_pixel_buffer = std::make_shared(new_sample_buffer->buf); @@ -46,7 +45,8 @@ auto old_data_retainer = std::make_shared( av_img->sample_buffer, av_img->pixel_buffer, - img_out->data); + img_out->data + ); av_img->sample_buffer = new_sample_buffer; av_img->pixel_buffer = new_pixel_buffer; @@ -74,33 +74,28 @@ return capture_e::ok; } - std::shared_ptr - alloc_img() override { + std::shared_ptr alloc_img() override { return std::make_shared(); } - std::unique_ptr - make_avcodec_encode_device(pix_fmt_e pix_fmt) override { + std::unique_ptr make_avcodec_encode_device(pix_fmt_e pix_fmt) override { if (pix_fmt == pix_fmt_e::yuv420p) { av_capture.pixelFormat = kCVPixelFormatType_32BGRA; return std::make_unique(); - } - else if (pix_fmt == pix_fmt_e::nv12 || pix_fmt == pix_fmt_e::p010) { + } else if (pix_fmt == pix_fmt_e::nv12 || pix_fmt == pix_fmt_e::p010) { auto device = std::make_unique(); device->init(static_cast(av_capture), pix_fmt, setResolution, setPixelFormat); return device; - } - else { + } else { BOOST_LOG(error) << "Unsupported Pixel Format."sv; return nullptr; } } - int - dummy_img(img_t *img) override { + int dummy_img(img_t *img) override { if (!platf::is_screen_capture_allowed()) { // If we don't have the screen capture permission, this function will hang // indefinitely without doing anything useful. Exit instead to avoid this. @@ -117,7 +112,8 @@ auto old_data_retainer = std::make_shared( av_img->sample_buffer, av_img->pixel_buffer, - img->data); + img->data + ); av_img->sample_buffer = new_sample_buffer; av_img->pixel_buffer = new_pixel_buffer; @@ -146,19 +142,16 @@ * width --> the intended capture width * height --> the intended capture height */ - static void - setResolution(void *display, int width, int height) { + static void setResolution(void *display, int width, int height) { [static_cast(display) setFrameWidth:width frameHeight:height]; } - static void - setPixelFormat(void *display, OSType pixelFormat) { + static void setPixelFormat(void *display, OSType pixelFormat) { static_cast(display).pixelFormat = pixelFormat; } }; - std::shared_ptr - display(platf::mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config) { + std::shared_ptr display(platf::mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config) { if (hwdevice_type != platf::mem_type_e::system && hwdevice_type != platf::mem_type_e::videotoolbox) { BOOST_LOG(error) << "Could not initialize display with the given hw device type."sv; return nullptr; @@ -200,8 +193,7 @@ return display; } - std::vector - display_names(mem_type_e hwdevice_type) { + std::vector display_names(mem_type_e hwdevice_type) { __block std::vector display_names; auto display_array = [AVVideo displayNames]; @@ -219,8 +211,7 @@ * @brief Returns if GPUs/drivers have changed since the last call to this function. * @return `true` if a change has occurred or if it is unknown whether a change occurred. */ - bool - needs_encoder_reenumeration() { + bool needs_encoder_reenumeration() { // We don't track GPU state, so we will always reenumerate. Fortunately, it is fast on macOS. return true; } diff --git a/src/platform/macos/input.cpp b/src/platform/macos/input.cpp index 6be72233a6e..7e61ab4be45 100644 --- a/src/platform/macos/input.cpp +++ b/src/platform/macos/input.cpp @@ -2,21 +2,24 @@ * @file src/platform/macos/input.cpp * @brief Definitions for macOS input handling. */ -#include "src/input.h" +// standard includes +#include +#include +#include +// platform includes +#include #import -#include +#include #include +// local includes +#include "src/display_device.h" +#include "src/input.h" #include "src/logging.h" #include "src/platform/common.h" #include "src/utility.h" -#include -#include -#include -#include - /** * @brief Delay for a double click, in milliseconds. * @todo Make this configurable. @@ -49,8 +52,7 @@ namespace platf { }; // Customized less operator for using std::lower_bound() on a KeyCodeMap array. - bool - operator<(const KeyCodeMap &a, const KeyCodeMap &b) { + bool operator<(const KeyCodeMap &a, const KeyCodeMap &b) { return a.win_keycode < b.win_keycode; } @@ -226,13 +228,15 @@ const KeyCodeMap kKeyCodesMap[] = { }; // clang-format on - int - keysym(int keycode) { + int keysym(int keycode) { KeyCodeMap key_map {}; key_map.win_keycode = keycode; const KeyCodeMap *temp_map = std::lower_bound( - kKeyCodesMap, kKeyCodesMap + sizeof(kKeyCodesMap) / sizeof(kKeyCodesMap[0]), key_map); + kKeyCodesMap, + kKeyCodesMap + sizeof(kKeyCodesMap) / sizeof(kKeyCodesMap[0]), + key_map + ); if (temp_map >= kKeyCodesMap + sizeof(kKeyCodesMap) / sizeof(kKeyCodesMap[0]) || temp_map->win_keycode != keycode || temp_map->mac_keycode == -1) { @@ -242,8 +246,7 @@ const KeyCodeMap kKeyCodesMap[] = { return temp_map->mac_keycode; } - void - keyboard_update(input_t &input, uint16_t modcode, bool release, uint8_t flags) { + void keyboard_update(input_t &input, uint16_t modcode, bool release, uint8_t flags) { auto key = keysym(modcode); BOOST_LOG(debug) << "got keycode: 0x"sv << std::hex << modcode << ", translated to: 0x" << std::hex << key << ", release:" << release; @@ -283,8 +286,7 @@ const KeyCodeMap kKeyCodesMap[] = { macos_input->kb_flags = release ? macos_input->kb_flags & ~mask : macos_input->kb_flags | mask; CGEventSetType(event, kCGEventFlagsChanged); CGEventSetFlags(event, macos_input->kb_flags); - } - else { + } else { CGEventSetIntegerValueField(event, kCGKeyboardEventKeycode, key); CGEventSetType(event, release ? kCGEventKeyUp : kCGEventKeyDown); } @@ -292,30 +294,25 @@ const KeyCodeMap kKeyCodesMap[] = { CGEventPost(kCGHIDEventTap, event); } - void - unicode(input_t &input, char *utf8, int size) { + void unicode(input_t &input, char *utf8, int size) { BOOST_LOG(info) << "unicode: Unicode input not yet implemented for MacOS."sv; } - int - alloc_gamepad(input_t &input, const gamepad_id_t &id, const gamepad_arrival_t &metadata, feedback_queue_t feedback_queue) { + int alloc_gamepad(input_t &input, const gamepad_id_t &id, const gamepad_arrival_t &metadata, feedback_queue_t feedback_queue) { BOOST_LOG(info) << "alloc_gamepad: Gamepad not yet implemented for MacOS."sv; return -1; } - void - free_gamepad(input_t &input, int nr) { + void free_gamepad(input_t &input, int nr) { BOOST_LOG(info) << "free_gamepad: Gamepad not yet implemented for MacOS."sv; } - void - gamepad_update(input_t &input, int nr, const gamepad_state_t &gamepad_state) { + void gamepad_update(input_t &input, int nr, const gamepad_state_t &gamepad_state) { BOOST_LOG(info) << "gamepad: Gamepad not yet implemented for MacOS."sv; } // returns current mouse location: - util::point_t - get_mouse_loc(input_t &input) { + util::point_t get_mouse_loc(input_t &input) { // Creating a new event every time to avoid any reuse risk const auto macos_input = static_cast(input.get()); const auto snapshot_event = CGEventCreate(macos_input->source); @@ -327,14 +324,14 @@ const KeyCodeMap kKeyCodesMap[] = { }; } - void - post_mouse( + void post_mouse( input_t &input, const CGMouseButton button, const CGEventType type, const util::point_t raw_location, const util::point_t previous_location, - const int click_count) { + const int click_count + ) { BOOST_LOG(debug) << "mouse_event: "sv << button << ", type: "sv << type << ", location:"sv << raw_location.x << ":"sv << raw_location.y << " click_count: "sv << click_count; const auto macos_input = static_cast(input.get()); @@ -367,8 +364,7 @@ const KeyCodeMap kKeyCodesMap[] = { CGWarpMouseCursorPosition(location); } - inline CGEventType - event_type_mouse(input_t &input) { + inline CGEventType event_type_mouse(input_t &input) { const auto macos_input = static_cast(input.get()); if (macos_input->mouse_down[0]) { @@ -383,28 +379,28 @@ const KeyCodeMap kKeyCodesMap[] = { return kCGEventMouseMoved; } - void - move_mouse( + void move_mouse( input_t &input, const int deltaX, - const int deltaY) { + const int deltaY + ) { const auto current = get_mouse_loc(input); - const auto location = util::point_t { current.x + deltaX, current.y + deltaY }; + const auto location = util::point_t {current.x + deltaX, current.y + deltaY}; post_mouse(input, kCGMouseButtonLeft, event_type_mouse(input), location, current, 0); } - void - abs_mouse( + void abs_mouse( input_t &input, const touch_port_t &touch_port, const float x, - const float y) { + const float y + ) { const auto macos_input = static_cast(input.get()); const auto scaling = macos_input->displayScaling; const auto display = macos_input->display; - auto location = util::point_t { x * scaling, y * scaling }; + auto location = util::point_t {x * scaling, y * scaling}; CGRect display_bounds = CGDisplayBounds(display); // in order to get the correct mouse location for capturing display , we need to add the display bounds to the location location.x += display_bounds.origin.x; @@ -413,8 +409,7 @@ const KeyCodeMap kKeyCodesMap[] = { post_mouse(input, kCGMouseButtonLeft, event_type_mouse(input), location, get_mouse_loc(input), 0); } - void - button_mouse(input_t &input, const int button, const bool release) { + void button_mouse(input_t &input, const int button, const bool release) { CGMouseButton mac_button; CGEventType event; @@ -446,26 +441,26 @@ const KeyCodeMap kKeyCodesMap[] = { if (now < macos_input->last_mouse_event[mac_button][release] + MULTICLICK_DELAY_MS) { post_mouse(input, mac_button, event, mouse_position, mouse_position, 2); - } - else { + } else { post_mouse(input, mac_button, event, mouse_position, mouse_position, 1); } macos_input->last_mouse_event[mac_button][release] = now; } - void - scroll(input_t &input, const int high_res_distance) { + void scroll(input_t &input, const int high_res_distance) { CGEventRef upEvent = CGEventCreateScrollWheelEvent( nullptr, kCGScrollEventUnitLine, - 2, high_res_distance > 0 ? 1 : -1, high_res_distance); + 2, + high_res_distance > 0 ? 1 : -1, + high_res_distance + ); CGEventPost(kCGHIDEventTap, upEvent); CFRelease(upEvent); } - void - hscroll(input_t &input, int high_res_distance) { + void hscroll(input_t &input, int high_res_distance) { // Unimplemented } @@ -474,8 +469,7 @@ const KeyCodeMap kKeyCodesMap[] = { * @param input The global input context. * @return A unique pointer to a per-client input data context. */ - std::unique_ptr - allocate_client_input_context(input_t &input) { + std::unique_ptr allocate_client_input_context(input_t &input) { // Unused return nullptr; } @@ -486,8 +480,7 @@ const KeyCodeMap kKeyCodesMap[] = { * @param touch_port The current viewport for translating to screen coordinates. * @param touch The touch event. */ - void - touch_update(client_input_t *input, const touch_port_t &touch_port, const touch_input_t &touch) { + void touch_update(client_input_t *input, const touch_port_t &touch_port, const touch_input_t &touch) { // Unimplemented feature - platform_caps::pen_touch } @@ -497,8 +490,7 @@ const KeyCodeMap kKeyCodesMap[] = { * @param touch_port The current viewport for translating to screen coordinates. * @param pen The pen event. */ - void - pen_update(client_input_t *input, const touch_port_t &touch_port, const pen_input_t &pen) { + void pen_update(client_input_t *input, const touch_port_t &touch_port, const pen_input_t &pen) { // Unimplemented feature - platform_caps::pen_touch } @@ -507,8 +499,7 @@ const KeyCodeMap kKeyCodesMap[] = { * @param input The global input context. * @param touch The touch event. */ - void - gamepad_touch(input_t &input, const gamepad_touch_t &touch) { + void gamepad_touch(input_t &input, const gamepad_touch_t &touch) { // Unimplemented feature - platform_caps::controller_touch } @@ -517,8 +508,7 @@ const KeyCodeMap kKeyCodesMap[] = { * @param input The global input context. * @param motion The motion event. */ - void - gamepad_motion(input_t &input, const gamepad_motion_t &motion) { + void gamepad_motion(input_t &input, const gamepad_motion_t &motion) { // Unimplemented } @@ -527,21 +517,19 @@ const KeyCodeMap kKeyCodesMap[] = { * @param input The global input context. * @param battery The battery event. */ - void - gamepad_battery(input_t &input, const gamepad_battery_t &battery) { + void gamepad_battery(input_t &input, const gamepad_battery_t &battery) { // Unimplemented } - input_t - input() { - input_t result { new macos_input_t() }; + input_t input() { + input_t result {new macos_input_t()}; const auto macos_input = static_cast(result.get()); // Default to main display macos_input->display = CGMainDisplayID(); - auto output_name = config::video.output_name; + auto output_name = display_device::map_output_name(config::video.output_name); // If output_name is set, try to find the display with that display id if (!output_name.empty()) { uint32_t max_display = 32; @@ -549,8 +537,7 @@ const KeyCodeMap kKeyCodesMap[] = { CGDirectDisplayID displays[max_display]; if (CGGetActiveDisplayList(max_display, displays, &display_count) != kCGErrorSuccess) { BOOST_LOG(error) << "Unable to get active display list , error: "sv << std::endl; - } - else { + } else { for (int i = 0; i < display_count; i++) { CGDirectDisplayID display_id = displays[i]; if (display_id == std::atoi(output_name.c_str())) { @@ -580,8 +567,7 @@ const KeyCodeMap kKeyCodesMap[] = { return result; } - void - freeInput(void *p) { + void freeInput(void *p) { const auto *input = static_cast(p); CFRelease(input->source); @@ -591,10 +577,9 @@ const KeyCodeMap kKeyCodesMap[] = { delete input; } - std::vector & - supported_gamepads(input_t *input) { + std::vector &supported_gamepads(input_t *input) { static std::vector gamepads { - supported_gamepad_t { "", false, "gamepads.macos_not_implemented" } + supported_gamepad_t {"", false, "gamepads.macos_not_implemented"} }; return gamepads; @@ -604,8 +589,7 @@ const KeyCodeMap kKeyCodesMap[] = { * @brief Returns the supported platform capabilities to advertise to the client. * @return Capability flags. */ - platform_caps::caps_t - get_capabilities() { + platform_caps::caps_t get_capabilities() { return 0; } } // namespace platf diff --git a/src/platform/macos/microphone.mm b/src/platform/macos/microphone.mm index 1e3a4cd65ed..06b9c19a899 100644 --- a/src/platform/macos/microphone.mm +++ b/src/platform/macos/microphone.mm @@ -2,11 +2,11 @@ * @file src/platform/macos/microphone.mm * @brief Definitions for microphone capture on macOS. */ -#include "src/platform/common.h" -#include "src/platform/macos/av_audio.h" - +// local includes #include "src/config.h" #include "src/logging.h" +#include "src/platform/common.h" +#include "src/platform/macos/av_audio.h" namespace platf { using namespace std::literals; @@ -18,8 +18,7 @@ [av_audio_capture release]; } - capture_e - sample(std::vector &sample_in) override { + capture_e sample(std::vector &sample_in) override { auto sample_size = sample_in.size(); uint32_t length = 0; @@ -45,14 +44,12 @@ AVCaptureDevice *audio_capture_device {}; public: - int - set_sink(const std::string &sink) override { + int set_sink(const std::string &sink) override { BOOST_LOG(warning) << "audio_control_t::set_sink() unimplemented: "sv << sink; 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) override { auto mic = std::make_unique(); const char *audio_sink = ""; @@ -81,16 +78,19 @@ return mic; } - std::optional - sink_info() override { + bool is_sink_available(const std::string &sink) override { + BOOST_LOG(warning) << "audio_control_t::is_sink_available() unimplemented: "sv << sink; + return true; + } + + std::optional sink_info() override { sink_t sink; return sink; } }; - std::unique_ptr - audio_control() { + std::unique_ptr audio_control() { return std::make_unique(); } } // namespace platf diff --git a/src/platform/macos/misc.h b/src/platform/macos/misc.h index 47d22ed4eca..e791661d1c4 100644 --- a/src/platform/macos/misc.h +++ b/src/platform/macos/misc.h @@ -4,21 +4,20 @@ */ #pragma once +// standard includes #include +// platform includes #include namespace platf { - bool - is_screen_capture_allowed(); + bool is_screen_capture_allowed(); } namespace dyn { typedef void (*apiproc)(); - int - load(void *handle, const std::vector> &funcs, bool strict = true); - void * - handle(const std::vector &libs); + int load(void *handle, const std::vector> &funcs, bool strict = true); + void *handle(const std::vector &libs); } // namespace dyn diff --git a/src/platform/macos/misc.mm b/src/platform/macos/misc.mm index 9f11669f0b9..540dd74ff04 100644 --- a/src/platform/macos/misc.mm +++ b/src/platform/macos/misc.mm @@ -8,24 +8,29 @@ #define __APPLE_USE_RFC_3542 1 #endif -#include -#include -#include +// standard includes #include #include + +// platform includes +#include +#include +#include #include #include #include +// lib includes +#include +#include +#include + +// local includes #include "misc.h" #include "src/entry_handler.h" #include "src/logging.h" #include "src/platform/common.h" -#include -#include -#include - using namespace std::literals; namespace fs = std::filesystem; namespace bp = boost::process; @@ -37,24 +42,20 @@ #if __MAC_OS_X_VERSION_MAX_ALLOWED < 110000 // __MAC_11_0 // If they're not in the SDK then we can use our own function definitions. // Need to use weak import so that this will link in macOS 10.14 and earlier - extern "C" bool - CGPreflightScreenCaptureAccess(void) __attribute__((weak_import)); - extern "C" bool - CGRequestScreenCaptureAccess(void) __attribute__((weak_import)); + extern "C" bool CGPreflightScreenCaptureAccess(void) __attribute__((weak_import)); + extern "C" bool CGRequestScreenCaptureAccess(void) __attribute__((weak_import)); #endif namespace { - auto screen_capture_allowed = std::atomic { false }; + auto screen_capture_allowed = std::atomic {false}; } // namespace // Return whether screen capture is allowed for this process. - bool - is_screen_capture_allowed() { + bool is_screen_capture_allowed() { return screen_capture_allowed; } - std::unique_ptr - init() { + std::unique_ptr init() { // This will generate a warning about CGPreflightScreenCaptureAccess and // CGRequestScreenCaptureAccess being unavailable before macOS 10.15, but // we have a guard to prevent it from being called on those earlier systems. @@ -69,7 +70,7 @@ #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunguarded-availability-new" #pragma clang diagnostic ignored "-Wtautological-pointer-compare" - if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:((NSOperatingSystemVersion) { 10, 15, 0 })] && + if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:((NSOperatingSystemVersion) {10, 15, 0})] && // Double check that these weakly-linked symbols have been loaded: CGPreflightScreenCaptureAccess != nullptr && CGRequestScreenCaptureAccess != nullptr && !CGPreflightScreenCaptureAccess()) { @@ -84,66 +85,55 @@ return std::make_unique(); } - fs::path - appdata() { + fs::path appdata() { const char *homedir; if ((homedir = getenv("HOME")) == nullptr) { homedir = getpwuid(geteuid())->pw_dir; } - return fs::path { homedir } / ".config/sunshine"sv; + return fs::path {homedir} / ".config/sunshine"sv; } using ifaddr_t = util::safe_ptr; - ifaddr_t - get_ifaddrs() { - ifaddrs *p { nullptr }; + ifaddr_t get_ifaddrs() { + ifaddrs *p {nullptr}; getifaddrs(&p); - return ifaddr_t { p }; + return ifaddr_t {p}; } - std::string - from_sockaddr(const sockaddr *const ip_addr) { + std::string from_sockaddr(const sockaddr *const ip_addr) { char data[INET6_ADDRSTRLEN] = {}; auto family = ip_addr->sa_family; if (family == AF_INET6) { - inet_ntop(AF_INET6, &((sockaddr_in6 *) ip_addr)->sin6_addr, data, - INET6_ADDRSTRLEN); - } - else if (family == AF_INET) { - inet_ntop(AF_INET, &((sockaddr_in *) ip_addr)->sin_addr, data, - INET_ADDRSTRLEN); + inet_ntop(AF_INET6, &((sockaddr_in6 *) ip_addr)->sin6_addr, data, INET6_ADDRSTRLEN); + } else if (family == AF_INET) { + inet_ntop(AF_INET, &((sockaddr_in *) ip_addr)->sin_addr, data, INET_ADDRSTRLEN); } - return std::string { data }; + return std::string {data}; } - std::pair - from_sockaddr_ex(const sockaddr *const ip_addr) { + std::pair from_sockaddr_ex(const sockaddr *const ip_addr) { char data[INET6_ADDRSTRLEN] = {}; auto family = ip_addr->sa_family; std::uint16_t port = 0; if (family == AF_INET6) { - inet_ntop(AF_INET6, &((sockaddr_in6 *) ip_addr)->sin6_addr, data, - INET6_ADDRSTRLEN); + inet_ntop(AF_INET6, &((sockaddr_in6 *) ip_addr)->sin6_addr, data, INET6_ADDRSTRLEN); port = ((sockaddr_in6 *) ip_addr)->sin6_port; - } - else if (family == AF_INET) { - inet_ntop(AF_INET, &((sockaddr_in *) ip_addr)->sin_addr, data, - INET_ADDRSTRLEN); + } else if (family == AF_INET) { + inet_ntop(AF_INET, &((sockaddr_in *) ip_addr)->sin_addr, data, INET_ADDRSTRLEN); port = ((sockaddr_in *) ip_addr)->sin_port; } - return { port, std::string { data } }; + return {port, std::string {data}}; } - std::string - get_mac_address(const std::string_view &address) { + std::string get_mac_address(const std::string_view &address) { auto ifaddrs = get_ifaddrs(); for (auto pos = ifaddrs.get(); pos != nullptr; pos = pos->ifa_next) { @@ -160,8 +150,7 @@ ptr = (unsigned char *) LLADDR((struct sockaddr_dl *) (ifaptr)->ifa_addr); char buff[100]; - snprintf(buff, sizeof(buff), "%02x:%02x:%02x:%02x:%02x:%02x", - *ptr, *(ptr + 1), *(ptr + 2), *(ptr + 3), *(ptr + 4), *(ptr + 5)); + snprintf(buff, sizeof(buff), "%02x:%02x:%02x:%02x:%02x:%02x", *ptr, *(ptr + 1), *(ptr + 2), *(ptr + 3), *(ptr + 4), *(ptr + 5)); mac_address = buff; break; } @@ -181,8 +170,7 @@ return "00:00:00:00:00:00"s; } - bp::child - run_command(bool elevated, bool interactive, const std::string &cmd, boost::filesystem::path &working_dir, const bp::environment &env, FILE *file, std::error_code &ec, bp::group *group) { + bp::child run_command(bool elevated, bool interactive, const std::string &cmd, boost::filesystem::path &working_dir, const bp::environment &env, FILE *file, std::error_code &ec, bp::group *group) { // clang-format off if (!group) { if (!file) { @@ -207,8 +195,7 @@ * @brief Open a url in the default web browser. * @param url The url to open. */ - void - open_url(const std::string &url) { + void open_url(const std::string &url) { boost::filesystem::path working_dir; std::string cmd = R"(open ")" + url + R"(")"; @@ -217,30 +204,25 @@ auto child = run_command(false, false, cmd, working_dir, _env, nullptr, ec, nullptr); if (ec) { BOOST_LOG(warning) << "Couldn't open url ["sv << url << "]: System: "sv << ec.message(); - } - else { + } else { BOOST_LOG(info) << "Opened url ["sv << url << "]"sv; child.detach(); } } - void - adjust_thread_priority(thread_priority_e priority) { + void adjust_thread_priority(thread_priority_e priority) { // Unimplemented } - void - streaming_will_start() { + void streaming_will_start() { // Nothing to do } - void - streaming_will_stop() { + void streaming_will_stop() { // Nothing to do } - void - restart_on_exit() { + void restart_on_exit() { char executable[2048]; uint32_t size = sizeof(executable); if (_NSGetExecutablePath(executable, &size) < 0) { @@ -261,42 +243,35 @@ } } - void - restart() { + void restart() { // Gracefully clean up and restart ourselves instead of exiting atexit(restart_on_exit); lifetime::exit_sunshine(0, true); } - int - set_env(const std::string &name, const std::string &value) { + int set_env(const std::string &name, const std::string &value) { return setenv(name.c_str(), value.c_str(), 1); } - int - unset_env(const std::string &name) { + int unset_env(const std::string &name) { return unsetenv(name.c_str()); } - bool - request_process_group_exit(std::uintptr_t native_handle) { + bool request_process_group_exit(std::uintptr_t native_handle) { if (killpg((pid_t) native_handle, SIGTERM) == 0 || errno == ESRCH) { BOOST_LOG(debug) << "Successfully sent SIGTERM to process group: "sv << native_handle; return true; - } - else { + } else { BOOST_LOG(warning) << "Unable to send SIGTERM to process group ["sv << native_handle << "]: "sv << errno; return false; } } - bool - process_group_running(std::uintptr_t native_handle) { + bool process_group_running(std::uintptr_t native_handle) { return waitpid(-((pid_t) native_handle), nullptr, WNOHANG) >= 0; } - struct sockaddr_in - to_sockaddr(boost::asio::ip::address_v4 address, uint16_t port) { + struct sockaddr_in to_sockaddr(boost::asio::ip::address_v4 address, uint16_t port) { struct sockaddr_in saddr_v4 = {}; saddr_v4.sin_family = AF_INET; @@ -308,8 +283,7 @@ return saddr_v4; } - struct sockaddr_in6 - to_sockaddr(boost::asio::ip::address_v6 address, uint16_t port) { + struct sockaddr_in6 to_sockaddr(boost::asio::ip::address_v6 address, uint16_t port) { struct sockaddr_in6 saddr_v6 = {}; saddr_v6.sin6_family = AF_INET6; @@ -322,14 +296,12 @@ return saddr_v6; } - bool - send_batch(batched_send_info_t &send_info) { + bool send_batch(batched_send_info_t &send_info) { // Fall back to unbatched send calls return false; } - bool - send(send_info_t &send_info) { + bool send(send_info_t &send_info) { auto sockfd = (int) send_info.native_socket; struct msghdr msg = {}; @@ -341,8 +313,7 @@ msg.msg_name = (struct sockaddr *) &taddr_v6; msg.msg_namelen = sizeof(taddr_v6); - } - else { + } else { taddr_v4 = to_sockaddr(send_info.target_address.to_v4(), send_info.target_port); msg.msg_name = (struct sockaddr *) &taddr_v4; @@ -353,6 +324,7 @@ char buf[std::max(CMSG_SPACE(sizeof(struct in_pktinfo)), CMSG_SPACE(sizeof(struct in6_pktinfo)))]; struct cmsghdr alignment; } cmbuf {}; + socklen_t cmbuflen = 0; msg.msg_control = cmbuf.buf; @@ -372,8 +344,7 @@ pktinfo_cm->cmsg_type = IPV6_PKTINFO; pktinfo_cm->cmsg_len = CMSG_LEN(sizeof(pktInfo)); memcpy(CMSG_DATA(pktinfo_cm), &pktInfo, sizeof(pktInfo)); - } - else { + } else { struct in_pktinfo pktInfo {}; struct sockaddr_in saddr_v4 = to_sockaddr(send_info.source_address.to_v4(), 0); @@ -438,7 +409,8 @@ class qos_t: public deinit_t { public: qos_t(int sockfd, std::vector> options): - sockfd(sockfd), options(options) { + sockfd(sockfd), + options(options) { qos_ref_count++; } @@ -466,8 +438,7 @@ * @param data_type The type of traffic sent on this socket. * @param dscp_tagging Specifies whether to enable DSCP tagging on outgoing traffic. */ - std::unique_ptr - enable_socket_qos(uintptr_t native_socket, boost::asio::ip::address &address, uint16_t port, qos_data_type_e data_type, bool dscp_tagging) { + std::unique_ptr enable_socket_qos(uintptr_t native_socket, boost::asio::ip::address &address, uint16_t port, qos_data_type_e data_type, bool dscp_tagging) { int sockfd = (int) native_socket; std::vector> reset_options; @@ -489,8 +460,7 @@ if (setsockopt(sockfd, SOL_SOCKET, SO_NET_SERVICE_TYPE, &service_type, sizeof(service_type)) == 0) { // Reset SO_NET_SERVICE_TYPE to best-effort when QoS is disabled reset_options.emplace_back(std::make_tuple(SOL_SOCKET, SO_NET_SERVICE_TYPE, NET_SERVICE_TYPE_BE)); - } - else { + } else { BOOST_LOG(error) << "Failed to set SO_NET_SERVICE_TYPE: "sv << errno; } } @@ -501,8 +471,7 @@ if (address.is_v6()) { level = IPPROTO_IPV6; option = IPV6_TCLASS; - } - else { + } else { level = IPPROTO_IP; option = IP_TOS; } @@ -529,8 +498,7 @@ if (setsockopt(sockfd, level, option, &dscp, sizeof(dscp)) == 0) { // Reset TOS to -1 when QoS is disabled reset_options.emplace_back(std::make_tuple(level, option, -1)); - } - else { + } else { BOOST_LOG(error) << "Failed to set TOS/TCLASS: "sv << errno; } } @@ -539,12 +507,10 @@ return std::make_unique(sockfd, reset_options); } - std::string - get_host_name() { + std::string get_host_name() { try { return boost::asio::ip::host_name(); - } - catch (boost::system::system_error &err) { + } catch (boost::system::system_error &err) { BOOST_LOG(error) << "Failed to get hostname: "sv << err.what(); return "Sunshine"s; } @@ -552,8 +518,7 @@ class macos_high_precision_timer: public high_precision_timer { public: - void - sleep_for(const std::chrono::nanoseconds &duration) override { + void sleep_for(const std::chrono::nanoseconds &duration) override { std::this_thread::sleep_for(duration); } @@ -562,15 +527,13 @@ operator bool() override { } }; - std::unique_ptr - create_high_precision_timer() { + std::unique_ptr create_high_precision_timer() { return std::make_unique(); } } // namespace platf namespace dyn { - void * - handle(const std::vector &libs) { + void *handle(const std::vector &libs) { void *handle; for (auto lib : libs) { @@ -593,8 +556,7 @@ operator bool() override { return nullptr; } - int - load(void *handle, const std::vector> &funcs, bool strict) { + int load(void *handle, const std::vector> &funcs, bool strict) { int err = 0; for (auto &func : funcs) { TUPLE_2D_REF(fn, name, func); diff --git a/src/platform/macos/nv12_zero_device.cpp b/src/platform/macos/nv12_zero_device.cpp index e50028902ee..b4fb28cb736 100644 --- a/src/platform/macos/nv12_zero_device.cpp +++ b/src/platform/macos/nv12_zero_device.cpp @@ -2,11 +2,12 @@ * @file src/platform/macos/nv12_zero_device.cpp * @brief Definitions for NV12 zero copy device on macOS. */ +// standard includes #include +// local includes #include "src/platform/macos/av_img_t.h" #include "src/platform/macos/nv12_zero_device.h" - #include "src/video.h" extern "C" { @@ -15,20 +16,17 @@ extern "C" { namespace platf { - void - free_frame(AVFrame *frame) { + void free_frame(AVFrame *frame) { av_frame_free(&frame); } - void - free_buffer(void *opaque, uint8_t *data) { + void free_buffer(void *opaque, uint8_t *data) { CVPixelBufferRelease((CVPixelBufferRef) data); } util::safe_ptr av_frame; - int - nv12_zero_device::convert(platf::img_t &img) { + int nv12_zero_device::convert(platf::img_t &img) { auto *av_img = (av_img_t *) &img; // Release any existing CVPixelBuffer previously retained for encoding @@ -47,8 +45,7 @@ namespace platf { return 0; } - int - nv12_zero_device::set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) { + int nv12_zero_device::set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) { this->frame = frame; av_frame.reset(frame); @@ -58,11 +55,8 @@ namespace platf { return 0; } - int - nv12_zero_device::init(void *display, pix_fmt_e pix_fmt, resolution_fn_t resolution_fn, const pixel_format_fn_t &pixel_format_fn) { - pixel_format_fn(display, pix_fmt == pix_fmt_e::nv12 ? - kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange : - kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange); + int nv12_zero_device::init(void *display, pix_fmt_e pix_fmt, resolution_fn_t resolution_fn, const pixel_format_fn_t &pixel_format_fn) { + pixel_format_fn(display, pix_fmt == pix_fmt_e::nv12 ? kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange : kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange); this->display = display; this->resolution_fn = std::move(resolution_fn); diff --git a/src/platform/macos/nv12_zero_device.h b/src/platform/macos/nv12_zero_device.h index fd9f80f86d8..3aa7540c3f3 100644 --- a/src/platform/macos/nv12_zero_device.h +++ b/src/platform/macos/nv12_zero_device.h @@ -4,13 +4,13 @@ */ #pragma once +// local includes #include "src/platform/common.h" struct AVFrame; namespace platf { - void - free_frame(AVFrame *frame); + void free_frame(AVFrame *frame); class nv12_zero_device: public avcodec_encode_device_t { // display holds a pointer to an av_video object. Since the namespaces of AVFoundation @@ -24,13 +24,10 @@ namespace platf { resolution_fn_t resolution_fn; using pixel_format_fn_t = std::function; - int - init(void *display, pix_fmt_e pix_fmt, resolution_fn_t resolution_fn, const pixel_format_fn_t &pixel_format_fn); + int init(void *display, pix_fmt_e pix_fmt, resolution_fn_t resolution_fn, const pixel_format_fn_t &pixel_format_fn); - int - convert(img_t &img) override; - int - set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) override; + int convert(img_t &img) override; + int set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) override; private: util::safe_ptr av_frame; diff --git a/src/platform/macos/publish.cpp b/src/platform/macos/publish.cpp index b8c977c0673..0718b784f50 100644 --- a/src/platform/macos/publish.cpp +++ b/src/platform/macos/publish.cpp @@ -2,9 +2,13 @@ * @file src/platform/macos/publish.cpp * @brief Definitions for publishing services on macOS. */ -#include +// standard includes #include +// platform includes +#include + +// local includes #include "src/logging.h" #include "src/network.h" #include "src/nvhttp.h" @@ -17,12 +21,13 @@ namespace platf::publish { /** @brief Custom deleter intended to be used for `std::unique_ptr`. */ struct ServiceRefDeleter { typedef DNSServiceRef pointer; ///< Type of object to be deleted. - void - operator()(pointer serviceRef) { + + void operator()(pointer serviceRef) { DNSServiceRefDeallocate(serviceRef); BOOST_LOG(info) << "Deregistered DNS service."sv; } }; + /** @brief This class encapsulates the polling and deinitialization of our connection with * the mDNS service. Implements the `::platf::deinit_t` interface. */ @@ -37,25 +42,25 @@ namespace platf::publish { */ deinit_t(DNSServiceRef serviceRef): unique_ptr(serviceRef) { - _thread = std::thread { [serviceRef, &_stopRequested = std::as_const(_stopRequested)]() { + _thread = std::thread {[serviceRef, &_stopRequested = std::as_const(_stopRequested)]() { const auto socket = DNSServiceRefSockFD(serviceRef); while (!_stopRequested) { auto fdset = fd_set {}; FD_ZERO(&fdset); FD_SET(socket, &fdset); - auto timeout = timeval { .tv_sec = 3, .tv_usec = 0 }; // 3 second timeout + auto timeout = timeval {.tv_sec = 3, .tv_usec = 0}; // 3 second timeout const auto ready = select(socket + 1, &fdset, nullptr, nullptr, &timeout); if (ready == -1) { BOOST_LOG(error) << "Failed to obtain response from DNS service."sv; break; - } - else if (ready != 0) { + } else if (ready != 0) { DNSServiceProcessResult(serviceRef); break; } } - } }; + }}; } + /** @brief Ensure that we gracefully finish polling the mDNS service before freeing our * connection to it. */ @@ -63,9 +68,9 @@ namespace platf::publish { _stopRequested = true; _thread.join(); } + deinit_t(const deinit_t &) = delete; - deinit_t & - operator=(const deinit_t &) = delete; + deinit_t &operator=(const deinit_t &) = delete; private: std::thread _thread; ///< Thread for polling the mDNS service for a response. @@ -75,10 +80,7 @@ namespace platf::publish { /** @brief Callback that will be invoked when the mDNS service finishes registering our service. * @param errorCode Describes whether the registration was successful. */ - void - registrationCallback(DNSServiceRef /*serviceRef*/, DNSServiceFlags /*flags*/, - DNSServiceErrorType errorCode, const char * /*name*/, - const char * /*regtype*/, const char * /*domain*/, void * /*context*/) { + void registrationCallback(DNSServiceRef /*serviceRef*/, DNSServiceFlags /*flags*/, DNSServiceErrorType errorCode, const char * /*name*/, const char * /*regtype*/, const char * /*domain*/, void * /*context*/) { if (errorCode != kDNSServiceErr_NoError) { BOOST_LOG(error) << "Failed to register DNS service: Error "sv << errorCode; return; @@ -98,8 +100,7 @@ namespace platf::publish { * which will manage polling for a response from the mDNS service, and then, when * deconstructed, will deregister the service. */ - [[nodiscard]] std::unique_ptr<::platf::deinit_t> - start() { + [[nodiscard]] std::unique_ptr<::platf::deinit_t> start() { auto serviceRef = DNSServiceRef {}; const auto status = DNSServiceRegister( &serviceRef, diff --git a/src/platform/windows/PolicyConfig.h b/src/platform/windows/PolicyConfig.h index 087f85fa84f..a61d28733b5 100644 --- a/src/platform/windows/PolicyConfig.h +++ b/src/platform/windows/PolicyConfig.h @@ -8,14 +8,15 @@ #pragma once +// platform includes #include #ifdef __MINGW32__ #undef DEFINE_GUID #ifdef __cplusplus - #define DEFINE_GUID(name, l, w1, w2, b1, b2, b3, b4, b5, b6, b7, b8) EXTERN_C const GUID DECLSPEC_SELECTANY name = { l, w1, w2, { b1, b2, b3, b4, b5, b6, b7, b8 } } + #define DEFINE_GUID(name, l, w1, w2, b1, b2, b3, b4, b5, b6, b7, b8) EXTERN_C const GUID DECLSPEC_SELECTANY name = {l, w1, w2, {b1, b2, b3, b4, b5, b6, b7, b8}} #else - #define DEFINE_GUID(name, l, w1, w2, b1, b2, b3, b4, b5, b6, b7, b8) const GUID DECLSPEC_SELECTANY name = { l, w1, w2, { b1, b2, b3, b4, b5, b6, b7, b8 } } + #define DEFINE_GUID(name, l, w1, w2, b1, b2, b3, b4, b5, b6, b7, b8) const GUID DECLSPEC_SELECTANY name = {l, w1, w2, {b1, b2, b3, b4, b5, b6, b7, b8}} #endif DEFINE_GUID(IID_IPolicyConfig, 0xf8679f50, 0x850a, 0x41cf, 0x9c, 0x72, 0x43, 0x0f, 0x29, 0x02, 0x90, 0xc8); @@ -40,64 +41,69 @@ class DECLSPEC_UUID("870af99c-171d-4f9e-af0d-e63df40c2bc9") CPolicyConfigClient; // ---------------------------------------------------------------------------- interface IPolicyConfig: public IUnknown { public: - virtual HRESULT - GetMixFormat( + virtual HRESULT GetMixFormat( PCWSTR, - WAVEFORMATEX **); + WAVEFORMATEX ** + ); - virtual HRESULT STDMETHODCALLTYPE - GetDeviceFormat( + virtual HRESULT STDMETHODCALLTYPE GetDeviceFormat( PCWSTR, INT, - WAVEFORMATEX **); + WAVEFORMATEX ** + ); virtual HRESULT STDMETHODCALLTYPE ResetDeviceFormat( - PCWSTR); + PCWSTR + ); virtual HRESULT STDMETHODCALLTYPE - SetDeviceFormat( - PCWSTR, - WAVEFORMATEX *, - WAVEFORMATEX *); + SetDeviceFormat( + PCWSTR, + WAVEFORMATEX *, + WAVEFORMATEX * + ); virtual HRESULT STDMETHODCALLTYPE GetProcessingPeriod( PCWSTR, INT, PINT64, - PINT64); + PINT64 + ); virtual HRESULT STDMETHODCALLTYPE SetProcessingPeriod( PCWSTR, - PINT64); + PINT64 + ); - virtual HRESULT STDMETHODCALLTYPE - GetShareMode( + virtual HRESULT STDMETHODCALLTYPE GetShareMode( PCWSTR, - struct DeviceShareMode *); + struct DeviceShareMode * + ); - virtual HRESULT STDMETHODCALLTYPE - SetShareMode( + virtual HRESULT STDMETHODCALLTYPE SetShareMode( PCWSTR, - struct DeviceShareMode *); + struct DeviceShareMode * + ); - virtual HRESULT STDMETHODCALLTYPE - GetPropertyValue( + virtual HRESULT STDMETHODCALLTYPE GetPropertyValue( PCWSTR, const PROPERTYKEY &, - PROPVARIANT *); + PROPVARIANT * + ); - virtual HRESULT STDMETHODCALLTYPE - SetPropertyValue( + virtual HRESULT STDMETHODCALLTYPE SetPropertyValue( PCWSTR, const PROPERTYKEY &, - PROPVARIANT *); + PROPVARIANT * + ); - virtual HRESULT STDMETHODCALLTYPE - SetDefaultEndpoint( + virtual HRESULT STDMETHODCALLTYPE SetDefaultEndpoint( PCWSTR wszDeviceId, - ERole eRole); + ERole eRole + ); virtual HRESULT STDMETHODCALLTYPE SetEndpointVisibility( PCWSTR, - INT); + INT + ); }; diff --git a/src/platform/windows/audio.cpp b/src/platform/windows/audio.cpp index 3335eeb0b0f..7c38b1bcacc 100644 --- a/src/platform/windows/audio.cpp +++ b/src/platform/windows/audio.cpp @@ -3,22 +3,21 @@ * @brief Definitions for Windows audio capture. */ #define INITGUID + +// platform includes #include +#include #include +#include #include - #include -#include - -#include - +// local includes +#include "misc.h" #include "src/config.h" #include "src/logging.h" #include "src/platform/common.h" -#include "misc.h" - // Must be the last included file // clang-format off #include "PolicyConfig.h" @@ -65,8 +64,7 @@ namespace { _size, }; - constexpr WAVEFORMATEXTENSIBLE - create_waveformat(sample_format_e sample_format, WORD channel_count, DWORD channel_mask) { + constexpr WAVEFORMATEXTENSIBLE create_waveformat(sample_format_e sample_format, WORD channel_count, DWORD channel_mask) { WAVEFORMATEXTENSIBLE waveformat = {}; switch (sample_format) { @@ -119,19 +117,28 @@ namespace { using virtual_sink_waveformats_t = std::vector; - template - virtual_sink_waveformats_t - create_virtual_sink_waveformats() { + /** + * @brief List of supported waveformats for an N-channel virtual audio device + * @tparam channel_count Number of virtual audio channels + * @returns std::vector + * @note The list of virtual formats returned are sorted in preference order and the first valid + * format will be used. All bits-per-sample options are listed because we try to match + * this to the default audio device. See also: set_format() below. + */ + template + virtual_sink_waveformats_t create_virtual_sink_waveformats() { if constexpr (channel_count == 2) { auto channel_mask = waveformat_mask_stereo; - // only choose 24 or 16-bit formats to avoid clobbering existing Dolby/DTS spatial audio settings + // The 32-bit formats are a lower priority for stereo because using one will disable Dolby/DTS + // spatial audio mode if the user enabled it on the Steam speaker. return { create_waveformat(sample_format_e::s24in32, channel_count, channel_mask), create_waveformat(sample_format_e::s24, channel_count, channel_mask), create_waveformat(sample_format_e::s16, channel_count, channel_mask), + create_waveformat(sample_format_e::f32, channel_count, channel_mask), + create_waveformat(sample_format_e::s32, channel_count, channel_mask), }; - } - else if (channel_count == 6) { + } else if (channel_count == 6) { auto channel_mask1 = waveformat_mask_surround51_with_backspeakers; auto channel_mask2 = waveformat_mask_surround51_with_sidespeakers; return { @@ -146,8 +153,7 @@ namespace { create_waveformat(sample_format_e::s16, channel_count, channel_mask1), create_waveformat(sample_format_e::s16, channel_count, channel_mask2), }; - } - else if (channel_count == 8) { + } else if (channel_count == 8) { auto channel_mask = waveformat_mask_surround71; return { create_waveformat(sample_format_e::f32, channel_count, channel_mask), @@ -159,8 +165,7 @@ namespace { } } - std::string - waveformat_to_pretty_string(const WAVEFORMATEXTENSIBLE &waveformat) { + std::string waveformat_to_pretty_string(const WAVEFORMATEXTENSIBLE &waveformat) { std::string result = waveformat.SubFormat == KSDATAFORMAT_SUBTYPE_IEEE_FLOAT ? "F" : waveformat.SubFormat == KSDATAFORMAT_SUBTYPE_PCM ? "S" : "UNKNOWN"; @@ -196,16 +201,15 @@ namespace { } // namespace using namespace std::literals; + namespace platf::audio { - template - void - Release(T *p) { + template + void Release(T *p) { p->Release(); } - template - void - co_task_free(T *p) { + template + void co_task_free(T *p) { CoTaskMemFree((LPVOID) p); } @@ -272,14 +276,14 @@ namespace platf::audio { }, }; - audio_client_t - make_audio_client(device_t &device, const format_t &format) { + audio_client_t make_audio_client(device_t &device, const format_t &format) { audio_client_t audio_client; auto status = device->Activate( IID_IAudioClient, CLSCTX_ALL, nullptr, - (void **) &audio_client); + (void **) &audio_client + ); if (FAILED(status)) { BOOST_LOG(error) << "Couldn't activate Device: [0x"sv << util::hex(status).to_string_view() << ']'; @@ -305,34 +309,40 @@ namespace platf::audio { auto waveformatext_pointer = reinterpret_cast(mixer_waveformat.get()); capture_waveformat.dwChannelMask = waveformatext_pointer->dwChannelMask; } + + BOOST_LOG(info) << "Audio mixer format is "sv << mixer_waveformat->wBitsPerSample << "-bit, "sv + << mixer_waveformat->nSamplesPerSec << " Hz, "sv + << ((mixer_waveformat->nSamplesPerSec != 48000) ? "will be resampled to 48000 by Windows"sv : "no resampling needed"sv); } status = audio_client->Initialize( AUDCLNT_SHAREMODE_SHARED, AUDCLNT_STREAMFLAGS_LOOPBACK | AUDCLNT_STREAMFLAGS_EVENTCALLBACK | AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM | AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY, // Enable automatic resampling to 48 KHz - 0, 0, + 0, + 0, (LPWAVEFORMATEX) &capture_waveformat, - nullptr); + nullptr + ); if (status) { BOOST_LOG(error) << "Couldn't initialize audio client for ["sv << format.name << "]: [0x"sv << util::hex(status).to_string_view() << ']'; return nullptr; } - BOOST_LOG(info) << "Audio capture format is " << logging::bracket(waveformat_to_pretty_string(capture_waveformat)); + BOOST_LOG(info) << "Audio capture format is "sv << logging::bracket(waveformat_to_pretty_string(capture_waveformat)); return audio_client; } - device_t - default_device(device_enum_t &device_enum) { + device_t default_device(device_enum_t &device_enum) { device_t device; HRESULT status; status = device_enum->GetDefaultAudioEndpoint( eRender, eConsole, - &device); + &device + ); if (FAILED(status)) { BOOST_LOG(error) << "Couldn't get default audio endpoint [0x"sv << util::hex(status).to_string_view() << ']'; @@ -345,68 +355,68 @@ namespace platf::audio { class audio_notification_t: public ::IMMNotificationClient { public: - audio_notification_t() {} + audio_notification_t() { + } // IUnknown implementation (unused by IMMDeviceEnumerator) - ULONG STDMETHODCALLTYPE - AddRef() { + ULONG STDMETHODCALLTYPE AddRef() { return 1; } - ULONG STDMETHODCALLTYPE - Release() { + ULONG STDMETHODCALLTYPE Release() { return 1; } - HRESULT STDMETHODCALLTYPE - QueryInterface(REFIID riid, VOID **ppvInterface) { + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, VOID **ppvInterface) { if (IID_IUnknown == riid) { AddRef(); *ppvInterface = (IUnknown *) this; return S_OK; - } - else if (__uuidof(IMMNotificationClient) == riid) { + } else if (__uuidof(IMMNotificationClient) == riid) { AddRef(); *ppvInterface = (IMMNotificationClient *) this; return S_OK; - } - else { + } else { *ppvInterface = NULL; return E_NOINTERFACE; } } // IMMNotificationClient - HRESULT STDMETHODCALLTYPE - OnDefaultDeviceChanged(EDataFlow flow, ERole role, LPCWSTR pwstrDeviceId) { + HRESULT STDMETHODCALLTYPE OnDefaultDeviceChanged(EDataFlow flow, ERole role, LPCWSTR pwstrDeviceId) { if (flow == eRender) { default_render_device_changed_flag.store(true); } return S_OK; } - HRESULT STDMETHODCALLTYPE - OnDeviceAdded(LPCWSTR pwstrDeviceId) { return S_OK; } + HRESULT STDMETHODCALLTYPE OnDeviceAdded(LPCWSTR pwstrDeviceId) { + return S_OK; + } - HRESULT STDMETHODCALLTYPE - OnDeviceRemoved(LPCWSTR pwstrDeviceId) { return S_OK; } + HRESULT STDMETHODCALLTYPE OnDeviceRemoved(LPCWSTR pwstrDeviceId) { + return S_OK; + } - HRESULT STDMETHODCALLTYPE - OnDeviceStateChanged( + HRESULT STDMETHODCALLTYPE OnDeviceStateChanged( LPCWSTR pwstrDeviceId, - DWORD dwNewState) { return S_OK; } + DWORD dwNewState + ) { + return S_OK; + } - HRESULT STDMETHODCALLTYPE - OnPropertyValueChanged( + HRESULT STDMETHODCALLTYPE OnPropertyValueChanged( LPCWSTR pwstrDeviceId, - const PROPERTYKEY key) { return S_OK; } + const PROPERTYKEY key + ) { + return S_OK; + } /** * @brief Checks if the default rendering device changed and resets the change flag * @return `true` if the device changed since last call */ - bool - check_default_render_device_changed() { + bool check_default_render_device_changed() { return default_render_device_changed_flag.exchange(false); } @@ -416,8 +426,7 @@ namespace platf::audio { class mic_wasapi_t: public mic_t { public: - capture_e - sample(std::vector &sample_out) override { + capture_e sample(std::vector &sample_out) override { auto sample_size = sample_out.size(); // Refill the sample buffer if needed @@ -438,8 +447,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) { audio_event.reset(CreateEventA(nullptr, FALSE, FALSE, nullptr)); if (!audio_event) { BOOST_LOG(error) << "Couldn't create Event handle"sv; @@ -454,7 +462,8 @@ namespace platf::audio { nullptr, CLSCTX_ALL, IID_IMMDeviceEnumerator, - (void **) &device_enum); + (void **) &device_enum + ); if (FAILED(status)) { BOOST_LOG(error) << "Couldn't create Device Enumerator [0x"sv << util::hex(status).to_string_view() << ']'; @@ -509,7 +518,7 @@ namespace platf::audio { } // *2 --> needs to fit double - sample_buf = util::buffer_t { std::max(frames, frame_size) * 2 * channels_out }; + sample_buf = util::buffer_t {std::max(frames, frame_size) * 2 * channels_out}; sample_buf_pos = std::begin(sample_buf); status = audio_client->GetService(IID_IAudioCaptureClient, (void **) &audio_capture); @@ -559,8 +568,7 @@ namespace platf::audio { } private: - capture_e - _fill_buffer() { + capture_e _fill_buffer() { HRESULT status; // Total number of samples @@ -600,13 +608,16 @@ namespace platf::audio { for ( status = audio_capture->GetNextPacketSize(&packet_size); SUCCEEDED(status) && packet_size > 0; - status = audio_capture->GetNextPacketSize(&packet_size)) { + status = audio_capture->GetNextPacketSize(&packet_size) + ) { DWORD buffer_flags; status = audio_capture->GetBuffer( (BYTE **) &sample_aligned.samples, &block_aligned.audio_sample_size, &buffer_flags, - nullptr, nullptr); + nullptr, + nullptr + ); switch (status) { case S_OK: @@ -631,8 +642,7 @@ namespace platf::audio { if (buffer_flags & AUDCLNT_BUFFERFLAGS_SILENT) { std::fill_n(sample_buf_pos, n, 0); - } - else { + } else { std::copy_n(sample_aligned.samples, n, sample_buf_pos); } @@ -674,8 +684,7 @@ namespace platf::audio { class audio_control_t: public ::platf::audio_control_t { public: - std::optional - sink_info() override { + std::optional sink_info() override { sink_t sink; // Fill host sink name with the device_id of the current default audio device. @@ -697,8 +706,7 @@ namespace platf::audio { match_fields_list_t match_list; if (config::audio.virtual_sink.empty()) { match_list = match_steam_speakers(); - } - else { + } else { match_list = match_all_fields(from_utf8(config::audio.virtual_sink)); } @@ -714,22 +722,26 @@ namespace platf::audio { "virtual-"s + formats[1].name + device_id, "virtual-"s + formats[2].name + device_id, }); - } - else if (!config::audio.virtual_sink.empty()) { + } else if (!config::audio.virtual_sink.empty()) { BOOST_LOG(warning) << "Couldn't find the specified virtual audio sink " << config::audio.virtual_sink; } return sink; } + bool is_sink_available(const std::string &sink) override { + const auto match_list = match_all_fields(from_utf8(sink)); + const auto matched = find_device_id(match_list); + return static_cast(matched); + } + /** * @brief Extract virtual audio sink information possibly encoded in the sink name. * @param sink The sink name * @return A pair of device_id and format reference if the sink name matches * our naming scheme for virtual audio sinks, `std::nullopt` otherwise. */ - std::optional>> - extract_virtual_sink_info(const std::string &sink) { + std::optional>> extract_virtual_sink_info(const std::string &sink) { // Encoding format: // [virtual-(format name)]device_id std::string current = sink; @@ -749,8 +761,7 @@ 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) override { auto mic = std::make_unique(); if (mic->init(sample_rate, frame_size, channels)) { @@ -777,8 +788,7 @@ namespace platf::audio { * virtual-(format name) * If it doesn't contain that prefix, then the format will not be changed */ - std::optional - set_format(const std::string &sink) { + std::optional set_format(const std::string &sink) { if (sink.empty()) { return std::nullopt; } @@ -792,13 +802,28 @@ namespace platf::audio { auto matched = find_device_id(match_all_fields(from_utf8(sink))); if (matched) { return matched->second; - } - else { + } else { BOOST_LOG(error) << "Couldn't find audio sink " << sink; return std::nullopt; } } + // When switching to a Steam virtual speaker device, try to retain the bit depth of the + // default audio device. Switching from a 16-bit device to a 24-bit one has been known to + // cause glitches for some users. + int wanted_bits_per_sample = 32; + auto current_default_dev = default_device(device_enum); + if (current_default_dev) { + audio::prop_t prop; + prop_var_t current_device_format; + + if (SUCCEEDED(current_default_dev->OpenPropertyStore(STGM_READ, &prop)) && SUCCEEDED(prop->GetValue(PKEY_AudioEngine_DeviceFormat, ¤t_device_format.prop))) { + auto *format = (WAVEFORMATEXTENSIBLE *) current_device_format.prop.blob.pBlobData; + wanted_bits_per_sample = format->Samples.wValidBitsPerSample; + BOOST_LOG(info) << "Virtual audio device will use "sv << wanted_bits_per_sample << "-bit to match default device"sv; + } + } + auto &device_id = virtual_sink_info->first; auto &waveformats = virtual_sink_info->second.get().virtual_sink_waveformats; for (const auto &waveformat : waveformats) { @@ -808,6 +833,10 @@ namespace platf::audio { auto waveformat_copy = waveformat; auto waveformat_copy_pointer = reinterpret_cast(&waveformat_copy); + if (wanted_bits_per_sample != waveformat.Samples.wValidBitsPerSample) { + continue; + } + WAVEFORMATEXTENSIBLE p {}; if (SUCCEEDED(policy->SetDeviceFormat(device_id_copy.c_str(), waveformat_copy_pointer, (WAVEFORMATEX *) &p))) { BOOST_LOG(info) << "Changed virtual audio sink format to " << logging::bracket(waveformat_to_pretty_string(waveformat)); @@ -819,8 +848,7 @@ namespace platf::audio { return std::nullopt; } - int - set_sink(const std::string &sink) override { + int set_sink(const std::string &sink) override { auto device_id = set_format(sink); if (!device_id) { return -1; @@ -833,8 +861,7 @@ namespace platf::audio { // Depending on the format of the string, we could get either of these errors if (status == HRESULT_FROM_WIN32(ERROR_NOT_FOUND) || status == E_INVALIDARG) { BOOST_LOG(warning) << "Audio sink not found: "sv << sink; - } - else { + } else { BOOST_LOG(warning) << "Couldn't set ["sv << sink << "] to role ["sv << x << "]: 0x"sv << util::hex(status).to_string_view(); } @@ -861,20 +888,18 @@ namespace platf::audio { using match_fields_list_t = std::vector>; using matched_field_t = std::pair; - audio_control_t::match_fields_list_t - match_steam_speakers() { + audio_control_t::match_fields_list_t match_steam_speakers() { return { - { match_field_e::adapter_friendly_name, L"Steam Streaming Speakers" } + {match_field_e::adapter_friendly_name, L"Steam Streaming Speakers"} }; } - audio_control_t::match_fields_list_t - match_all_fields(const std::wstring &name) { + audio_control_t::match_fields_list_t match_all_fields(const std::wstring &name) { return { - { match_field_e::device_id, name }, // {0.0.0.00000000}.{29dd7668-45b2-4846-882d-950f55bf7eb8} - { match_field_e::device_friendly_name, name }, // Digital Audio (S/PDIF) (High Definition Audio Device) - { match_field_e::device_description, name }, // Digital Audio (S/PDIF) - { match_field_e::adapter_friendly_name, name }, // High Definition Audio Device + {match_field_e::device_id, name}, // {0.0.0.00000000}.{29dd7668-45b2-4846-882d-950f55bf7eb8} + {match_field_e::device_friendly_name, name}, // Digital Audio (S/PDIF) (High Definition Audio Device) + {match_field_e::device_description, name}, // Digital Audio (S/PDIF) + {match_field_e::adapter_friendly_name, name}, // High Definition Audio Device }; } @@ -883,8 +908,7 @@ namespace platf::audio { * @param match_list Pairs of match fields and values * @return Optional pair of matched field and device_id */ - std::optional - find_device_id(const match_fields_list_t &match_list) { + std::optional find_device_id(const match_fields_list_t &match_list) { if (match_list.empty()) { return std::nullopt; } @@ -958,8 +982,7 @@ namespace platf::audio { /** * @brief Resets the default audio device from Steam Streaming Speakers. */ - void - reset_default_device() { + void reset_default_device() { auto matched_steam = find_device_id(match_steam_speakers()); if (!matched_steam) { return; @@ -1020,8 +1043,7 @@ namespace platf::audio { * @brief Installs the Steam Streaming Speakers driver, if present. * @return `true` if installation was successful. */ - bool - install_steam_audio_drivers() { + bool install_steam_audio_drivers() { #ifdef STEAM_DRIVER_SUBDIR // MinGW's libnewdev.a is missing DiInstallDriverW() even though the headers have it, // so we have to load it at runtime. It's Vista or later, so it will always be available. @@ -1065,8 +1087,7 @@ namespace platf::audio { } return true; - } - else { + } else { auto err = GetLastError(); switch (err) { case ERROR_ACCESS_DENIED: @@ -1089,14 +1110,14 @@ namespace platf::audio { #endif } - int - init() { + int init() { auto status = CoCreateInstance( CLSID_CPolicyConfigClient, nullptr, CLSCTX_ALL, IID_IPolicyConfig, - (void **) &policy); + (void **) &policy + ); if (FAILED(status)) { BOOST_LOG(error) << "Couldn't create audio policy config: [0x"sv << util::hex(status).to_string_view() << ']'; @@ -1109,7 +1130,8 @@ namespace platf::audio { nullptr, CLSCTX_ALL, IID_IMMDeviceEnumerator, - (void **) &device_enum); + (void **) &device_enum + ); if (FAILED(status)) { BOOST_LOG(error) << "Couldn't create Device Enumerator: [0x"sv << util::hex(status).to_string_view() << ']'; @@ -1119,7 +1141,8 @@ namespace platf::audio { return 0; } - ~audio_control_t() override {} + ~audio_control_t() override { + } policy_t policy; audio::device_enum_t device_enum; @@ -1131,12 +1154,10 @@ namespace platf { // It's not big enough to justify it's own source file :/ namespace dxgi { - int - init(); + int init(); } - std::unique_ptr - audio_control() { + std::unique_ptr audio_control() { auto control = std::make_unique(); if (control->init()) { @@ -1153,8 +1174,7 @@ namespace platf { return control; } - std::unique_ptr - init() { + std::unique_ptr init() { if (dxgi::init()) { return nullptr; } diff --git a/src/platform/windows/display.h b/src/platform/windows/display.h index 3e035490394..74c4be7c22c 100644 --- a/src/platform/windows/display.h +++ b/src/platform/windows/display.h @@ -4,16 +4,17 @@ */ #pragma once +// platform includes #include #include #include #include #include #include - #include #include +// local includes #include "src/platform/common.h" #include "src/utility.h" #include "src/video.h" @@ -23,11 +24,10 @@ namespace platf::dxgi { // Add D3D11_CREATE_DEVICE_DEBUG here to enable the D3D11 debug runtime. // You should have a debugger like WinDbg attached to receive debug messages. - auto constexpr D3D11_CREATE_DEVICE_FLAGS = D3D11_CREATE_DEVICE_VIDEO_SUPPORT; + auto constexpr D3D11_CREATE_DEVICE_FLAGS = 0; - template - void - Release(T *dxgi) { + template + void Release(T *dxgi) { dxgi->Release(); } @@ -72,6 +72,7 @@ namespace platf::dxgi { } // namespace video class hwdevice_t; + struct cursor_t { std::vector img_data; @@ -83,10 +84,9 @@ namespace platf::dxgi { class gpu_cursor_t { public: gpu_cursor_t(): - cursor_view { 0, 0, 0, 0, 0.0f, 1.0f } {}; + cursor_view {0, 0, 0, 0, 0.0f, 1.0f} {}; - void - set_pos(LONG topleft_x, LONG topleft_y, LONG display_width, LONG display_height, DXGI_MODE_ROTATION display_rotation, bool visible) { + void set_pos(LONG topleft_x, LONG topleft_y, LONG display_width, LONG display_height, DXGI_MODE_ROTATION display_rotation, bool visible) { this->topleft_x = topleft_x; this->topleft_y = topleft_y; this->display_width = display_width; @@ -96,16 +96,14 @@ namespace platf::dxgi { update_viewport(); } - void - set_texture(LONG texture_width, LONG texture_height, texture2d_t &&texture) { + void set_texture(LONG texture_width, LONG texture_height, texture2d_t &&texture) { this->texture = std::move(texture); this->texture_width = texture_width; this->texture_height = texture_height; update_viewport(); } - void - update_viewport() { + void update_viewport() { switch (display_rotation) { case DXGI_MODE_ROTATION_UNSPECIFIED: case DXGI_MODE_ROTATION_IDENTITY: @@ -158,11 +156,9 @@ namespace platf::dxgi { class display_base_t: public display_t { public: - int - init(const ::video::config_t &config, const std::string &display_name); + int init(const ::video::config_t &config, const std::string &display_name); - capture_e - capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override; + capture_e capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override; factory1_t factory; adapter_t adapter; @@ -209,6 +205,7 @@ namespace platf::dxgi { UINT IndependentVidPnVSyncControl : 1; UINT Reserved : 28; }; + UINT Value; }; } D3DKMT_WDDM_2_7_CAPS; @@ -231,30 +228,21 @@ namespace platf::dxgi { typedef NTSTATUS(WINAPI *PD3DKMTQueryAdapterInfo)(D3DKMT_QUERYADAPTERINFO *); typedef NTSTATUS(WINAPI *PD3DKMTCloseAdapter)(D3DKMT_CLOSEADAPTER *); - virtual bool - is_hdr() override; - virtual bool - get_hdr_metadata(SS_HDR_METADATA &metadata) override; + virtual bool is_hdr() override; + virtual bool get_hdr_metadata(SS_HDR_METADATA &metadata) override; - const char * - dxgi_format_to_string(DXGI_FORMAT format); - const char * - colorspace_to_string(DXGI_COLOR_SPACE_TYPE type); - virtual std::vector - get_supported_capture_formats() = 0; + const char *dxgi_format_to_string(DXGI_FORMAT format); + const char *colorspace_to_string(DXGI_COLOR_SPACE_TYPE type); + virtual std::vector get_supported_capture_formats() = 0; protected: - int - get_pixel_pitch() { + int get_pixel_pitch() { return (capture_format == DXGI_FORMAT_R16G16B16A16_FLOAT) ? 8 : 4; } - virtual capture_e - snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor_visible) = 0; - virtual capture_e - release_snapshot() = 0; - virtual int - complete_img(img_t *img, bool dummy) = 0; + virtual capture_e snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor_visible) = 0; + virtual capture_e release_snapshot() = 0; + virtual int complete_img(img_t *img, bool dummy) = 0; }; /** @@ -262,17 +250,12 @@ namespace platf::dxgi { */ class display_ram_t: public display_base_t { public: - std::shared_ptr - alloc_img() override; - int - dummy_img(img_t *img) override; - int - complete_img(img_t *img, bool dummy) override; - std::vector - get_supported_capture_formats() override; - - std::unique_ptr - make_avcodec_encode_device(pix_fmt_e pix_fmt) override; + std::shared_ptr alloc_img() override; + int dummy_img(img_t *img) override; + int complete_img(img_t *img, bool dummy) override; + std::vector get_supported_capture_formats() override; + + std::unique_ptr make_avcodec_encode_device(pix_fmt_e pix_fmt) override; D3D11_MAPPED_SUBRESOURCE img_info; texture2d_t texture; @@ -283,23 +266,16 @@ namespace platf::dxgi { */ class display_vram_t: public display_base_t, public std::enable_shared_from_this { public: - std::shared_ptr - alloc_img() override; - int - dummy_img(img_t *img_base) override; - int - complete_img(img_t *img_base, bool dummy) override; - std::vector - get_supported_capture_formats() override; + std::shared_ptr alloc_img() override; + int dummy_img(img_t *img_base) override; + int complete_img(img_t *img_base, bool dummy) override; + std::vector get_supported_capture_formats() override; - bool - is_codec_supported(std::string_view name, const ::video::config_t &config) override; + bool is_codec_supported(std::string_view name, const ::video::config_t &config) override; - std::unique_ptr - make_avcodec_encode_device(pix_fmt_e pix_fmt) override; + std::unique_ptr make_avcodec_encode_device(pix_fmt_e pix_fmt) override; - std::unique_ptr - make_nvenc_encode_device(pix_fmt_e pix_fmt) override; + std::unique_ptr make_nvenc_encode_device(pix_fmt_e pix_fmt) override; std::atomic next_image_id; }; @@ -313,14 +289,10 @@ namespace platf::dxgi { bool has_frame {}; std::chrono::steady_clock::time_point last_protected_content_warning_time {}; - int - init(display_base_t *display, const ::video::config_t &config); - capture_e - next_frame(DXGI_OUTDUPL_FRAME_INFO &frame_info, std::chrono::milliseconds timeout, resource_t::pointer *res_p); - capture_e - reset(dup_t::pointer dup_p = dup_t::pointer()); - capture_e - release_frame(); + int init(display_base_t *display, const ::video::config_t &config); + capture_e next_frame(DXGI_OUTDUPL_FRAME_INFO &frame_info, std::chrono::milliseconds timeout, resource_t::pointer *res_p); + capture_e reset(dup_t::pointer dup_p = dup_t::pointer()); + capture_e release_frame(); ~duplication_t(); }; @@ -330,12 +302,9 @@ namespace platf::dxgi { */ class display_ddup_ram_t: public display_ram_t { public: - int - init(const ::video::config_t &config, const std::string &display_name); - capture_e - snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor_visible) override; - capture_e - release_snapshot() override; + int init(const ::video::config_t &config, const std::string &display_name); + capture_e snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor_visible) override; + capture_e release_snapshot() override; duplication_t dup; cursor_t cursor; @@ -346,12 +315,9 @@ namespace platf::dxgi { */ class display_ddup_vram_t: public display_vram_t { public: - int - init(const ::video::config_t &config, const std::string &display_name); - capture_e - snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor_visible) override; - capture_e - release_snapshot() override; + int init(const ::video::config_t &config, const std::string &display_name); + capture_e snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor_visible) override; + capture_e release_snapshot() override; duplication_t dup; sampler_state_t sampler_linear; @@ -375,29 +341,24 @@ namespace platf::dxgi { * Display duplicator that uses the Windows.Graphics.Capture API. */ class wgc_capture_t { - winrt::Windows::Graphics::DirectX::Direct3D11::IDirect3DDevice uwp_device { nullptr }; - winrt::Windows::Graphics::Capture::GraphicsCaptureItem item { nullptr }; - winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool frame_pool { nullptr }; - winrt::Windows::Graphics::Capture::GraphicsCaptureSession capture_session { nullptr }; - winrt::Windows::Graphics::Capture::Direct3D11CaptureFrame produced_frame { nullptr }, consumed_frame { nullptr }; + winrt::Windows::Graphics::DirectX::Direct3D11::IDirect3DDevice uwp_device {nullptr}; + winrt::Windows::Graphics::Capture::GraphicsCaptureItem item {nullptr}; + winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool frame_pool {nullptr}; + winrt::Windows::Graphics::Capture::GraphicsCaptureSession capture_session {nullptr}; + winrt::Windows::Graphics::Capture::Direct3D11CaptureFrame produced_frame {nullptr}, consumed_frame {nullptr}; SRWLOCK frame_lock = SRWLOCK_INIT; CONDITION_VARIABLE frame_present_cv; - void - on_frame_arrived(winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool const &sender, winrt::Windows::Foundation::IInspectable const &); + void on_frame_arrived(winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool const &sender, winrt::Windows::Foundation::IInspectable const &); public: wgc_capture_t(); ~wgc_capture_t(); - int - init(display_base_t *display, const ::video::config_t &config); - capture_e - next_frame(std::chrono::milliseconds timeout, ID3D11Texture2D **out, uint64_t &out_time); - capture_e - release_frame(); - int - set_cursor_visible(bool); + int init(display_base_t *display, const ::video::config_t &config); + capture_e next_frame(std::chrono::milliseconds timeout, ID3D11Texture2D **out, uint64_t &out_time); + capture_e release_frame(); + int set_cursor_visible(bool); }; /** @@ -407,12 +368,9 @@ namespace platf::dxgi { wgc_capture_t dup; public: - int - init(const ::video::config_t &config, const std::string &display_name); - capture_e - snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor_visible) override; - capture_e - release_snapshot() override; + int init(const ::video::config_t &config, const std::string &display_name); + capture_e snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor_visible) override; + capture_e release_snapshot() override; }; /** @@ -422,11 +380,8 @@ namespace platf::dxgi { wgc_capture_t dup; public: - int - init(const ::video::config_t &config, const std::string &display_name); - capture_e - snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor_visible) override; - capture_e - release_snapshot() override; + int init(const ::video::config_t &config, const std::string &display_name); + capture_e snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor_visible) override; + capture_e release_snapshot() override; }; } // namespace platf::dxgi diff --git a/src/platform/windows/display_base.cpp b/src/platform/windows/display_base.cpp index c6167886925..6698566207f 100644 --- a/src/platform/windows/display_base.cpp +++ b/src/platform/windows/display_base.cpp @@ -2,20 +2,36 @@ * @file src/platform/windows/display_base.cpp * @brief Definitions for the Windows display base code. */ +// standard includes #include -#include #include +// platform includes +#include + +// lib includes #include #include +#include // We have to include boost/process/v1.hpp before display.h due to WinSock.h, // but that prevents the definition of NTSTATUS so we must define it ourself. typedef long NTSTATUS; +// Definition from the WDK's d3dkmthk.h +typedef enum _D3DKMT_GPU_PREFERENCE_QUERY_STATE: DWORD { + D3DKMT_GPU_PREFERENCE_STATE_UNINITIALIZED, ///< The GPU preference isn't initialized. + D3DKMT_GPU_PREFERENCE_STATE_HIGH_PERFORMANCE, ///< The highest performing GPU is preferred. + D3DKMT_GPU_PREFERENCE_STATE_MINIMUM_POWER, ///< The minimum-powered GPU is preferred. + D3DKMT_GPU_PREFERENCE_STATE_UNSPECIFIED, ///< A GPU preference isn't specified. + D3DKMT_GPU_PREFERENCE_STATE_NOT_FOUND, ///< A GPU preference isn't found. + D3DKMT_GPU_PREFERENCE_STATE_USER_SPECIFIED_GPU ///< A specific GPU is preferred. +} D3DKMT_GPU_PREFERENCE_QUERY_STATE; + #include "display.h" #include "misc.h" #include "src/config.h" +#include "src/display_device.h" #include "src/logging.h" #include "src/platform/common.h" #include "src/video.h" @@ -23,14 +39,14 @@ typedef long NTSTATUS; namespace platf { using namespace std::literals; } + namespace platf::dxgi { namespace bp = boost::process; /** * DDAPI-specific initialization goes here. */ - int - duplication_t::init(display_base_t *display, const ::video::config_t &config) { + int duplication_t::init(display_base_t *display, const ::video::config_t &config) { HRESULT status; // Capture format will be determined from the first call to AcquireNextFrame() @@ -68,8 +84,7 @@ namespace platf::dxgi { BOOST_LOG(warning) << "DuplicateOutput1 Failed [0x"sv << util::hex(status).to_string_view() << ']'; return -1; } - } - else { + } else { BOOST_LOG(warning) << "IDXGIOutput5 is not supported by your OS. Capture performance may be reduced."sv; dxgi::output1_t output1 {}; @@ -111,8 +126,7 @@ namespace platf::dxgi { return 0; } - capture_e - duplication_t::next_frame(DXGI_OUTDUPL_FRAME_INFO &frame_info, std::chrono::milliseconds timeout, resource_t::pointer *res_p) { + capture_e duplication_t::next_frame(DXGI_OUTDUPL_FRAME_INFO &frame_info, std::chrono::milliseconds timeout, resource_t::pointer *res_p) { auto capture_status = release_frame(); if (capture_status != capture_e::ok) { return capture_status; @@ -144,8 +158,7 @@ namespace platf::dxgi { } } - capture_e - duplication_t::reset(dup_t::pointer dup_p) { + capture_e duplication_t::reset(dup_t::pointer dup_p) { auto capture_status = release_frame(); dup.reset(dup_p); @@ -153,8 +166,7 @@ namespace platf::dxgi { return capture_status; } - capture_e - duplication_t::release_frame() { + capture_e duplication_t::release_frame() { if (!has_frame) { return capture_e::ok; } @@ -182,16 +194,14 @@ namespace platf::dxgi { release_frame(); } - 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) { + 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 { // Adjust capture frame interval when display refresh rate is not integral but very close to requested fps. if (display_refresh_rate.Denominator > 1) { DXGI_RATIONAL candidate = display_refresh_rate; if (client_frame_rate % display_refresh_rate_rounded == 0) { candidate.Numerator *= client_frame_rate / display_refresh_rate_rounded; - } - else if (display_refresh_rate_rounded % client_frame_rate == 0) { + } else if (display_refresh_rate_rounded % client_frame_rate == 0) { candidate.Denominator *= display_refresh_rate_rounded / client_frame_rate; } double candidate_rate = (double) candidate.Numerator / candidate.Denominator; @@ -202,7 +212,7 @@ namespace platf::dxgi { } } - return { (uint32_t) client_frame_rate, 1 }; + return {(uint32_t) client_frame_rate, 1}; }; DXGI_RATIONAL client_frame_rate_adjusted = adjust_client_frame_rate(); @@ -245,8 +255,7 @@ namespace platf::dxgi { frame_pacing_group_start = std::nullopt; frame_pacing_group_frames = 0; status = capture_e::timeout; - } - else { + } else { timer->sleep_for(sleep_period); sleep_overshoot_logger.first_point(sleep_target); sleep_overshoot_logger.second_point_now_and_log(); @@ -255,8 +264,7 @@ namespace platf::dxgi { if (status == capture_e::ok && img_out) { frame_pacing_group_frames += 1; - } - else { + } else { frame_pacing_group_start = std::nullopt; frame_pacing_group_frames = 0; } @@ -276,8 +284,7 @@ namespace platf::dxgi { } frame_pacing_group_frames = 1; - } - else if (status == platf::capture_e::timeout) { + } else if (status == platf::capture_e::timeout) { // The D3D11 device is protected by an unfair lock that is held the entire time that // IDXGIOutputDuplication::AcquireNextFrame() is running. This is normally harmless, // however sometimes the encoding thread needs to interact with our ID3D11Device to @@ -328,111 +335,6 @@ namespace platf::dxgi { return capture_e::ok; } - bool - set_gpu_preference_on_self(int preference) { - // The GPU preferences key uses app path as the value name. - WCHAR sunshine_path[MAX_PATH]; - GetModuleFileNameW(NULL, sunshine_path, ARRAYSIZE(sunshine_path)); - - WCHAR value_data[128]; - swprintf_s(value_data, L"GpuPreference=%d;", preference); - - auto status = RegSetKeyValueW(HKEY_CURRENT_USER, - L"Software\\Microsoft\\DirectX\\UserGpuPreferences", - sunshine_path, - REG_SZ, - value_data, - (wcslen(value_data) + 1) * sizeof(WCHAR)); - if (status != ERROR_SUCCESS) { - BOOST_LOG(error) << "Failed to set GPU preference: "sv << status; - return false; - } - - BOOST_LOG(info) << "Set GPU preference: "sv << preference; - return true; - } - - bool - validate_and_test_gpu_preference(const std::string &display_name, bool verify_frame_capture) { - std::string cmd = "tools\\ddprobe.exe"; - - // We start at 1 because 0 is automatic selection which can be overridden by - // the GPU driver control panel options. Since ddprobe.exe can have different - // GPU driver overrides than Sunshine.exe, we want to avoid a scenario where - // autoselection might work for ddprobe.exe but not for us. - for (int i = 1; i < 5; i++) { - // Run the probe tool. It returns the status of DuplicateOutput(). - // - // Arg format: [GPU preference] [Display name] [--verify-frame-capture] - HRESULT result; - std::vector args = { std::to_string(i), display_name }; - try { - if (verify_frame_capture) { - args.emplace_back("--verify-frame-capture"); - } - result = bp::system(cmd, bp::args(args), bp::std_out > bp::null, bp::std_err > bp::null); - } - catch (bp::process_error &e) { - BOOST_LOG(error) << "Failed to start ddprobe.exe: "sv << e.what(); - return false; - } - - BOOST_LOG(info) << "ddprobe.exe " << boost::algorithm::join(args, " ") << " returned 0x" - << util::hex(result).to_string_view(); - - // E_ACCESSDENIED can happen at the login screen. If we get this error, - // we know capture would have been supported, because DXGI_ERROR_UNSUPPORTED - // would have been raised first if it wasn't. - if (result == S_OK || result == E_ACCESSDENIED) { - // We found a working GPU preference, so set ourselves to use that. - if (set_gpu_preference_on_self(i)) { - return true; - } - else { - return false; - } - } - } - - // If no valid configuration was found, return false - return false; - } - - // On hybrid graphics systems, Windows will change the order of GPUs reported by - // DXGI in accordance with the user's GPU preference. If the selected GPU is a - // render-only device with no displays, DXGI will add virtual outputs to the - // that device to avoid confusing applications. While this works properly for most - // applications, it breaks the Desktop Duplication API because DXGI doesn't proxy - // the virtual DXGIOutput to the real GPU it is attached to. When trying to call - // DuplicateOutput() on one of these virtual outputs, it fails with DXGI_ERROR_UNSUPPORTED - // (even if you try sneaky stuff like passing the ID3D11Device for the iGPU and the - // virtual DXGIOutput from the dGPU). Because the GPU preference is once-per-process, - // we spawn a helper tool to probe for us before we set our own GPU preference. - bool - probe_for_gpu_preference(const std::string &display_name) { - static bool set_gpu_preference = false; - - // If we've already been through here, there's nothing to do this time. - if (set_gpu_preference) { - return true; - } - - // Try probing with different GPU preferences and verify_frame_capture flag - if (validate_and_test_gpu_preference(display_name, true)) { - set_gpu_preference = true; - return true; - } - - // If no valid configuration was found, try again with verify_frame_capture == false - if (validate_and_test_gpu_preference(display_name, false)) { - set_gpu_preference = true; - return true; - } - - // If neither worked, return false - return false; - } - /** * @brief Tests to determine if the Desktop Duplication API can capture the given output. * @details When testing for enumeration only, we avoid resyncing the thread desktop. @@ -440,8 +342,7 @@ namespace platf::dxgi { * @param output The DXGI output to capture. * @param enumeration_only Specifies whether this test is occurring for display enumeration. */ - bool - test_dxgi_duplication(adapter_t &adapter, output_t &output, bool enumeration_only) { + bool test_dxgi_duplication(adapter_t &adapter, output_t &output, bool enumeration_only) { D3D_FEATURE_LEVEL featureLevels[] { D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0, @@ -458,11 +359,13 @@ namespace platf::dxgi { D3D_DRIVER_TYPE_UNKNOWN, nullptr, D3D11_CREATE_DEVICE_FLAGS, - featureLevels, sizeof(featureLevels) / sizeof(D3D_FEATURE_LEVEL), + featureLevels, + sizeof(featureLevels) / sizeof(D3D_FEATURE_LEVEL), D3D11_SDK_VERSION, &device, nullptr, - nullptr); + nullptr + ); if (FAILED(status)) { BOOST_LOG(error) << "Failed to create D3D11 device for DD test [0x"sv << util::hex(status).to_string_view() << ']'; return false; @@ -495,8 +398,7 @@ namespace platf::dxgi { // capture the current desktop, just bail immediately. Retrying won't help. if (enumeration_only && status == E_ACCESSDENIED) { break; - } - else { + } else { std::this_thread::sleep_for(200ms); } } @@ -505,8 +407,26 @@ namespace platf::dxgi { return false; } - int - display_base_t::init(const ::video::config_t &config, const std::string &display_name) { + /** + * @brief Hook for NtGdiDdDDIGetCachedHybridQueryValue() from win32u.dll. + * @param gpuPreference A pointer to the location where the preference will be written. + * @return Always STATUS_SUCCESS if valid arguments are provided. + */ + NTSTATUS __stdcall NtGdiDdDDIGetCachedHybridQueryValueHook(D3DKMT_GPU_PREFERENCE_QUERY_STATE *gpuPreference) { + // By faking a cached GPU preference state of D3DKMT_GPU_PREFERENCE_STATE_UNSPECIFIED, this will + // prevent DXGI from performing the normal GPU preference resolution that looks at the registry, + // power settings, and the hybrid adapter DDI interface to pick a GPU. Instead, we will not be + // bound to any specific GPU. This will prevent DXGI from performing output reparenting (moving + // outputs from their true location to the render GPU), which breaks DDA. + if (gpuPreference) { + *gpuPreference = D3DKMT_GPU_PREFERENCE_STATE_UNSPECIFIED; + return 0; // STATUS_SUCCESS + } else { + return STATUS_INVALID_PARAMETER; + } + } + + int display_base_t::init(const ::video::config_t &config, const std::string &display_name) { std::once_flag windows_cpp_once_flag; std::call_once(windows_cpp_once_flag, []() { @@ -514,13 +434,22 @@ namespace platf::dxgi { typedef BOOL (*User32_SetProcessDpiAwarenessContext)(DPI_AWARENESS_CONTEXT value); - auto user32 = LoadLibraryA("user32.dll"); - auto f = (User32_SetProcessDpiAwarenessContext) GetProcAddress(user32, "SetProcessDpiAwarenessContext"); - if (f) { - f(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); + { + auto user32 = LoadLibraryA("user32.dll"); + auto f = (User32_SetProcessDpiAwarenessContext) GetProcAddress(user32, "SetProcessDpiAwarenessContext"); + if (f) { + f(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); + } + + FreeLibrary(user32); } - FreeLibrary(user32); + { + // We aren't calling MH_Uninitialize(), but that's okay because this hook lasts for the life of the process + MH_Initialize(); + MH_CreateHookApi(L"win32u.dll", "NtGdiDdDDIGetCachedHybridQueryValue", (void *) NtGdiDdDDIGetCachedHybridQueryValueHook, nullptr); + MH_EnableHook(MH_ALL_HOOKS); + } }); // Get rectangle of full desktop for absolute mouse coordinates @@ -529,11 +458,6 @@ namespace platf::dxgi { HRESULT status; - // We must set the GPU preference before calling any DXGI APIs! - if (!probe_for_gpu_preference(display_name)) { - BOOST_LOG(warning) << "Failed to set GPU preference. Capture may not work!"sv; - } - status = CreateDXGIFactory1(IID_IDXGIFactory1, (void **) &factory); if (FAILED(status)) { BOOST_LOG(error) << "Failed to create DXGIFactory1 [0x"sv << util::hex(status).to_string_view() << ']'; @@ -546,7 +470,7 @@ namespace platf::dxgi { adapter_t::pointer adapter_p; for (int tries = 0; tries < 2; ++tries) { for (int x = 0; factory->EnumAdapters1(x, &adapter_p) != DXGI_ERROR_NOT_FOUND; ++x) { - dxgi::adapter_t adapter_tmp { adapter_p }; + dxgi::adapter_t adapter_tmp {adapter_p}; DXGI_ADAPTER_DESC1 adapter_desc; adapter_tmp->GetDesc1(&adapter_desc); @@ -557,7 +481,7 @@ namespace platf::dxgi { dxgi::output_t::pointer output_p; for (int y = 0; adapter_tmp->EnumOutputs(y, &output_p) != DXGI_ERROR_NOT_FOUND; ++y) { - dxgi::output_t output_tmp { output_p }; + dxgi::output_t output_tmp {output_p}; DXGI_OUTPUT_DESC desc; output_tmp->GetDesc(&desc); @@ -579,8 +503,7 @@ namespace platf::dxgi { display_rotation == DXGI_MODE_ROTATION_ROTATE270) { width_before_rotation = height; height_before_rotation = width; - } - else { + } else { width_before_rotation = width; height_before_rotation = height; } @@ -637,11 +560,13 @@ namespace platf::dxgi { D3D_DRIVER_TYPE_UNKNOWN, nullptr, D3D11_CREATE_DEVICE_FLAGS, - featureLevels, sizeof(featureLevels) / sizeof(D3D_FEATURE_LEVEL), + featureLevels, + sizeof(featureLevels) / sizeof(D3D_FEATURE_LEVEL), D3D11_SDK_VERSION, &device, &feature_level, - &device_ctx); + &device_ctx + ); adapter_p->Release(); @@ -699,7 +624,7 @@ namespace platf::dxgi { return false; } - D3DKMT_OPENADAPTERFROMLUID d3dkmt_adapter = { adapter }; + D3DKMT_OPENADAPTERFROMLUID d3dkmt_adapter = {adapter}; if (FAILED(d3dkmt_open_adapter(&d3dkmt_adapter))) { BOOST_LOG(error) << "D3DKMTOpenAdapterFromLuid() failed while trying to determine GPU HAGS status"; return false; @@ -716,13 +641,12 @@ namespace platf::dxgi { if (SUCCEEDED(d3dkmt_query_adapter_info(&d3dkmt_adapter_info))) { result = d3dkmt_adapter_caps.HwSchEnabled; - } - else { + } else { BOOST_LOG(warning) << "D3DKMTQueryAdapterInfo() failed while trying to determine GPU HAGS status"; result = false; } - D3DKMT_CLOSEADAPTER d3dkmt_close_adapter_wrap = { d3dkmt_adapter.hAdapter }; + D3DKMT_CLOSEADAPTER d3dkmt_close_adapter_wrap = {d3dkmt_adapter.hAdapter}; if (FAILED(d3dkmt_close_adapter(&d3dkmt_close_adapter_wrap))) { BOOST_LOG(error) << "D3DKMTCloseAdapter() failed while trying to determine GPU HAGS status"; } @@ -738,15 +662,16 @@ namespace platf::dxgi { // As of 2023.07, NVIDIA driver has unfixed bug(s) where "realtime" can cause unrecoverable encoding freeze or outright driver crash // This issue happens more frequently with HAGS, in DX12 games or when VRAM is filled close to max capacity // Track OBS to see if they find better workaround or NVIDIA fixes it on their end, they seem to be in communication - if (hags_enabled && !config::video.nv_realtime_hags) priority = D3DKMT_SCHEDULINGPRIORITYCLASS_HIGH; + if (hags_enabled && !config::video.nv_realtime_hags) { + priority = D3DKMT_SCHEDULINGPRIORITYCLASS_HIGH; + } } BOOST_LOG(info) << "Active GPU has HAGS " << (hags_enabled ? "enabled" : "disabled"); BOOST_LOG(info) << "Using " << (priority == D3DKMT_SCHEDULINGPRIORITYCLASS_HIGH ? "high" : "realtime") << " GPU priority"; if (FAILED(d3dkmt_set_process_priority(GetCurrentProcess(), priority))) { BOOST_LOG(warning) << "Failed to adjust GPU priority. Please run application as administrator for optimal performance."; } - } - else { + } else { BOOST_LOG(error) << "Couldn't load D3DKMTSetProcessSchedulingPriorityClass function from gdi32.dll to adjust GPU priority"; } } @@ -807,8 +732,7 @@ namespace platf::dxgi { return 0; } - bool - display_base_t::is_hdr() { + bool display_base_t::is_hdr() { dxgi::output6_t output6 {}; auto status = output->QueryInterface(IID_IDXGIOutput6, (void **) &output6); @@ -823,8 +747,7 @@ namespace platf::dxgi { return desc1.ColorSpace == DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020; } - bool - display_base_t::get_hdr_metadata(SS_HDR_METADATA &metadata) { + bool display_base_t::get_hdr_metadata(SS_HDR_METADATA &metadata) { dxgi::output6_t output6 {}; std::memset(&metadata, 0, sizeof(metadata)); @@ -995,20 +918,31 @@ namespace platf::dxgi { "DXGI_FORMAT_A8P8", "DXGI_FORMAT_B4G4R4A4_UNORM", - NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, "DXGI_FORMAT_P208", "DXGI_FORMAT_V208", "DXGI_FORMAT_V408" }; - const char * - display_base_t::dxgi_format_to_string(DXGI_FORMAT format) { + const char *display_base_t::dxgi_format_to_string(DXGI_FORMAT format) { return format_str[format]; } - const char * - display_base_t::colorspace_to_string(DXGI_COLOR_SPACE_TYPE type) { + const char *display_base_t::colorspace_to_string(DXGI_COLOR_SPACE_TYPE type) { const char *type_str[] = { "DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709", "DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709", @@ -1039,8 +973,7 @@ namespace platf::dxgi { if (type < ARRAYSIZE(type_str)) { return type_str[type]; - } - else { + } else { return "UNKNOWN"; } } @@ -1052,8 +985,7 @@ namespace platf { * Pick a display adapter and capture method. * @param hwdevice_type enables possible use of hardware encoder */ - std::shared_ptr - display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config) { + std::shared_ptr display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config) { if (config::video.capture == "ddx" || config::video.capture.empty()) { if (hwdevice_type == mem_type_e::dxgi) { auto disp = std::make_shared(); @@ -1061,8 +993,7 @@ namespace platf { if (!disp->init(config, display_name)) { return disp; } - } - else if (hwdevice_type == mem_type_e::system) { + } else if (hwdevice_type == mem_type_e::system) { auto disp = std::make_shared(); if (!disp->init(config, display_name)) { @@ -1078,8 +1009,7 @@ namespace platf { if (!disp->init(config, display_name)) { return disp; } - } - else if (hwdevice_type == mem_type_e::system) { + } else if (hwdevice_type == mem_type_e::system) { auto disp = std::make_shared(); if (!disp->init(config, display_name)) { @@ -1092,19 +1022,13 @@ namespace platf { return nullptr; } - std::vector - display_names(mem_type_e) { + std::vector display_names(mem_type_e) { std::vector display_names; HRESULT status; BOOST_LOG(debug) << "Detecting monitors..."sv; - // We must set the GPU preference before calling any DXGI APIs! - if (!dxgi::probe_for_gpu_preference(config::video.output_name)) { - BOOST_LOG(warning) << "Failed to set GPU preference. Capture may not work!"sv; - } - // We sync the thread desktop once before we start the enumeration process // to ensure test_dxgi_duplication() returns consistent results for all GPUs // even if the current desktop changes during our enumeration process. @@ -1138,7 +1062,7 @@ namespace platf { dxgi::output_t::pointer output_p {}; for (int y = 0; adapter->EnumOutputs(y, &output_p) != DXGI_ERROR_NOT_FOUND; ++y) { - dxgi::output_t output { output_p }; + dxgi::output_t output {output_p}; DXGI_OUTPUT_DESC desc; output->GetDesc(&desc); @@ -1168,8 +1092,7 @@ namespace platf { * @brief Returns if GPUs/drivers have changed since the last call to this function. * @return `true` if a change has occurred or if it is unknown whether a change occurred. */ - bool - needs_encoder_reenumeration() { + bool needs_encoder_reenumeration() { // Serialize access to the static DXGI factory static std::mutex reenumeration_state_lock; auto lg = std::lock_guard(reenumeration_state_lock); @@ -1189,8 +1112,7 @@ namespace platf { // can deal with any initialization races that may occur when the system is booting. BOOST_LOG(info) << "Encoder reenumeration is required"sv; return true; - } - else { + } else { // The DXGI factory from last time is still current, so no encoder changes have occurred. return false; } diff --git a/src/platform/windows/display_ram.cpp b/src/platform/windows/display_ram.cpp index 1105c674c30..e8ec540ebca 100644 --- a/src/platform/windows/display_ram.cpp +++ b/src/platform/windows/display_ram.cpp @@ -2,8 +2,8 @@ * @file src/platform/windows/display_ram.cpp * @brief Definitions for handling ram. */ +// local includes #include "display.h" - #include "misc.h" #include "src/logging.h" @@ -19,8 +19,7 @@ namespace platf::dxgi { } }; - void - blend_cursor_monochrome(const cursor_t &cursor, img_t &img) { + void blend_cursor_monochrome(const cursor_t &cursor, img_t &img) { int height = cursor.shape_info.Height / 2; int width = cursor.shape_info.Width; int pitch = cursor.shape_info.Pitch; @@ -82,8 +81,7 @@ namespace platf::dxgi { } } - void - apply_color_alpha(int *img_pixel_p, int cursor_pixel) { + void apply_color_alpha(int *img_pixel_p, int cursor_pixel) { auto colors_out = (std::uint8_t *) &cursor_pixel; auto colors_in = (std::uint8_t *) img_pixel_p; @@ -91,28 +89,24 @@ namespace platf::dxgi { auto alpha = colors_out[3]; if (alpha == 255) { *img_pixel_p = cursor_pixel; - } - else { + } else { colors_in[0] = colors_out[0] + (colors_in[0] * (255 - alpha) + 255 / 2) / 255; colors_in[1] = colors_out[1] + (colors_in[1] * (255 - alpha) + 255 / 2) / 255; colors_in[2] = colors_out[2] + (colors_in[2] * (255 - alpha) + 255 / 2) / 255; } } - void - apply_color_masked(int *img_pixel_p, int cursor_pixel) { + void apply_color_masked(int *img_pixel_p, int cursor_pixel) { // TODO: When use of IDXGIOutput5 is implemented, support different color formats auto alpha = ((std::uint8_t *) &cursor_pixel)[3]; if (alpha == 0xFF) { *img_pixel_p ^= cursor_pixel; - } - else { + } else { *img_pixel_p = cursor_pixel; } } - void - blend_cursor_color(const cursor_t &cursor, img_t &img, const bool masked) { + void blend_cursor_color(const cursor_t &cursor, img_t &img, const bool masked) { int height = cursor.shape_info.Height; int width = cursor.shape_info.Width; int pitch = cursor.shape_info.Pitch; @@ -150,8 +144,7 @@ namespace platf::dxgi { std::for_each(cursor_begin, cursor_end, [&](int cursor_pixel) { if (masked) { apply_color_masked(img_pixel_p, cursor_pixel); - } - else { + } else { apply_color_alpha(img_pixel_p, cursor_pixel); } ++img_pixel_p; @@ -159,8 +152,7 @@ namespace platf::dxgi { } } - void - blend_cursor(const cursor_t &cursor, img_t &img) { + void blend_cursor(const cursor_t &cursor, img_t &img) { switch (cursor.shape_info.Type) { case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_COLOR: blend_cursor_color(cursor, img, false); @@ -176,14 +168,13 @@ namespace platf::dxgi { } } - capture_e - display_ddup_ram_t::snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor_visible) { + capture_e display_ddup_ram_t::snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor_visible) { HRESULT status; DXGI_OUTDUPL_FRAME_INFO frame_info; resource_t::pointer res_p {}; auto capture_status = dup.next_frame(frame_info, timeout, &res_p); - resource_t res { res_p }; + resource_t res {res_p}; if (capture_status != capture_e::ok) { return capture_status; @@ -290,8 +281,7 @@ namespace platf::dxgi { if (dummy_img(img)) { return capture_e::error; } - } - else { + } else { // Map the staging texture for CPU access (making it inaccessible for the GPU) status = device_ctx->Map(texture.get(), 0, D3D11_MAP_READ, 0, &img_info); if (FAILED(status)) { @@ -325,13 +315,11 @@ namespace platf::dxgi { return capture_e::ok; } - capture_e - display_ddup_ram_t::release_snapshot() { + capture_e display_ddup_ram_t::release_snapshot() { return dup.release_frame(); } - std::shared_ptr - display_ram_t::alloc_img() { + std::shared_ptr display_ram_t::alloc_img() { auto img = std::make_shared(); // Initialize fields that are format-independent @@ -341,8 +329,7 @@ namespace platf::dxgi { return img; } - int - display_ram_t::complete_img(platf::img_t *img, bool dummy) { + int display_ram_t::complete_img(platf::img_t *img, bool dummy) { // If this is not a dummy image, we must know the format by now if (!dummy && capture_format == DXGI_FORMAT_UNKNOWN) { BOOST_LOG(error) << "display_ram_t::complete_img() called with unknown capture format!"; @@ -373,8 +360,7 @@ namespace platf::dxgi { /** * @memberof platf::dxgi::display_ram_t */ - int - display_ram_t::dummy_img(platf::img_t *img) { + int display_ram_t::dummy_img(platf::img_t *img) { if (complete_img(img, true)) { return -1; } @@ -383,13 +369,11 @@ namespace platf::dxgi { return 0; } - std::vector - display_ram_t::get_supported_capture_formats() { - return { DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_B8G8R8X8_UNORM }; + std::vector display_ram_t::get_supported_capture_formats() { + return {DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_B8G8R8X8_UNORM}; } - int - display_ddup_ram_t::init(const ::video::config_t &config, const std::string &display_name) { + int display_ddup_ram_t::init(const ::video::config_t &config, const std::string &display_name) { if (display_base_t::init(config, display_name) || dup.init(this, config)) { return -1; } @@ -397,8 +381,7 @@ namespace platf::dxgi { return 0; } - std::unique_ptr - display_ram_t::make_avcodec_encode_device(pix_fmt_e pix_fmt) { + std::unique_ptr display_ram_t::make_avcodec_encode_device(pix_fmt_e pix_fmt) { return std::make_unique(); } diff --git a/src/platform/windows/display_vram.cpp b/src/platform/windows/display_vram.cpp index ba2b0685187..7a8b07058d6 100644 --- a/src/platform/windows/display_vram.cpp +++ b/src/platform/windows/display_vram.cpp @@ -2,8 +2,10 @@ * @file src/platform/windows/display_vram.cpp * @brief Definitions for handling video ram. */ +// standard includes #include +// platform includes #include #include @@ -12,6 +14,11 @@ extern "C" { #include } +// lib includes +#include +#include + +// local includes #include "display.h" #include "misc.h" #include "src/config.h" @@ -22,10 +29,6 @@ extern "C" { #include "src/nvenc/nvenc_utils.h" #include "src/video.h" -#include - -#include - #if !defined(SUNSHINE_SHADERS_DIR) // for testing this needs to be defined in cmake as we don't do an install #define SUNSHINE_SHADERS_DIR SUNSHINE_ASSETS_DIR "/shaders/directx" #endif @@ -33,8 +36,7 @@ namespace platf { using namespace std::literals; } -static void -free_frame(AVFrame *frame) { +static void free_frame(AVFrame *frame) { av_frame_free(&frame); } @@ -42,9 +44,8 @@ using frame_t = util::safe_ptr; namespace platf::dxgi { - template - buf_t - make_buffer(device_t::pointer device, const T &t) { + template + buf_t make_buffer(device_t::pointer device, const T &t) { static_assert(sizeof(T) % 16 == 0, "Buffer needs to be aligned on a 16-byte alignment"); D3D11_BUFFER_DESC buffer_desc { @@ -64,11 +65,10 @@ namespace platf::dxgi { return nullptr; } - return buf_t { buf_p }; + return buf_t {buf_p}; } - blend_t - make_blend(device_t::pointer device, bool enable, bool invert) { + blend_t make_blend(device_t::pointer device, bool enable, bool invert) { D3D11_BLEND_DESC bdesc {}; auto &rt = bdesc.RenderTarget[0]; rt.BlendEnable = enable; @@ -82,8 +82,7 @@ namespace platf::dxgi { // Invert colors rt.SrcBlend = D3D11_BLEND_INV_DEST_COLOR; rt.DestBlend = D3D11_BLEND_INV_SRC_COLOR; - } - else { + } else { // Regular alpha blending rt.SrcBlend = D3D11_BLEND_SRC_ALPHA; rt.DestBlend = D3D11_BLEND_INV_SRC_ALPHA; @@ -163,8 +162,7 @@ namespace platf::dxgi { bool _locked = false; texture_lock_helper(const texture_lock_helper &) = delete; - texture_lock_helper & - operator=(const texture_lock_helper &) = delete; + texture_lock_helper &operator=(const texture_lock_helper &) = delete; texture_lock_helper(texture_lock_helper &&other) { _mutex.reset(other._mutex.release()); @@ -172,9 +170,10 @@ namespace platf::dxgi { other._locked = false; } - texture_lock_helper & - operator=(texture_lock_helper &&other) { - if (_locked) _mutex->ReleaseSync(0); + texture_lock_helper &operator=(texture_lock_helper &&other) { + if (_locked) { + _mutex->ReleaseSync(0); + } _mutex.reset(other._mutex.release()); _locked = other._locked; other._locked = false; @@ -183,29 +182,32 @@ namespace platf::dxgi { texture_lock_helper(IDXGIKeyedMutex *mutex): _mutex(mutex) { - if (_mutex) _mutex->AddRef(); + if (_mutex) { + _mutex->AddRef(); + } } ~texture_lock_helper() { - if (_locked) _mutex->ReleaseSync(0); + if (_locked) { + _mutex->ReleaseSync(0); + } } - bool - lock() { - if (_locked) return true; + bool lock() { + if (_locked) { + return true; + } HRESULT status = _mutex->AcquireSync(0, INFINITE); if (status == S_OK) { _locked = true; - } - else { + } else { BOOST_LOG(error) << "Failed to acquire texture mutex [0x"sv << util::hex(status).to_string_view() << ']'; } return _locked; } }; - util::buffer_t - make_cursor_xor_image(const util::buffer_t &img_data, DXGI_OUTDUPL_POINTER_SHAPE_INFO shape_info) { + util::buffer_t make_cursor_xor_image(const util::buffer_t &img_data, DXGI_OUTDUPL_POINTER_SHAPE_INFO shape_info) { constexpr std::uint32_t inverted = 0xFFFFFFFF; constexpr std::uint32_t transparent = 0; @@ -213,25 +215,24 @@ namespace platf::dxgi { case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_COLOR: // This type doesn't require any XOR-blending return {}; - case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MASKED_COLOR: { - util::buffer_t cursor_img = img_data; - std::for_each((std::uint32_t *) std::begin(cursor_img), (std::uint32_t *) std::end(cursor_img), [](auto &pixel) { - auto alpha = (std::uint8_t)((pixel >> 24) & 0xFF); - if (alpha == 0xFF) { - // Pixels with 0xFF alpha will be XOR-blended as is. - } - else if (alpha == 0x00) { - // Pixels with 0x00 alpha will be blended by make_cursor_alpha_image(). - // We make them transparent for the XOR-blended cursor image. - pixel = transparent; - } - else { - // Other alpha values are illegal in masked color cursors - BOOST_LOG(warning) << "Illegal alpha value in masked color cursor: " << alpha; - } - }); - return cursor_img; - } + case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MASKED_COLOR: + { + util::buffer_t cursor_img = img_data; + std::for_each((std::uint32_t *) std::begin(cursor_img), (std::uint32_t *) std::end(cursor_img), [](auto &pixel) { + auto alpha = (std::uint8_t) ((pixel >> 24) & 0xFF); + if (alpha == 0xFF) { + // Pixels with 0xFF alpha will be XOR-blended as is. + } else if (alpha == 0x00) { + // Pixels with 0x00 alpha will be blended by make_cursor_alpha_image(). + // We make them transparent for the XOR-blended cursor image. + pixel = transparent; + } else { + // Other alpha values are illegal in masked color cursors + BOOST_LOG(warning) << "Illegal alpha value in masked color cursor: " << alpha; + } + }); + return cursor_img; + } case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MONOCHROME: // Monochrome is handled below break; @@ -242,7 +243,7 @@ namespace platf::dxgi { shape_info.Height /= 2; - util::buffer_t cursor_img { shape_info.Width * shape_info.Height * 4 }; + util::buffer_t cursor_img {shape_info.Width * shape_info.Height * 4}; auto bytes = shape_info.Pitch * shape_info.Height; auto pixel_begin = (std::uint32_t *) std::begin(cursor_img); @@ -275,33 +276,31 @@ namespace platf::dxgi { return cursor_img; } - util::buffer_t - make_cursor_alpha_image(const util::buffer_t &img_data, DXGI_OUTDUPL_POINTER_SHAPE_INFO shape_info) { + util::buffer_t make_cursor_alpha_image(const util::buffer_t &img_data, DXGI_OUTDUPL_POINTER_SHAPE_INFO shape_info) { constexpr std::uint32_t black = 0xFF000000; constexpr std::uint32_t white = 0xFFFFFFFF; constexpr std::uint32_t transparent = 0; switch (shape_info.Type) { - case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MASKED_COLOR: { - util::buffer_t cursor_img = img_data; - std::for_each((std::uint32_t *) std::begin(cursor_img), (std::uint32_t *) std::end(cursor_img), [](auto &pixel) { - auto alpha = (std::uint8_t)((pixel >> 24) & 0xFF); - if (alpha == 0xFF) { - // Pixels with 0xFF alpha will be XOR-blended by make_cursor_xor_image(). - // We make them transparent for the alpha-blended cursor image. - pixel = transparent; - } - else if (alpha == 0x00) { - // Pixels with 0x00 alpha will be blended as opaque with the alpha-blended image. - pixel |= 0xFF000000; - } - else { - // Other alpha values are illegal in masked color cursors - BOOST_LOG(warning) << "Illegal alpha value in masked color cursor: " << alpha; - } - }); - return cursor_img; - } + case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MASKED_COLOR: + { + util::buffer_t cursor_img = img_data; + std::for_each((std::uint32_t *) std::begin(cursor_img), (std::uint32_t *) std::end(cursor_img), [](auto &pixel) { + auto alpha = (std::uint8_t) ((pixel >> 24) & 0xFF); + if (alpha == 0xFF) { + // Pixels with 0xFF alpha will be XOR-blended by make_cursor_xor_image(). + // We make them transparent for the alpha-blended cursor image. + pixel = transparent; + } else if (alpha == 0x00) { + // Pixels with 0x00 alpha will be blended as opaque with the alpha-blended image. + pixel |= 0xFF000000; + } else { + // Other alpha values are illegal in masked color cursors + BOOST_LOG(warning) << "Illegal alpha value in masked color cursor: " << alpha; + } + }); + return cursor_img; + } case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_COLOR: // Color cursors are just an ARGB bitmap which requires no processing. return img_data; @@ -315,7 +314,7 @@ namespace platf::dxgi { shape_info.Height /= 2; - util::buffer_t cursor_img { shape_info.Width * shape_info.Height * 4 }; + util::buffer_t cursor_img {shape_info.Width * shape_info.Height * 4}; auto bytes = shape_info.Pitch * shape_info.Height; auto pixel_begin = (std::uint32_t *) std::begin(cursor_img); @@ -350,8 +349,7 @@ namespace platf::dxgi { return cursor_img; } - blob_t - compile_shader(LPCSTR file, LPCSTR entrypoint, LPCSTR shader_model) { + blob_t compile_shader(LPCSTR file, LPCSTR entrypoint, LPCSTR shader_model) { blob_t::pointer msg_p = nullptr; blob_t::pointer compiled_p; @@ -365,7 +363,7 @@ namespace platf::dxgi { auto status = D3DCompileFromFile(wFile.c_str(), nullptr, D3D_COMPILE_STANDARD_FILE_INCLUDE, entrypoint, shader_model, flags, 0, &compiled_p, &msg_p); if (msg_p) { - BOOST_LOG(warning) << std::string_view { (const char *) msg_p->GetBufferPointer(), msg_p->GetBufferSize() - 1 }; + BOOST_LOG(warning) << std::string_view {(const char *) msg_p->GetBufferPointer(), msg_p->GetBufferSize() - 1}; msg_p->Release(); } @@ -374,29 +372,25 @@ namespace platf::dxgi { return nullptr; } - return blob_t { compiled_p }; + return blob_t {compiled_p}; } - blob_t - compile_pixel_shader(LPCSTR file) { + blob_t compile_pixel_shader(LPCSTR file) { return compile_shader(file, "main_ps", "ps_5_0"); } - blob_t - compile_vertex_shader(LPCSTR file) { + blob_t compile_vertex_shader(LPCSTR file) { return compile_shader(file, "main_vs", "vs_5_0"); } class d3d_base_encode_device final { public: - int - convert(platf::img_t &img_base) { + int convert(platf::img_t &img_base) { // Garbage collect mapped capture images whose weak references have expired for (auto it = img_ctx_map.begin(); it != img_ctx_map.end();) { if (it->second.img_weak.expired()) { it = img_ctx_map.erase(it); - } - else { + } else { it++; } } @@ -443,7 +437,9 @@ namespace platf::dxgi { // Clear render target view(s) once so that the aspect ratio mismatch "bars" appear black if (!rtvs_cleared) { auto black = create_black_texture_for_rtv_clear(); - if (black) draw(black, out_Y_or_YUV_viewports_for_clear, out_UV_viewport_for_clear); + if (black) { + draw(black, out_Y_or_YUV_viewports_for_clear, out_UV_viewport_for_clear); + } rtvs_cleared = true; } @@ -460,8 +456,7 @@ namespace platf::dxgi { return 0; } - void - apply_colorspace(const ::video::sunshine_colorspace_t &colorspace) { + void apply_colorspace(const ::video::sunshine_colorspace_t &colorspace) { auto color_vectors = ::video::color_vectors_from_colorspace(colorspace); if (format == DXGI_FORMAT_AYUV || @@ -486,23 +481,22 @@ namespace platf::dxgi { this->color_matrix = std::move(color_matrix); } - int - init_output(ID3D11Texture2D *frame_texture, int width, int height) { + int init_output(ID3D11Texture2D *frame_texture, int width, int height) { // The underlying frame pool owns the texture, so we must reference it for ourselves frame_texture->AddRef(); output_texture.reset(frame_texture); HRESULT status = S_OK; -#define create_vertex_shader_helper(x, y) \ +#define create_vertex_shader_helper(x, y) \ if (FAILED(status = device->CreateVertexShader(x->GetBufferPointer(), x->GetBufferSize(), nullptr, &y))) { \ - BOOST_LOG(error) << "Failed to create vertex shader " << #x << ": " << util::log_hex(status); \ - return -1; \ + BOOST_LOG(error) << "Failed to create vertex shader " << #x << ": " << util::log_hex(status); \ + return -1; \ } -#define create_pixel_shader_helper(x, y) \ +#define create_pixel_shader_helper(x, y) \ if (FAILED(status = device->CreatePixelShader(x->GetBufferPointer(), x->GetBufferSize(), nullptr, &y))) { \ - BOOST_LOG(error) << "Failed to create pixel shader " << #x << ": " << util::log_hex(status); \ - return -1; \ + BOOST_LOG(error) << "Failed to create pixel shader " << #x << ": " << util::log_hex(status); \ + return -1; \ } const bool downscaling = display->width > width || display->height > height; @@ -517,8 +511,7 @@ namespace platf::dxgi { create_vertex_shader_helper(convert_yuv420_packed_uv_type0s_vs_hlsl, convert_UV_vs); create_pixel_shader_helper(convert_yuv420_packed_uv_type0s_ps_hlsl, convert_UV_ps); create_pixel_shader_helper(convert_yuv420_packed_uv_type0s_ps_linear_hlsl, convert_UV_fp16_ps); - } - else { + } else { create_vertex_shader_helper(convert_yuv420_packed_uv_type0_vs_hlsl, convert_UV_vs); create_pixel_shader_helper(convert_yuv420_packed_uv_type0_ps_hlsl, convert_UV_ps); create_pixel_shader_helper(convert_yuv420_packed_uv_type0_ps_linear_hlsl, convert_UV_fp16_ps); @@ -531,8 +524,7 @@ namespace platf::dxgi { create_pixel_shader_helper(convert_yuv420_planar_y_ps_hlsl, convert_Y_or_YUV_ps); if (display->is_hdr()) { create_pixel_shader_helper(convert_yuv420_planar_y_ps_perceptual_quantizer_hlsl, convert_Y_or_YUV_fp16_ps); - } - else { + } else { create_pixel_shader_helper(convert_yuv420_planar_y_ps_linear_hlsl, convert_Y_or_YUV_fp16_ps); } if (downscaling) { @@ -540,18 +532,15 @@ namespace platf::dxgi { create_pixel_shader_helper(convert_yuv420_packed_uv_type0s_ps_hlsl, convert_UV_ps); if (display->is_hdr()) { create_pixel_shader_helper(convert_yuv420_packed_uv_type0s_ps_perceptual_quantizer_hlsl, convert_UV_fp16_ps); - } - else { + } else { create_pixel_shader_helper(convert_yuv420_packed_uv_type0s_ps_linear_hlsl, convert_UV_fp16_ps); } - } - else { + } else { create_vertex_shader_helper(convert_yuv420_packed_uv_type0_vs_hlsl, convert_UV_vs); create_pixel_shader_helper(convert_yuv420_packed_uv_type0_ps_hlsl, convert_UV_ps); if (display->is_hdr()) { create_pixel_shader_helper(convert_yuv420_packed_uv_type0_ps_perceptual_quantizer_hlsl, convert_UV_fp16_ps); - } - else { + } else { create_pixel_shader_helper(convert_yuv420_packed_uv_type0_ps_linear_hlsl, convert_UV_fp16_ps); } } @@ -563,8 +552,7 @@ namespace platf::dxgi { create_pixel_shader_helper(convert_yuv444_planar_ps_hlsl, convert_Y_or_YUV_ps); if (display->is_hdr()) { create_pixel_shader_helper(convert_yuv444_planar_ps_perceptual_quantizer_hlsl, convert_Y_or_YUV_fp16_ps); - } - else { + } else { create_pixel_shader_helper(convert_yuv444_planar_ps_linear_hlsl, convert_Y_or_YUV_fp16_ps); } break; @@ -582,8 +570,7 @@ namespace platf::dxgi { create_pixel_shader_helper(convert_yuv444_packed_y410_ps_hlsl, convert_Y_or_YUV_ps); if (display->is_hdr()) { create_pixel_shader_helper(convert_yuv444_packed_y410_ps_perceptual_quantizer_hlsl, convert_Y_or_YUV_fp16_ps); - } - else { + } else { create_pixel_shader_helper(convert_yuv444_packed_y410_ps_linear_hlsl, convert_Y_or_YUV_fp16_ps); } break; @@ -611,22 +598,22 @@ namespace platf::dxgi { auto offsetX = (out_width - out_width_f) / 2; auto offsetY = (out_height - out_height_f) / 2; - out_Y_or_YUV_viewports[0] = { offsetX, offsetY, out_width_f, out_height_f, 0.0f, 1.0f }; // Y plane + out_Y_or_YUV_viewports[0] = {offsetX, offsetY, out_width_f, out_height_f, 0.0f, 1.0f}; // Y plane out_Y_or_YUV_viewports[1] = out_Y_or_YUV_viewports[0]; // U plane out_Y_or_YUV_viewports[1].TopLeftY += out_height; out_Y_or_YUV_viewports[2] = out_Y_or_YUV_viewports[1]; // V plane out_Y_or_YUV_viewports[2].TopLeftY += out_height; - out_Y_or_YUV_viewports_for_clear[0] = { 0, 0, (float) out_width, (float) out_height, 0.0f, 1.0f }; // Y plane + out_Y_or_YUV_viewports_for_clear[0] = {0, 0, (float) out_width, (float) out_height, 0.0f, 1.0f}; // Y plane out_Y_or_YUV_viewports_for_clear[1] = out_Y_or_YUV_viewports_for_clear[0]; // U plane out_Y_or_YUV_viewports_for_clear[1].TopLeftY += out_height; out_Y_or_YUV_viewports_for_clear[2] = out_Y_or_YUV_viewports_for_clear[1]; // V plane out_Y_or_YUV_viewports_for_clear[2].TopLeftY += out_height; - out_UV_viewport = { offsetX / 2, offsetY / 2, out_width_f / 2, out_height_f / 2, 0.0f, 1.0f }; - out_UV_viewport_for_clear = { 0, 0, (float) out_width / 2, (float) out_height / 2, 0.0f, 1.0f }; + out_UV_viewport = {offsetX / 2, offsetY / 2, out_width_f / 2, out_height_f / 2, 0.0f, 1.0f}; + out_UV_viewport_for_clear = {0, 0, (float) out_width / 2, (float) out_height / 2, 0.0f, 1.0f}; - float subsample_offset_in[16 / sizeof(float)] { 1.0f / (float) out_width_f, 1.0f / (float) out_height_f }; // aligned to 16-byte + float subsample_offset_in[16 / sizeof(float)] {1.0f / (float) out_width_f, 1.0f / (float) out_height_f}; // aligned to 16-byte subsample_offset = make_buffer(device.get(), subsample_offset_in); if (!subsample_offset) { @@ -637,7 +624,7 @@ namespace platf::dxgi { { int32_t rotation_modifier = display->display_rotation == DXGI_MODE_ROTATION_UNSPECIFIED ? 0 : display->display_rotation - 1; - int32_t rotation_data[16 / sizeof(int32_t)] { -rotation_modifier }; // aligned to 16-byte + int32_t rotation_data[16 / sizeof(int32_t)] {-rotation_modifier}; // aligned to 16-byte auto rotation = make_buffer(device.get(), rotation_data); if (!rotation) { BOOST_LOG(error) << "Failed to create display rotation vertex constant buffer"; @@ -695,22 +682,25 @@ namespace platf::dxgi { }; // Create Y/YUV render target view - if (!create_rtv(out_Y_or_YUV_rtv, rtv_Y_or_YUV_format)) return -1; + if (!create_rtv(out_Y_or_YUV_rtv, rtv_Y_or_YUV_format)) { + return -1; + } // Create UV render target view if needed - if (rtv_UV_format != DXGI_FORMAT_UNKNOWN && !create_rtv(out_UV_rtv, rtv_UV_format)) return -1; + if (rtv_UV_format != DXGI_FORMAT_UNKNOWN && !create_rtv(out_UV_rtv, rtv_UV_format)) { + return -1; + } if (rtv_simple_clear) { // Clear the RTVs to ensure the aspect ratio padding is black - const float y_black[] = { 0.0f, 0.0f, 0.0f, 0.0f }; + const float y_black[] = {0.0f, 0.0f, 0.0f, 0.0f}; device_ctx->ClearRenderTargetView(out_Y_or_YUV_rtv.get(), y_black); if (out_UV_rtv) { - const float uv_black[] = { 0.5f, 0.5f, 0.5f, 0.5f }; + const float uv_black[] = {0.5f, 0.5f, 0.5f, 0.5f}; device_ctx->ClearRenderTargetView(out_UV_rtv.get(), uv_black); } rtvs_cleared = true; - } - else { + } else { // Can't use ClearRenderTargetView(), will clear on first convert() rtvs_cleared = false; } @@ -718,8 +708,7 @@ namespace platf::dxgi { return 0; } - int - init(std::shared_ptr display, adapter_t::pointer adapter_p, pix_fmt_e pix_fmt) { + int init(std::shared_ptr display, adapter_t::pointer adapter_p, pix_fmt_e pix_fmt) { switch (pix_fmt) { case pix_fmt_e::nv12: format = DXGI_FORMAT_NV12; @@ -760,12 +749,14 @@ namespace platf::dxgi { adapter_p, D3D_DRIVER_TYPE_UNKNOWN, nullptr, - D3D11_CREATE_DEVICE_FLAGS, - featureLevels, sizeof(featureLevels) / sizeof(D3D_FEATURE_LEVEL), + D3D11_CREATE_DEVICE_FLAGS | D3D11_CREATE_DEVICE_VIDEO_SUPPORT, + featureLevels, + sizeof(featureLevels) / sizeof(D3D_FEATURE_LEVEL), D3D11_SDK_VERSION, &device, nullptr, - &device_ctx); + &device_ctx + ); if (FAILED(status)) { BOOST_LOG(error) << "Failed to create encoder D3D11 device [0x"sv << util::hex(status).to_string_view() << ']'; @@ -842,8 +833,7 @@ namespace platf::dxgi { std::weak_ptr img_weak; - void - reset() { + void reset() { capture_texture_p = nullptr; encoder_texture.reset(); encoder_input_res.reset(); @@ -852,8 +842,7 @@ namespace platf::dxgi { } }; - int - initialize_image_context(const img_d3d_t &img, encoder_img_ctx_t &img_ctx) { + int initialize_image_context(const img_d3d_t &img, encoder_img_ctx_t &img_ctx) { // If we've already opened the shared texture, we're done if (img_ctx.encoder_texture && img.capture_texture.get() == img_ctx.capture_texture_p) { return 0; @@ -898,8 +887,7 @@ namespace platf::dxgi { return 0; } - shader_res_t - create_black_texture_for_rtv_clear() { + shader_res_t create_black_texture_for_rtv_clear() { constexpr auto width = 32; constexpr auto height = 32; @@ -914,7 +902,7 @@ namespace platf::dxgi { texture_desc.BindFlags = D3D11_BIND_SHADER_RESOURCE; std::vector mem(4 * width * height, 0); - D3D11_SUBRESOURCE_DATA texture_data = { mem.data(), 4 * width, 0 }; + D3D11_SUBRESOURCE_DATA texture_data = {mem.data(), 4 * width, 0}; texture2d_t texture; auto status = device->CreateTexture2D(&texture_desc, &texture_data, &texture); @@ -974,25 +962,21 @@ namespace platf::dxgi { class d3d_avcodec_encode_device_t: public avcodec_encode_device_t { public: - int - init(std::shared_ptr display, adapter_t::pointer adapter_p, pix_fmt_e pix_fmt) { + int init(std::shared_ptr display, adapter_t::pointer adapter_p, pix_fmt_e pix_fmt) { int result = base.init(display, adapter_p, pix_fmt); data = base.device.get(); return result; } - int - convert(platf::img_t &img_base) override { + int convert(platf::img_t &img_base) override { return base.convert(img_base); } - void - apply_colorspace() override { + void apply_colorspace() override { base.apply_colorspace(colorspace); } - void - init_hwframes(AVHWFramesContext *frames) override { + void init_hwframes(AVHWFramesContext *frames) override { // We may be called with a QSV or D3D11VA context if (frames->device_ctx->type == AV_HWDEVICE_TYPE_D3D11VA) { auto d3d11_frames = (AVD3D11VAFramesContext *) frames->hwctx; @@ -1006,8 +990,7 @@ namespace platf::dxgi { frames->initial_pool_size = 1; } - int - prepare_to_derive_context(int hw_device_type) override { + int prepare_to_derive_context(int hw_device_type) override { // QuickSync requires our device to be multithread-protected if (hw_device_type == AV_HWDEVICE_TYPE_QSV) { multithread_t mt; @@ -1024,8 +1007,7 @@ namespace platf::dxgi { return 0; } - int - set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) override { + int set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) override { this->hwframe.reset(frame); this->frame = frame; @@ -1033,7 +1015,7 @@ namespace platf::dxgi { if (!frame->buf[0]) { auto err = av_hwframe_get_buffer(hw_frames_ctx, frame, 0); if (err) { - char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 }; + char err_str[AV_ERROR_MAX_STRING_SIZE] {0}; BOOST_LOG(error) << "Failed to get hwframe buffer: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err); return -1; } @@ -1042,21 +1024,20 @@ namespace platf::dxgi { // If this is a frame from a derived context, we'll need to map it to D3D11 ID3D11Texture2D *frame_texture; if (frame->format != AV_PIX_FMT_D3D11) { - frame_t d3d11_frame { av_frame_alloc() }; + frame_t d3d11_frame {av_frame_alloc()}; d3d11_frame->format = AV_PIX_FMT_D3D11; auto err = av_hwframe_map(d3d11_frame.get(), frame, AV_HWFRAME_MAP_WRITE | AV_HWFRAME_MAP_OVERWRITE); if (err) { - char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 }; + char err_str[AV_ERROR_MAX_STRING_SIZE] {0}; BOOST_LOG(error) << "Failed to map D3D11 frame: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err); return -1; } // Get the texture from the mapped frame frame_texture = (ID3D11Texture2D *) d3d11_frame->data[0]; - } - else { + } else { // Otherwise, we can just use the texture inside the original frame frame_texture = (ID3D11Texture2D *) frame->data[0]; } @@ -1071,20 +1052,20 @@ namespace platf::dxgi { class d3d_nvenc_encode_device_t: public nvenc_encode_device_t { public: - bool - init_device(std::shared_ptr display, adapter_t::pointer adapter_p, pix_fmt_e pix_fmt) { + bool init_device(std::shared_ptr display, adapter_t::pointer adapter_p, pix_fmt_e pix_fmt) { buffer_format = nvenc::nvenc_format_from_sunshine_format(pix_fmt); if (buffer_format == NV_ENC_BUFFER_FORMAT_UNDEFINED) { BOOST_LOG(error) << "Unexpected pixel format for NvENC ["sv << from_pix_fmt(pix_fmt) << ']'; return false; } - if (base.init(display, adapter_p, pix_fmt)) return false; + if (base.init(display, adapter_p, pix_fmt)) { + return false; + } if (pix_fmt == pix_fmt_e::yuv444p16) { nvenc_d3d = std::make_unique(base.device.get()); - } - else { + } else { nvenc_d3d = std::make_unique(base.device.get()); } nvenc = nvenc_d3d.get(); @@ -1092,19 +1073,21 @@ namespace platf::dxgi { return true; } - bool - init_encoder(const ::video::config_t &client_config, const ::video::sunshine_colorspace_t &colorspace) override { - if (!nvenc_d3d) return false; + bool init_encoder(const ::video::config_t &client_config, const ::video::sunshine_colorspace_t &colorspace) override { + if (!nvenc_d3d) { + return false; + } auto nvenc_colorspace = nvenc::nvenc_colorspace_from_sunshine_colorspace(colorspace); - if (!nvenc_d3d->create_encoder(config::video.nv, client_config, nvenc_colorspace, buffer_format)) return false; + if (!nvenc_d3d->create_encoder(config::video.nv, client_config, nvenc_colorspace, buffer_format)) { + return false; + } base.apply_colorspace(colorspace); return base.init_output(nvenc_d3d->get_input_texture(), client_config.width, client_config.height) == 0; } - int - convert(platf::img_t &img_base) override { + int convert(platf::img_t &img_base) override { return base.convert(img_base); } @@ -1114,8 +1097,7 @@ namespace platf::dxgi { NV_ENC_BUFFER_FORMAT buffer_format = NV_ENC_BUFFER_FORMAT_UNDEFINED; }; - bool - set_cursor_texture(device_t::pointer device, gpu_cursor_t &cursor, util::buffer_t &&cursor_img, DXGI_OUTDUPL_POINTER_SHAPE_INFO &shape_info) { + bool set_cursor_texture(device_t::pointer device, gpu_cursor_t &cursor, util::buffer_t &&cursor_img, DXGI_OUTDUPL_POINTER_SHAPE_INFO &shape_info) { // This cursor image may not be used if (cursor_img.size() == 0) { cursor.input_res.reset(); @@ -1159,14 +1141,13 @@ namespace platf::dxgi { return true; } - capture_e - display_ddup_vram_t::snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor_visible) { + capture_e display_ddup_vram_t::snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor_visible) { HRESULT status; DXGI_OUTDUPL_FRAME_INFO frame_info; resource_t::pointer res_p {}; auto capture_status = dup.next_frame(frame_info, timeout, &res_p); - resource_t res { res_p }; + resource_t res {res_p}; if (capture_status != capture_e::ok) { return capture_status; @@ -1189,7 +1170,7 @@ namespace platf::dxgi { if (frame_info.PointerShapeBufferSize > 0) { DXGI_OUTDUPL_POINTER_SHAPE_INFO shape_info {}; - util::buffer_t img_data { frame_info.PointerShapeBufferSize }; + util::buffer_t img_data {frame_info.PointerShapeBufferSize}; UINT dummy; status = dup.dup->GetFramePointerShape(img_data.size(), std::begin(img_data), &dummy, &shape_info); @@ -1209,11 +1190,9 @@ namespace platf::dxgi { } if (frame_info.LastMouseUpdateTime.QuadPart) { - cursor_alpha.set_pos(frame_info.PointerPosition.Position.x, frame_info.PointerPosition.Position.y, - width, height, display_rotation, frame_info.PointerPosition.Visible); + cursor_alpha.set_pos(frame_info.PointerPosition.Position.x, frame_info.PointerPosition.Position.y, width, height, display_rotation, frame_info.PointerPosition.Visible); - cursor_xor.set_pos(frame_info.PointerPosition.Position.x, frame_info.PointerPosition.Position.y, - width, height, display_rotation, frame_info.PointerPosition.Visible); + cursor_xor.set_pos(frame_info.PointerPosition.Position.x, frame_info.PointerPosition.Position.y, width, height, display_rotation, frame_info.PointerPosition.Visible); } const bool blend_mouse_cursor_flag = (cursor_alpha.visible || cursor_xor.visible) && cursor_visible; @@ -1272,8 +1251,7 @@ namespace platf::dxgi { // We don't know the final capture format yet, so we will encode a black dummy image last_frame_action = lfa::nothing; out_frame_action = ofa::dummy_fallback; - } - else { + } else { if (src) { // We got a new frame from DesktopDuplication... if (blend_mouse_cursor_flag) { @@ -1285,8 +1263,7 @@ namespace platf::dxgi { last_frame_action = lfa::copy_src_to_surface; // Copy the intermediate surface to a new image from pull_free_image_cb and blend the mouse cursor onto it. out_frame_action = ofa::copy_last_surface_and_blend_cursor; - } - else { + } else { // ...and we don't need to blend the mouse cursor. // Copy the frame to a new image from pull_free_image_cb and save the shared pointer to the image // in case the mouse cursor appears without a new frame from DesktopDuplication. @@ -1294,8 +1271,7 @@ namespace platf::dxgi { // Use saved last image shared pointer as output image evading copy. out_frame_action = ofa::forward_last_img; } - } - else if (!std::holds_alternative(last_frame_variant)) { + } else if (!std::holds_alternative(last_frame_variant)) { // We didn't get a new frame from DesktopDuplication... if (blend_mouse_cursor_flag) { // ...but we need to blend the mouse cursor. @@ -1307,8 +1283,7 @@ namespace platf::dxgi { // Copy the intermediate surface which contains last DesktopDuplication frame // to a new image from pull_free_image_cb and blend the mouse cursor onto it. out_frame_action = ofa::copy_last_surface_and_blend_cursor; - } - else { + } else { // ...and we don't need to blend the mouse cursor. // This happens when the mouse cursor disappears from screen, // or there's mouse cursor on screen, but its drawing is disabled in sunshine. @@ -1355,92 +1330,113 @@ namespace platf::dxgi { // Finish creating the image (if it hasn't happened already), // also creates synchronization primitives for shared access from multiple direct3d devices. - if (complete_img(d3d_img.get(), dummy)) return { nullptr, nullptr }; + if (complete_img(d3d_img.get(), dummy)) { + return {nullptr, nullptr}; + } // This image is shared between capture direct3d device and encoders direct3d devices, // we must acquire lock before doing anything to it. texture_lock_helper lock_helper(d3d_img->capture_mutex.get()); if (!lock_helper.lock()) { BOOST_LOG(error) << "Failed to lock capture texture"; - return { nullptr, nullptr }; + return {nullptr, nullptr}; } // Clear the blank flag now that we're ready to capture into the image d3d_img->blank = false; - return { std::move(d3d_img), std::move(lock_helper) }; + return {std::move(d3d_img), std::move(lock_helper)}; }; switch (last_frame_action) { - case lfa::nothing: { - break; - } - - case lfa::replace_surface_with_img: { - auto p_surface = std::get_if(&last_frame_variant); - if (!p_surface) { - BOOST_LOG(error) << "Logical error at " << __FILE__ << ":" << __LINE__; - return capture_e::error; + case lfa::nothing: + { + break; } - std::shared_ptr img; - if (!pull_free_image_cb(img)) return capture_e::interrupted; + case lfa::replace_surface_with_img: + { + auto p_surface = std::get_if(&last_frame_variant); + if (!p_surface) { + BOOST_LOG(error) << "Logical error at " << __FILE__ << ":" << __LINE__; + return capture_e::error; + } - auto [d3d_img, lock] = get_locked_d3d_img(img); - if (!d3d_img) return capture_e::error; + std::shared_ptr img; + if (!pull_free_image_cb(img)) { + return capture_e::interrupted; + } - device_ctx->CopyResource(d3d_img->capture_texture.get(), p_surface->get()); + auto [d3d_img, lock] = get_locked_d3d_img(img); + if (!d3d_img) { + return capture_e::error; + } - // We delay the destruction of intermediate surface in case the mouse cursor reappears shortly. - old_surface_delayed_destruction.reset(p_surface->release()); - old_surface_timestamp = std::chrono::steady_clock::now(); + device_ctx->CopyResource(d3d_img->capture_texture.get(), p_surface->get()); - last_frame_variant = img; - break; - } + // We delay the destruction of intermediate surface in case the mouse cursor reappears shortly. + old_surface_delayed_destruction.reset(p_surface->release()); + old_surface_timestamp = std::chrono::steady_clock::now(); - case lfa::replace_img_with_surface: { - auto p_img = std::get_if>(&last_frame_variant); - if (!p_img) { - BOOST_LOG(error) << "Logical error at " << __FILE__ << ":" << __LINE__; - return capture_e::error; + last_frame_variant = img; + break; } - auto [d3d_img, lock] = get_locked_d3d_img(*p_img); - if (!d3d_img) return capture_e::error; - p_img = nullptr; - last_frame_variant = texture2d_t {}; - auto &surface = std::get(last_frame_variant); - if (!create_surface(surface)) return capture_e::error; + case lfa::replace_img_with_surface: + { + auto p_img = std::get_if>(&last_frame_variant); + if (!p_img) { + BOOST_LOG(error) << "Logical error at " << __FILE__ << ":" << __LINE__; + return capture_e::error; + } + auto [d3d_img, lock] = get_locked_d3d_img(*p_img); + if (!d3d_img) { + return capture_e::error; + } - device_ctx->CopyResource(surface.get(), d3d_img->capture_texture.get()); - break; - } + p_img = nullptr; + last_frame_variant = texture2d_t {}; + auto &surface = std::get(last_frame_variant); + if (!create_surface(surface)) { + return capture_e::error; + } - case lfa::copy_src_to_img: { - last_frame_variant = {}; + device_ctx->CopyResource(surface.get(), d3d_img->capture_texture.get()); + break; + } - std::shared_ptr img; - if (!pull_free_image_cb(img)) return capture_e::interrupted; + case lfa::copy_src_to_img: + { + last_frame_variant = {}; - auto [d3d_img, lock] = get_locked_d3d_img(img); - if (!d3d_img) return capture_e::error; + std::shared_ptr img; + if (!pull_free_image_cb(img)) { + return capture_e::interrupted; + } - device_ctx->CopyResource(d3d_img->capture_texture.get(), src.get()); - last_frame_variant = img; - break; - } + auto [d3d_img, lock] = get_locked_d3d_img(img); + if (!d3d_img) { + return capture_e::error; + } - case lfa::copy_src_to_surface: { - auto p_surface = std::get_if(&last_frame_variant); - if (!p_surface) { - last_frame_variant = texture2d_t {}; - p_surface = std::get_if(&last_frame_variant); - if (!create_surface(*p_surface)) return capture_e::error; + device_ctx->CopyResource(d3d_img->capture_texture.get(), src.get()); + last_frame_variant = img; + break; + } + + case lfa::copy_src_to_surface: + { + auto p_surface = std::get_if(&last_frame_variant); + if (!p_surface) { + last_frame_variant = texture2d_t {}; + p_surface = std::get_if(&last_frame_variant); + if (!create_surface(*p_surface)) { + return capture_e::error; + } + } + device_ctx->CopyResource(p_surface->get(), src.get()); + break; } - device_ctx->CopyResource(p_surface->get(), src.get()); - break; - } } auto blend_cursor = [&](img_d3d_t &d3d_img) { @@ -1476,59 +1472,70 @@ namespace platf::dxgi { }; switch (out_frame_action) { - case ofa::forward_last_img: { - auto p_img = std::get_if>(&last_frame_variant); - if (!p_img) { - BOOST_LOG(error) << "Logical error at " << __FILE__ << ":" << __LINE__; - return capture_e::error; + case ofa::forward_last_img: + { + auto p_img = std::get_if>(&last_frame_variant); + if (!p_img) { + BOOST_LOG(error) << "Logical error at " << __FILE__ << ":" << __LINE__; + return capture_e::error; + } + img_out = *p_img; + break; } - img_out = *p_img; - break; - } - case ofa::copy_last_surface_and_blend_cursor: { - auto p_surface = std::get_if(&last_frame_variant); - if (!p_surface) { - BOOST_LOG(error) << "Logical error at " << __FILE__ << ":" << __LINE__; - return capture_e::error; - } - if (!blend_mouse_cursor_flag) { - BOOST_LOG(error) << "Logical error at " << __FILE__ << ":" << __LINE__; - return capture_e::error; - } + case ofa::copy_last_surface_and_blend_cursor: + { + auto p_surface = std::get_if(&last_frame_variant); + if (!p_surface) { + BOOST_LOG(error) << "Logical error at " << __FILE__ << ":" << __LINE__; + return capture_e::error; + } + if (!blend_mouse_cursor_flag) { + BOOST_LOG(error) << "Logical error at " << __FILE__ << ":" << __LINE__; + return capture_e::error; + } + + if (!pull_free_image_cb(img_out)) { + return capture_e::interrupted; + } - if (!pull_free_image_cb(img_out)) return capture_e::interrupted; + auto [d3d_img, lock] = get_locked_d3d_img(img_out); + if (!d3d_img) { + return capture_e::error; + } - auto [d3d_img, lock] = get_locked_d3d_img(img_out); - if (!d3d_img) return capture_e::error; + device_ctx->CopyResource(d3d_img->capture_texture.get(), p_surface->get()); + blend_cursor(*d3d_img); + break; + } - device_ctx->CopyResource(d3d_img->capture_texture.get(), p_surface->get()); - blend_cursor(*d3d_img); - break; - } + case ofa::dummy_fallback: + { + if (!pull_free_image_cb(img_out)) { + return capture_e::interrupted; + } - case ofa::dummy_fallback: { - if (!pull_free_image_cb(img_out)) return capture_e::interrupted; + // Clear the image if it has been used as a dummy. + // It can have the mouse cursor blended onto it. + auto old_d3d_img = (img_d3d_t *) img_out.get(); + bool reclear_dummy = !old_d3d_img->blank && old_d3d_img->capture_texture; - // Clear the image if it has been used as a dummy. - // It can have the mouse cursor blended onto it. - auto old_d3d_img = (img_d3d_t *) img_out.get(); - bool reclear_dummy = !old_d3d_img->blank && old_d3d_img->capture_texture; + auto [d3d_img, lock] = get_locked_d3d_img(img_out, true); + if (!d3d_img) { + return capture_e::error; + } - auto [d3d_img, lock] = get_locked_d3d_img(img_out, true); - if (!d3d_img) return capture_e::error; + if (reclear_dummy) { + const float rgb_black[] = {0.0f, 0.0f, 0.0f, 0.0f}; + device_ctx->ClearRenderTargetView(d3d_img->capture_rt.get(), rgb_black); + } - if (reclear_dummy) { - const float rgb_black[] = { 0.0f, 0.0f, 0.0f, 0.0f }; - device_ctx->ClearRenderTargetView(d3d_img->capture_rt.get(), rgb_black); - } + if (blend_mouse_cursor_flag) { + blend_cursor(*d3d_img); + } - if (blend_mouse_cursor_flag) { - blend_cursor(*d3d_img); + break; } - - break; - } } // Perform delayed destruction of the unused surface if the time is due. @@ -1543,13 +1550,11 @@ namespace platf::dxgi { return capture_e::ok; } - capture_e - display_ddup_vram_t::release_snapshot() { + capture_e display_ddup_vram_t::release_snapshot() { return dup.release_frame(); } - int - display_ddup_vram_t::init(const ::video::config_t &config, const std::string &display_name) { + int display_ddup_vram_t::init(const ::video::config_t &config, const std::string &display_name) { if (display_base_t::init(config, display_name) || dup.init(this, config)) { return -1; } @@ -1577,7 +1582,7 @@ namespace platf::dxgi { { int32_t rotation_modifier = display_rotation == DXGI_MODE_ROTATION_UNSPECIFIED ? 0 : display_rotation - 1; - int32_t rotation_data[16 / sizeof(int32_t)] { rotation_modifier }; // aligned to 16-byte + int32_t rotation_data[16 / sizeof(int32_t)] {rotation_modifier}; // aligned to 16-byte auto rotation = make_buffer(device.get(), rotation_data); if (!rotation) { BOOST_LOG(error) << "Failed to create display rotation vertex constant buffer"; @@ -1597,7 +1602,7 @@ namespace platf::dxgi { // Use a 300 nit target for the mouse cursor. We should really get // the user's SDR white level in nits, but there is no API that // provides that information to Win32 apps. - float white_multiplier_data[16 / sizeof(float)] { 300.0f / 80.f }; // aligned to 16-byte + float white_multiplier_data[16 / sizeof(float)] {300.0f / 80.f}; // aligned to 16-byte auto white_multiplier = make_buffer(device.get(), white_multiplier_data); if (!white_multiplier) { BOOST_LOG(warning) << "Failed to create cursor blending (normalized white) white multiplier constant buffer"; @@ -1605,8 +1610,7 @@ namespace platf::dxgi { } device_ctx->PSSetConstantBuffers(1, 1, &white_multiplier); - } - else { + } else { status = device->CreatePixelShader(cursor_ps_hlsl->GetBufferPointer(), cursor_ps_hlsl->GetBufferSize(), nullptr, &cursor_ps); if (status) { BOOST_LOG(error) << "Failed to create cursor blending pixel shader [0x"sv << util::hex(status).to_string_view() << ']'; @@ -1636,14 +1640,14 @@ namespace platf::dxgi { * @param timeout how long to wait for the next frame * @param cursor_visible */ - capture_e - display_wgc_vram_t::snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor_visible) { + capture_e display_wgc_vram_t::snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor_visible) { texture2d_t src; uint64_t frame_qpc; dup.set_cursor_visible(cursor_visible); auto capture_status = dup.next_frame(timeout, &src, frame_qpc); - if (capture_status != capture_e::ok) + if (capture_status != capture_e::ok) { return capture_status; + } auto frame_timestamp = std::chrono::steady_clock::now() - qpc_time_difference(qpc_counter(), frame_qpc); D3D11_TEXTURE2D_DESC desc; @@ -1664,8 +1668,9 @@ namespace platf::dxgi { } std::shared_ptr img; - if (!pull_free_image_cb(img)) + if (!pull_free_image_cb(img)) { return capture_e::interrupted; + } auto d3d_img = std::static_pointer_cast(img); d3d_img->blank = false; // image is always ready for capture @@ -1673,13 +1678,11 @@ namespace platf::dxgi { texture_lock_helper lock_helper(d3d_img->capture_mutex.get()); if (lock_helper.lock()) { device_ctx->CopyResource(d3d_img->capture_texture.get(), src.get()); - } - else { + } else { BOOST_LOG(error) << "Failed to lock capture texture"; return capture_e::error; } - } - else { + } else { return capture_e::error; } img_out = img; @@ -1690,21 +1693,19 @@ namespace platf::dxgi { return capture_e::ok; } - capture_e - display_wgc_vram_t::release_snapshot() { + capture_e display_wgc_vram_t::release_snapshot() { return dup.release_frame(); } - int - display_wgc_vram_t::init(const ::video::config_t &config, const std::string &display_name) { - if (display_base_t::init(config, display_name) || dup.init(this, config)) + int display_wgc_vram_t::init(const ::video::config_t &config, const std::string &display_name) { + if (display_base_t::init(config, display_name) || dup.init(this, config)) { return -1; + } return 0; } - std::shared_ptr - display_vram_t::alloc_img() { + std::shared_ptr display_vram_t::alloc_img() { auto img = std::make_shared(); // Initialize format-independent fields @@ -1717,8 +1718,7 @@ namespace platf::dxgi { } // This cannot use ID3D11DeviceContext because it can be called concurrently by the encoding thread - int - display_vram_t::complete_img(platf::img_t *img_base, bool dummy) { + int display_vram_t::complete_img(platf::img_t *img_base, bool dummy) { auto img = (img_d3d_t *) img_base; // If this already has a capture texture and it's not switching dummy state, nothing to do @@ -1801,13 +1801,11 @@ namespace platf::dxgi { /** * @memberof platf::dxgi::display_vram_t */ - int - display_vram_t::dummy_img(platf::img_t *img_base) { + int display_vram_t::dummy_img(platf::img_t *img_base) { return complete_img(img_base, true); } - std::vector - display_vram_t::get_supported_capture_formats() { + std::vector display_vram_t::get_supported_capture_formats() { return { // scRGB FP16 is the ideal format for Wide Color Gamut and Advanced Color // displays (both SDR and HDR). This format uses linear gamma, so we will @@ -1837,8 +1835,7 @@ namespace platf::dxgi { * @param config The codec configuration. * @return `true` if supported, `false` otherwise. */ - bool - display_vram_t::is_codec_supported(std::string_view name, const ::video::config_t &config) { + bool display_vram_t::is_codec_supported(std::string_view name, const ::video::config_t &config) { DXGI_ADAPTER_DESC adapter_desc; adapter->GetDesc(&adapter_desc); @@ -1871,8 +1868,7 @@ namespace platf::dxgi { << AMF_GET_BUILD_VERSION(version); BOOST_LOG(warning) << "If your AMD GPU supports AV1 encoding, update your graphics drivers!"sv; return false; - } - else if (config.dynamicRange && version < AMF_MAKE_FULL_VERSION(1, 4, 23, 0)) { + } else if (config.dynamicRange && version < AMF_MAKE_FULL_VERSION(1, 4, 23, 0)) { // Older versions of the AMD AMF runtime can crash when fed P010 surfaces. // Fail if AMF version is below 1.4.23 where HEVC Main10 encoding was introduced. // AMF 1.4.23 corresponds to driver version 21.12.1 (21.40.11.03) or newer. @@ -1884,20 +1880,16 @@ namespace platf::dxgi { BOOST_LOG(warning) << "If your AMD GPU supports HEVC Main10 encoding, update your graphics drivers!"sv; return false; } - } - else { + } else { BOOST_LOG(warning) << "AMFQueryVersion() failed: "sv << result; } - } - else { + } else { BOOST_LOG(warning) << "AMF DLL missing export: "sv << AMF_QUERY_VERSION_FUNCTION_NAME; } - } - else { + } else { BOOST_LOG(warning) << "Detected AMD GPU but AMF failed to load"sv; } - } - else if (adapter_desc.VendorId == 0x8086) { // Intel + } else if (adapter_desc.VendorId == 0x8086) { // Intel // If it's not a QSV encoder, it's not compatible with an Intel GPU if (!boost::algorithm::ends_with(name, "_qsv")) { return false; @@ -1909,22 +1901,19 @@ namespace platf::dxgi { } // TODO: Blacklist HEVC 4:4:4 based on adapter model } - } - else if (adapter_desc.VendorId == 0x10de) { // Nvidia + } else if (adapter_desc.VendorId == 0x10de) { // Nvidia // If it's not an NVENC encoder, it's not compatible with an Nvidia GPU if (!boost::algorithm::ends_with(name, "_nvenc")) { return false; } - } - else { + } else { BOOST_LOG(warning) << "Unknown GPU vendor ID: " << util::hex(adapter_desc.VendorId).to_string_view(); } return true; } - std::unique_ptr - display_vram_t::make_avcodec_encode_device(pix_fmt_e pix_fmt) { + std::unique_ptr display_vram_t::make_avcodec_encode_device(pix_fmt_e pix_fmt) { auto device = std::make_unique(); if (device->init(shared_from_this(), adapter.get(), pix_fmt) != 0) { return nullptr; @@ -1932,8 +1921,7 @@ namespace platf::dxgi { return device; } - std::unique_ptr - display_vram_t::make_nvenc_encode_device(pix_fmt_e pix_fmt) { + std::unique_ptr display_vram_t::make_nvenc_encode_device(pix_fmt_e pix_fmt) { auto device = std::make_unique(); if (!device->init_device(shared_from_this(), adapter.get(), pix_fmt)) { return nullptr; @@ -1941,14 +1929,15 @@ namespace platf::dxgi { return device; } - int - init() { + int init() { BOOST_LOG(info) << "Compiling shaders..."sv; #define compile_vertex_shader_helper(x) \ - if (!(x##_hlsl = compile_vertex_shader(SUNSHINE_SHADERS_DIR "/" #x ".hlsl"))) return -1; + if (!(x##_hlsl = compile_vertex_shader(SUNSHINE_SHADERS_DIR "/" #x ".hlsl"))) \ + return -1; #define compile_pixel_shader_helper(x) \ - if (!(x##_hlsl = compile_pixel_shader(SUNSHINE_SHADERS_DIR "/" #x ".hlsl"))) return -1; + if (!(x##_hlsl = compile_pixel_shader(SUNSHINE_SHADERS_DIR "/" #x ".hlsl"))) \ + return -1; compile_pixel_shader_helper(convert_yuv420_packed_uv_type0_ps); compile_pixel_shader_helper(convert_yuv420_packed_uv_type0_ps_linear); diff --git a/src/platform/windows/display_wgc.cpp b/src/platform/windows/display_wgc.cpp index df5567eec2c..5b31dcae246 100644 --- a/src/platform/windows/display_wgc.cpp +++ b/src/platform/windows/display_wgc.cpp @@ -1,336 +1,335 @@ -/** - * @file src/platform/windows/display_wgc.cpp - * @brief Definitions for WinRT Windows.Graphics.Capture API - */ -#include - -#include "display.h" - -#include "misc.h" -#include "src/logging.h" - -// Gross hack to work around MINGW-packages#22160 -#define ____FIReference_1_boolean_INTERFACE_DEFINED__ - -#include -#include -#include -#include - -namespace platf { - using namespace std::literals; -} - -namespace winrt { - using namespace Windows::Foundation; - using namespace Windows::Foundation::Metadata; - using namespace Windows::Graphics::Capture; - using namespace Windows::Graphics::DirectX::Direct3D11; - - extern "C" { - HRESULT __stdcall CreateDirect3D11DeviceFromDXGIDevice(::IDXGIDevice *dxgiDevice, ::IInspectable **graphicsDevice); - } - - /** - * Windows structures sometimes have compile-time GUIDs. GCC supports this, but in a roundabout way. - * If WINRT_IMPL_HAS_DECLSPEC_UUID is true, then the compiler supports adding this attribute to a struct. For example, Visual Studio. - * If not, then MinGW GCC has a workaround to assign a GUID to a structure. - */ - struct -#if WINRT_IMPL_HAS_DECLSPEC_UUID - __declspec(uuid("A9B3D012-3DF2-4EE3-B8D1-8695F457D3C1")) -#endif - IDirect3DDxgiInterfaceAccess: ::IUnknown { - virtual HRESULT __stdcall GetInterface(REFIID id, void **object) = 0; - }; -} // namespace winrt -#if !WINRT_IMPL_HAS_DECLSPEC_UUID -static constexpr GUID GUID__IDirect3DDxgiInterfaceAccess = { - 0xA9B3D012, 0x3DF2, 0x4EE3, { 0xB8, 0xD1, 0x86, 0x95, 0xF4, 0x57, 0xD3, 0xC1 } - // compare with __declspec(uuid(...)) for the struct above. -}; -template <> -constexpr auto -__mingw_uuidof() -> GUID const & { - return GUID__IDirect3DDxgiInterfaceAccess; -} -#endif - -namespace platf::dxgi { - wgc_capture_t::wgc_capture_t() { - InitializeConditionVariable(&frame_present_cv); - } - - wgc_capture_t::~wgc_capture_t() { - if (capture_session) - capture_session.Close(); - if (frame_pool) - frame_pool.Close(); - item = nullptr; - capture_session = nullptr; - frame_pool = nullptr; - } - - /** - * @brief Initialize the Windows.Graphics.Capture backend. - * @return 0 on success, -1 on failure. - */ - int - wgc_capture_t::init(display_base_t *display, const ::video::config_t &config) { - HRESULT status; - dxgi::dxgi_t dxgi; - winrt::com_ptr<::IInspectable> d3d_comhandle; - try { - if (!winrt::GraphicsCaptureSession::IsSupported()) { - BOOST_LOG(error) << "Screen capture is not supported on this device for this release of Windows!"sv; - return -1; - } - if (FAILED(status = display->device->QueryInterface(IID_IDXGIDevice, (void **) &dxgi))) { - BOOST_LOG(error) << "Failed to query DXGI interface from device [0x"sv << util::hex(status).to_string_view() << ']'; - return -1; - } - if (FAILED(status = winrt::CreateDirect3D11DeviceFromDXGIDevice(*&dxgi, d3d_comhandle.put()))) { - BOOST_LOG(error) << "Failed to query WinRT DirectX interface from device [0x"sv << util::hex(status).to_string_view() << ']'; - return -1; - } - } - catch (winrt::hresult_error &e) { - BOOST_LOG(error) << "Screen capture is not supported on this device for this release of Windows: failed to acquire device: [0x"sv << util::hex(e.code()).to_string_view() << ']'; - return -1; - } - - DXGI_OUTPUT_DESC output_desc; - uwp_device = d3d_comhandle.as(); - display->output->GetDesc(&output_desc); - - auto monitor_factory = winrt::get_activation_factory(); - if (monitor_factory == nullptr || - FAILED(status = monitor_factory->CreateForMonitor(output_desc.Monitor, winrt::guid_of(), winrt::put_abi(item)))) { - BOOST_LOG(error) << "Screen capture is not supported on this device for this release of Windows: failed to acquire display: [0x"sv << util::hex(status).to_string_view() << ']'; - return -1; - } - - if (config.dynamicRange) - display->capture_format = DXGI_FORMAT_R16G16B16A16_FLOAT; - else - display->capture_format = DXGI_FORMAT_B8G8R8A8_UNORM; - - try { - frame_pool = winrt::Direct3D11CaptureFramePool::CreateFreeThreaded(uwp_device, static_cast(display->capture_format), 2, item.Size()); - capture_session = frame_pool.CreateCaptureSession(item); - frame_pool.FrameArrived({ this, &wgc_capture_t::on_frame_arrived }); - } - catch (winrt::hresult_error &e) { - BOOST_LOG(error) << "Screen capture is not supported on this device for this release of Windows: failed to create capture session: [0x"sv << util::hex(e.code()).to_string_view() << ']'; - return -1; - } - try { - if (winrt::ApiInformation::IsPropertyPresent(L"Windows.Graphics.Capture.GraphicsCaptureSession", L"IsBorderRequired")) { - capture_session.IsBorderRequired(false); - } - else { - BOOST_LOG(warning) << "Can't disable colored border around capture area on this version of Windows"; - } - } - 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 { - capture_session.StartCapture(); - } - catch (winrt::hresult_error &e) { - BOOST_LOG(error) << "Screen capture is not supported on this device for this release of Windows: failed to start capture: [0x"sv << util::hex(e.code()).to_string_view() << ']'; - return -1; - } - return 0; - } - - /** - * This function runs in a separate thread spawned by the frame pool and is a producer of frames. - * To maintain parity with the original display interface, this frame will be consumed by the capture thread. - * Acquire a read-write lock, make the produced frame available to the capture thread, then wake the capture thread. - */ - void - wgc_capture_t::on_frame_arrived(winrt::Direct3D11CaptureFramePool const &sender, winrt::IInspectable const &) { - winrt::Windows::Graphics::Capture::Direct3D11CaptureFrame frame { nullptr }; - try { - frame = sender.TryGetNextFrame(); - } - catch (winrt::hresult_error &e) { - BOOST_LOG(warning) << "Failed to capture frame: "sv << e.code(); - return; - } - if (frame != nullptr) { - AcquireSRWLockExclusive(&frame_lock); - if (produced_frame) - produced_frame.Close(); - - produced_frame = frame; - ReleaseSRWLockExclusive(&frame_lock); - WakeConditionVariable(&frame_present_cv); - } - } - - /** - * @brief Get the next frame from the producer thread. - * If not available, the capture thread blocks until one is, or the wait times out. - * @param timeout how long to wait for the next frame - * @param out a texture containing the frame just captured - * @param out_time the timestamp of the frame just captured - */ - capture_e - wgc_capture_t::next_frame(std::chrono::milliseconds timeout, ID3D11Texture2D **out, uint64_t &out_time) { - // this CONSUMER runs in the capture thread - release_frame(); - - AcquireSRWLockExclusive(&frame_lock); - if (produced_frame == nullptr && SleepConditionVariableSRW(&frame_present_cv, &frame_lock, timeout.count(), 0) == 0) { - ReleaseSRWLockExclusive(&frame_lock); - if (GetLastError() == ERROR_TIMEOUT) - return capture_e::timeout; - else - return capture_e::error; - } - if (produced_frame) { - consumed_frame = produced_frame; - produced_frame = nullptr; - } - ReleaseSRWLockExclusive(&frame_lock); - if (consumed_frame == nullptr) // spurious wakeup - return capture_e::timeout; - - auto capture_access = consumed_frame.Surface().as(); - if (capture_access == nullptr) - return capture_e::error; - capture_access->GetInterface(IID_ID3D11Texture2D, (void **) out); - out_time = consumed_frame.SystemRelativeTime().count(); // raw ticks from query performance counter - return capture_e::ok; - } - - capture_e - wgc_capture_t::release_frame() { - if (consumed_frame != nullptr) { - consumed_frame.Close(); - consumed_frame = nullptr; - } - return capture_e::ok; - } - - int - wgc_capture_t::set_cursor_visible(bool x) { - try { - if (capture_session.IsCursorCaptureEnabled() != x) - capture_session.IsCursorCaptureEnabled(x); - return 0; - } - catch (winrt::hresult_error &) { - return -1; - } - } - - int - display_wgc_ram_t::init(const ::video::config_t &config, const std::string &display_name) { - if (display_base_t::init(config, display_name) || dup.init(this, config)) - return -1; - - texture.reset(); - return 0; - } - - /** - * @brief Get the next frame from the Windows.Graphics.Capture API and copy it into a new snapshot texture. - * @param pull_free_image_cb call this to get a new free image from the video subsystem. - * @param img_out the captured frame is returned here - * @param timeout how long to wait for the next frame - * @param cursor_visible whether to capture the cursor - */ - capture_e - display_wgc_ram_t::snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor_visible) { - HRESULT status; - texture2d_t src; - uint64_t frame_qpc; - dup.set_cursor_visible(cursor_visible); - auto capture_status = dup.next_frame(timeout, &src, frame_qpc); - if (capture_status != capture_e::ok) - return capture_status; - - auto frame_timestamp = std::chrono::steady_clock::now() - qpc_time_difference(qpc_counter(), frame_qpc); - D3D11_TEXTURE2D_DESC desc; - src->GetDesc(&desc); - - // Create the staging texture if it doesn't exist. It should match the source in size and format. - if (texture == nullptr) { - capture_format = desc.Format; - BOOST_LOG(info) << "Capture format ["sv << dxgi_format_to_string(capture_format) << ']'; - - D3D11_TEXTURE2D_DESC t {}; - t.Width = width; - t.Height = height; - t.MipLevels = 1; - t.ArraySize = 1; - t.SampleDesc.Count = 1; - t.Usage = D3D11_USAGE_STAGING; - t.Format = capture_format; - t.CPUAccessFlags = D3D11_CPU_ACCESS_READ; - - auto status = device->CreateTexture2D(&t, nullptr, &texture); - - if (FAILED(status)) { - BOOST_LOG(error) << "Failed to create staging texture [0x"sv << util::hex(status).to_string_view() << ']'; - return capture_e::error; - } - } - - // It's possible for our display enumeration to race with mode changes and result in - // mismatched image pool and desktop texture sizes. If this happens, just reinit again. - if (desc.Width != width || desc.Height != height) { - BOOST_LOG(info) << "Capture size changed ["sv << width << 'x' << height << " -> "sv << desc.Width << 'x' << desc.Height << ']'; - return capture_e::reinit; - } - // It's also possible for the capture format to change on the fly. If that happens, - // reinitialize capture to try format detection again and create new images. - if (capture_format != desc.Format) { - BOOST_LOG(info) << "Capture format changed ["sv << dxgi_format_to_string(capture_format) << " -> "sv << dxgi_format_to_string(desc.Format) << ']'; - return capture_e::reinit; - } - - // Copy from GPU to CPU - device_ctx->CopyResource(texture.get(), src.get()); - - if (!pull_free_image_cb(img_out)) { - return capture_e::interrupted; - } - auto img = (img_t *) img_out.get(); - - // Map the staging texture for CPU access (making it inaccessible for the GPU) - if (FAILED(status = device_ctx->Map(texture.get(), 0, D3D11_MAP_READ, 0, &img_info))) { - BOOST_LOG(error) << "Failed to map texture [0x"sv << util::hex(status).to_string_view() << ']'; - - return capture_e::error; - } - - // Now that we know the capture format, we can finish creating the image - if (complete_img(img, false)) { - device_ctx->Unmap(texture.get(), 0); - img_info.pData = nullptr; - return capture_e::error; - } - - std::copy_n((std::uint8_t *) img_info.pData, height * img_info.RowPitch, (std::uint8_t *) img->data); - - // Unmap the staging texture to allow GPU access again - device_ctx->Unmap(texture.get(), 0); - img_info.pData = nullptr; - - if (img) { - img->frame_timestamp = frame_timestamp; - } - - return capture_e::ok; - } - - capture_e - display_wgc_ram_t::release_snapshot() { - return dup.release_frame(); - } -} // namespace platf::dxgi +/** + * @file src/platform/windows/display_wgc.cpp + * @brief Definitions for WinRT Windows.Graphics.Capture API + */ +// platform includes +#include + +// local includes +#include "display.h" +#include "misc.h" +#include "src/logging.h" + +// Gross hack to work around MINGW-packages#22160 +#define ____FIReference_1_boolean_INTERFACE_DEFINED__ + +#include +#include +#include +#include + +namespace platf { + using namespace std::literals; +} + +namespace winrt { + using namespace Windows::Foundation; + using namespace Windows::Foundation::Metadata; + using namespace Windows::Graphics::Capture; + using namespace Windows::Graphics::DirectX::Direct3D11; + + extern "C" { + HRESULT __stdcall CreateDirect3D11DeviceFromDXGIDevice(::IDXGIDevice *dxgiDevice, ::IInspectable **graphicsDevice); + } + + /** + * Windows structures sometimes have compile-time GUIDs. GCC supports this, but in a roundabout way. + * If WINRT_IMPL_HAS_DECLSPEC_UUID is true, then the compiler supports adding this attribute to a struct. For example, Visual Studio. + * If not, then MinGW GCC has a workaround to assign a GUID to a structure. + */ + struct +#if WINRT_IMPL_HAS_DECLSPEC_UUID + __declspec(uuid("A9B3D012-3DF2-4EE3-B8D1-8695F457D3C1")) +#endif + IDirect3DDxgiInterfaceAccess: ::IUnknown { + virtual HRESULT __stdcall GetInterface(REFIID id, void **object) = 0; + }; +} // namespace winrt +#if !WINRT_IMPL_HAS_DECLSPEC_UUID +static constexpr GUID GUID__IDirect3DDxgiInterfaceAccess = { + 0xA9B3D012, + 0x3DF2, + 0x4EE3, + {0xB8, 0xD1, 0x86, 0x95, 0xF4, 0x57, 0xD3, 0xC1} + // compare with __declspec(uuid(...)) for the struct above. +}; + +template<> +constexpr auto __mingw_uuidof() -> GUID const & { + return GUID__IDirect3DDxgiInterfaceAccess; +} +#endif + +namespace platf::dxgi { + wgc_capture_t::wgc_capture_t() { + InitializeConditionVariable(&frame_present_cv); + } + + wgc_capture_t::~wgc_capture_t() { + if (capture_session) { + capture_session.Close(); + } + if (frame_pool) { + frame_pool.Close(); + } + item = nullptr; + capture_session = nullptr; + frame_pool = nullptr; + } + + /** + * @brief Initialize the Windows.Graphics.Capture backend. + * @return 0 on success, -1 on failure. + */ + int wgc_capture_t::init(display_base_t *display, const ::video::config_t &config) { + HRESULT status; + dxgi::dxgi_t dxgi; + winrt::com_ptr<::IInspectable> d3d_comhandle; + try { + if (!winrt::GraphicsCaptureSession::IsSupported()) { + BOOST_LOG(error) << "Screen capture is not supported on this device for this release of Windows!"sv; + return -1; + } + if (FAILED(status = display->device->QueryInterface(IID_IDXGIDevice, (void **) &dxgi))) { + BOOST_LOG(error) << "Failed to query DXGI interface from device [0x"sv << util::hex(status).to_string_view() << ']'; + return -1; + } + if (FAILED(status = winrt::CreateDirect3D11DeviceFromDXGIDevice(*&dxgi, d3d_comhandle.put()))) { + BOOST_LOG(error) << "Failed to query WinRT DirectX interface from device [0x"sv << util::hex(status).to_string_view() << ']'; + return -1; + } + } catch (winrt::hresult_error &e) { + BOOST_LOG(error) << "Screen capture is not supported on this device for this release of Windows: failed to acquire device: [0x"sv << util::hex(e.code()).to_string_view() << ']'; + return -1; + } + + DXGI_OUTPUT_DESC output_desc; + uwp_device = d3d_comhandle.as(); + display->output->GetDesc(&output_desc); + + auto monitor_factory = winrt::get_activation_factory(); + if (monitor_factory == nullptr || + FAILED(status = monitor_factory->CreateForMonitor(output_desc.Monitor, winrt::guid_of(), winrt::put_abi(item)))) { + BOOST_LOG(error) << "Screen capture is not supported on this device for this release of Windows: failed to acquire display: [0x"sv << util::hex(status).to_string_view() << ']'; + return -1; + } + + if (config.dynamicRange) { + display->capture_format = DXGI_FORMAT_R16G16B16A16_FLOAT; + } else { + display->capture_format = DXGI_FORMAT_B8G8R8A8_UNORM; + } + + try { + frame_pool = winrt::Direct3D11CaptureFramePool::CreateFreeThreaded(uwp_device, static_cast(display->capture_format), 2, item.Size()); + capture_session = frame_pool.CreateCaptureSession(item); + frame_pool.FrameArrived({this, &wgc_capture_t::on_frame_arrived}); + } catch (winrt::hresult_error &e) { + BOOST_LOG(error) << "Screen capture is not supported on this device for this release of Windows: failed to create capture session: [0x"sv << util::hex(e.code()).to_string_view() << ']'; + return -1; + } + try { + if (winrt::ApiInformation::IsPropertyPresent(L"Windows.Graphics.Capture.GraphicsCaptureSession", L"IsBorderRequired")) { + capture_session.IsBorderRequired(false); + } else { + BOOST_LOG(warning) << "Can't disable colored border around capture area on this version of Windows"; + } + } 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 { + capture_session.StartCapture(); + } catch (winrt::hresult_error &e) { + BOOST_LOG(error) << "Screen capture is not supported on this device for this release of Windows: failed to start capture: [0x"sv << util::hex(e.code()).to_string_view() << ']'; + return -1; + } + return 0; + } + + /** + * This function runs in a separate thread spawned by the frame pool and is a producer of frames. + * To maintain parity with the original display interface, this frame will be consumed by the capture thread. + * Acquire a read-write lock, make the produced frame available to the capture thread, then wake the capture thread. + */ + void wgc_capture_t::on_frame_arrived(winrt::Direct3D11CaptureFramePool const &sender, winrt::IInspectable const &) { + winrt::Windows::Graphics::Capture::Direct3D11CaptureFrame frame {nullptr}; + try { + frame = sender.TryGetNextFrame(); + } catch (winrt::hresult_error &e) { + BOOST_LOG(warning) << "Failed to capture frame: "sv << e.code(); + return; + } + if (frame != nullptr) { + AcquireSRWLockExclusive(&frame_lock); + if (produced_frame) { + produced_frame.Close(); + } + + produced_frame = frame; + ReleaseSRWLockExclusive(&frame_lock); + WakeConditionVariable(&frame_present_cv); + } + } + + /** + * @brief Get the next frame from the producer thread. + * If not available, the capture thread blocks until one is, or the wait times out. + * @param timeout how long to wait for the next frame + * @param out a texture containing the frame just captured + * @param out_time the timestamp of the frame just captured + */ + capture_e wgc_capture_t::next_frame(std::chrono::milliseconds timeout, ID3D11Texture2D **out, uint64_t &out_time) { + // this CONSUMER runs in the capture thread + release_frame(); + + AcquireSRWLockExclusive(&frame_lock); + if (produced_frame == nullptr && SleepConditionVariableSRW(&frame_present_cv, &frame_lock, timeout.count(), 0) == 0) { + ReleaseSRWLockExclusive(&frame_lock); + if (GetLastError() == ERROR_TIMEOUT) { + return capture_e::timeout; + } else { + return capture_e::error; + } + } + if (produced_frame) { + consumed_frame = produced_frame; + produced_frame = nullptr; + } + ReleaseSRWLockExclusive(&frame_lock); + if (consumed_frame == nullptr) { // spurious wakeup + return capture_e::timeout; + } + + auto capture_access = consumed_frame.Surface().as(); + if (capture_access == nullptr) { + return capture_e::error; + } + capture_access->GetInterface(IID_ID3D11Texture2D, (void **) out); + out_time = consumed_frame.SystemRelativeTime().count(); // raw ticks from query performance counter + return capture_e::ok; + } + + capture_e wgc_capture_t::release_frame() { + if (consumed_frame != nullptr) { + consumed_frame.Close(); + consumed_frame = nullptr; + } + return capture_e::ok; + } + + int wgc_capture_t::set_cursor_visible(bool x) { + try { + if (capture_session.IsCursorCaptureEnabled() != x) { + capture_session.IsCursorCaptureEnabled(x); + } + return 0; + } catch (winrt::hresult_error &) { + return -1; + } + } + + int display_wgc_ram_t::init(const ::video::config_t &config, const std::string &display_name) { + if (display_base_t::init(config, display_name) || dup.init(this, config)) { + return -1; + } + + texture.reset(); + return 0; + } + + /** + * @brief Get the next frame from the Windows.Graphics.Capture API and copy it into a new snapshot texture. + * @param pull_free_image_cb call this to get a new free image from the video subsystem. + * @param img_out the captured frame is returned here + * @param timeout how long to wait for the next frame + * @param cursor_visible whether to capture the cursor + */ + capture_e display_wgc_ram_t::snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor_visible) { + HRESULT status; + texture2d_t src; + uint64_t frame_qpc; + dup.set_cursor_visible(cursor_visible); + auto capture_status = dup.next_frame(timeout, &src, frame_qpc); + if (capture_status != capture_e::ok) { + return capture_status; + } + + auto frame_timestamp = std::chrono::steady_clock::now() - qpc_time_difference(qpc_counter(), frame_qpc); + D3D11_TEXTURE2D_DESC desc; + src->GetDesc(&desc); + + // Create the staging texture if it doesn't exist. It should match the source in size and format. + if (texture == nullptr) { + capture_format = desc.Format; + BOOST_LOG(info) << "Capture format ["sv << dxgi_format_to_string(capture_format) << ']'; + + D3D11_TEXTURE2D_DESC t {}; + t.Width = width; + t.Height = height; + t.MipLevels = 1; + t.ArraySize = 1; + t.SampleDesc.Count = 1; + t.Usage = D3D11_USAGE_STAGING; + t.Format = capture_format; + t.CPUAccessFlags = D3D11_CPU_ACCESS_READ; + + auto status = device->CreateTexture2D(&t, nullptr, &texture); + + if (FAILED(status)) { + BOOST_LOG(error) << "Failed to create staging texture [0x"sv << util::hex(status).to_string_view() << ']'; + return capture_e::error; + } + } + + // It's possible for our display enumeration to race with mode changes and result in + // mismatched image pool and desktop texture sizes. If this happens, just reinit again. + if (desc.Width != width || desc.Height != height) { + BOOST_LOG(info) << "Capture size changed ["sv << width << 'x' << height << " -> "sv << desc.Width << 'x' << desc.Height << ']'; + return capture_e::reinit; + } + // It's also possible for the capture format to change on the fly. If that happens, + // reinitialize capture to try format detection again and create new images. + if (capture_format != desc.Format) { + BOOST_LOG(info) << "Capture format changed ["sv << dxgi_format_to_string(capture_format) << " -> "sv << dxgi_format_to_string(desc.Format) << ']'; + return capture_e::reinit; + } + + // Copy from GPU to CPU + device_ctx->CopyResource(texture.get(), src.get()); + + if (!pull_free_image_cb(img_out)) { + return capture_e::interrupted; + } + auto img = (img_t *) img_out.get(); + + // Map the staging texture for CPU access (making it inaccessible for the GPU) + if (FAILED(status = device_ctx->Map(texture.get(), 0, D3D11_MAP_READ, 0, &img_info))) { + BOOST_LOG(error) << "Failed to map texture [0x"sv << util::hex(status).to_string_view() << ']'; + + return capture_e::error; + } + + // Now that we know the capture format, we can finish creating the image + if (complete_img(img, false)) { + device_ctx->Unmap(texture.get(), 0); + img_info.pData = nullptr; + return capture_e::error; + } + + std::copy_n((std::uint8_t *) img_info.pData, height * img_info.RowPitch, (std::uint8_t *) img->data); + + // Unmap the staging texture to allow GPU access again + device_ctx->Unmap(texture.get(), 0); + img_info.pData = nullptr; + + if (img) { + img->frame_timestamp = frame_timestamp; + } + + return capture_e::ok; + } + + capture_e display_wgc_ram_t::release_snapshot() { + return dup.release_frame(); + } +} // namespace platf::dxgi diff --git a/src/platform/windows/input.cpp b/src/platform/windows/input.cpp index ea0551746cd..50f8aab8b04 100644 --- a/src/platform/windows/input.cpp +++ b/src/platform/windows/input.cpp @@ -3,13 +3,18 @@ * @brief Definitions for input handling on Windows. */ #define WINVER 0x0A00 + +// platform includes #include +// standard includes #include #include +// lib includes #include +// local includes #include "keylayout.h" #include "misc.h" #include "src/config.h" @@ -19,12 +24,9 @@ #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); +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 { @@ -33,28 +35,32 @@ namespace platf { thread_local HDESK _lastKnownInputDesktop = nullptr; constexpr touch_port_t target_touch_port { - 0, 0, - 65535, 65535 + 0, + 0, + 65535, + 65535 }; using client_t = util::safe_ptr<_VIGEM_CLIENT_T, vigem_free>; using target_t = util::safe_ptr<_VIGEM_TARGET_T, vigem_target_free>; - void CALLBACK - x360_notify( + void CALLBACK x360_notify( client_t::pointer client, target_t::pointer target, - std::uint8_t largeMotor, std::uint8_t smallMotor, + std::uint8_t largeMotor, + std::uint8_t smallMotor, std::uint8_t /* led_number */, - void *userdata); + void *userdata + ); - void CALLBACK - ds4_notify( + void CALLBACK ds4_notify( client_t::pointer client, target_t::pointer target, - std::uint8_t largeMotor, std::uint8_t smallMotor, + std::uint8_t largeMotor, + std::uint8_t smallMotor, DS4_LIGHTBAR_COLOR /* led_color */, - void *userdata); + void *userdata + ); struct gp_touch_context_t { uint8_t pointerIndex; @@ -86,22 +92,22 @@ namespace platf { constexpr float EARTH_G = 9.80665f; -#define MPS2_TO_DS4_ACCEL(x) (int32_t)(((x) / EARTH_G) * 8192) -#define DPS_TO_DS4_GYRO(x) (int32_t)((x) * (1024 / 64)) +#define MPS2_TO_DS4_ACCEL(x) (int32_t) (((x) / EARTH_G) * 8192) +#define DPS_TO_DS4_GYRO(x) (int32_t) ((x) * (1024 / 64)) -#define APPLY_CALIBRATION(val, bias, scale) (int32_t)(((float) (val) + (bias)) / (scale)) +#define APPLY_CALIBRATION(val, bias, scale) (int32_t) (((float) (val) + (bias)) / (scale)) constexpr DS4_TOUCH ds4_touch_unused = { .bPacketCounter = 0, .bIsUpTrackingNum1 = 0x80, - .bTouchData1 = { 0x00, 0x00, 0x00 }, + .bTouchData1 = {0x00, 0x00, 0x00}, .bIsUpTrackingNum2 = 0x80, - .bTouchData2 = { 0x00, 0x00, 0x00 }, + .bTouchData2 = {0x00, 0x00, 0x00}, }; // See https://github.com/ViGEm/ViGEmBus/blob/22835473d17fbf0c4d4bb2f2d42fd692b6e44df4/sys/Ds4Pdo.cpp#L153-L164 constexpr DS4_REPORT_EX ds4_report_init_ex = { - { { .bThumbLX = 0x80, + {{.bThumbLX = 0x80, .bThumbLY = 0x80, .bThumbRX = 0x80, .bThumbRY = 0x80, @@ -117,12 +123,12 @@ namespace platf { .wAccelX = 0, .wAccelY = 0, .wAccelZ = 0, - ._bUnknown1 = { 0x00, 0x00, 0x00, 0x00, 0x00 }, + ._bUnknown1 = {0x00, 0x00, 0x00, 0x00, 0x00}, .bBatteryLvlSpecial = 0x1A, // Wired - Full battery - ._bUnknown2 = { 0x00, 0x00 }, + ._bUnknown2 = {0x00, 0x00}, .bTouchPacketsN = 1, .sCurrentTouch = ds4_touch_unused, - .sPreviousTouch = { ds4_touch_unused, ds4_touch_unused } } } + .sPreviousTouch = {ds4_touch_unused, ds4_touch_unused}}} }; /** @@ -134,8 +140,7 @@ namespace platf { * @param y Y component of motion. * @param z Z component of motion. */ - static void - ds4_update_motion(gamepad_context_t &gamepad, uint8_t motion_type, float x, float y, float z) { + static void ds4_update_motion(gamepad_context_t &gamepad, uint8_t motion_type, float x, float y, float z) { auto &report = gamepad.report.ds4.Report; // Use int32 to process this data, so we can clamp if needed. @@ -192,17 +197,15 @@ namespace platf { class vigem_t { public: - int - init() { + int init() { // Probe ViGEm during startup to see if we can successfully attach gamepads. This will allow us to // immediately display the error message in the web UI even before the user tries to stream. - client_t client { vigem_alloc() }; + client_t client {vigem_alloc()}; VIGEM_ERROR status = vigem_connect(client.get()); if (!VIGEM_SUCCESS(status)) { // Log a special fatal message for this case to show the error in the web UI BOOST_LOG(fatal) << "ViGEmBus is not installed or running. You must install ViGEmBus for gamepad support!"sv; - } - else { + } else { vigem_disconnect(client.get()); } @@ -218,8 +221,7 @@ namespace platf { * @param gp_type The type of gamepad. * @return 0 on success. */ - int - alloc_gamepad_internal(const gamepad_id_t &id, feedback_queue_t &feedback_queue, VIGEM_TARGET_TYPE gp_type) { + int alloc_gamepad_internal(const gamepad_id_t &id, feedback_queue_t &feedback_queue, VIGEM_TARGET_TYPE gp_type) { auto &gamepad = gamepads[id.globalIndex]; assert(!gamepad.gp); @@ -242,8 +244,7 @@ namespace platf { if (gp_type == Xbox360Wired) { gamepad.gp.reset(vigem_target_x360_alloc()); XUSB_REPORT_INIT(&gamepad.report.x360); - } - else { + } else { gamepad.gp.reset(vigem_target_ds4_alloc()); // There is no equivalent DS4_REPORT_EX_INIT() @@ -272,8 +273,7 @@ namespace platf { if (gp_type == Xbox360Wired) { status = vigem_target_x360_register_notification(client.get(), gamepad.gp.get(), x360_notify, this); - } - else { + } else { status = vigem_target_ds4_register_notification(client.get(), gamepad.gp.get(), ds4_notify, this); } @@ -288,8 +288,7 @@ namespace platf { * @brief Detaches the specified gamepad * @param nr The gamepad. */ - void - free_target(int nr) { + void free_target(int nr) { auto &gamepad = gamepads[nr]; if (gamepad.repeat_task) { @@ -327,8 +326,7 @@ namespace platf { * @param largeMotor The large motor. * @param smallMotor The small motor. */ - void - rumble(target_t::pointer target, std::uint8_t largeMotor, std::uint8_t smallMotor) { + void rumble(target_t::pointer target, std::uint8_t largeMotor, std::uint8_t smallMotor) { for (int x = 0; x < gamepads.size(); ++x) { auto &gamepad = gamepads[x]; @@ -342,7 +340,10 @@ namespace platf { normalizedLargeMotor != gamepad.last_rumble.data.rumble.lowfreq) { // We have to use the client-relative index when communicating back to the client gamepad_feedback_msg_t msg = gamepad_feedback_msg_t::make_rumble( - gamepad.client_relative_index, normalizedLargeMotor, normalizedSmallMotor); + gamepad.client_relative_index, + normalizedLargeMotor, + normalizedSmallMotor + ); gamepad.feedback_queue->raise(msg); gamepad.last_rumble = msg; } @@ -358,8 +359,7 @@ namespace platf { * @param g The red channel. * @param b The red channel. */ - void - set_rgb_led(target_t::pointer target, std::uint8_t r, std::uint8_t g, std::uint8_t b) { + void set_rgb_led(target_t::pointer target, std::uint8_t r, std::uint8_t g, std::uint8_t b) { for (int x = 0; x < gamepads.size(); ++x) { auto &gamepad = gamepads[x]; @@ -401,13 +401,14 @@ namespace platf { client_t client; }; - void CALLBACK - x360_notify( + void CALLBACK x360_notify( client_t::pointer client, target_t::pointer target, - std::uint8_t largeMotor, std::uint8_t smallMotor, + std::uint8_t largeMotor, + std::uint8_t smallMotor, std::uint8_t /* led_number */, - void *userdata) { + void *userdata + ) { BOOST_LOG(debug) << "largeMotor: "sv << (int) largeMotor << std::endl << "smallMotor: "sv << (int) smallMotor; @@ -415,13 +416,14 @@ namespace platf { task_pool.push(&vigem_t::rumble, (vigem_t *) userdata, target, largeMotor, smallMotor); } - void CALLBACK - ds4_notify( + void CALLBACK ds4_notify( client_t::pointer client, target_t::pointer target, - std::uint8_t largeMotor, std::uint8_t smallMotor, + std::uint8_t largeMotor, + std::uint8_t smallMotor, DS4_LIGHTBAR_COLOR led_color, - void *userdata) { + void *userdata + ) { BOOST_LOG(debug) << "largeMotor: "sv << (int) largeMotor << std::endl << "smallMotor: "sv << (int) smallMotor << std::endl @@ -445,9 +447,8 @@ namespace platf { decltype(DestroySyntheticPointerDevice) *fnDestroySyntheticPointerDevice; }; - input_t - input() { - input_t result { new input_raw_t {} }; + input_t input() { + input_t result {new input_raw_t {}}; auto &raw = *(input_raw_t *) result.get(); raw.vigem = new vigem_t {}; @@ -468,8 +469,7 @@ namespace platf { * @brief Calls SendInput() and switches input desktops if required. * @param i The `INPUT` struct to send. */ - void - send_input(INPUT &i) { + void send_input(INPUT &i) { retry: auto send = SendInput(1, &i, sizeof(INPUT)); if (send != 1) { @@ -491,8 +491,7 @@ namespace platf { * @param count The number of elements in `pointerInfo`. * @return true if input was successfully injected. */ - bool - inject_synthetic_pointer_input(input_raw_t *input, HSYNTHETICPOINTERDEVICE device, const POINTER_TYPE_INFO *pointerInfo, UINT32 count) { + bool inject_synthetic_pointer_input(input_raw_t *input, HSYNTHETICPOINTERDEVICE device, const POINTER_TYPE_INFO *pointerInfo, UINT32 count) { retry: if (!input->fnInjectSyntheticPointerInput(device, pointerInfo, count)) { auto hDesk = syncThreadDesktop(); @@ -505,8 +504,7 @@ namespace platf { return true; } - void - abs_mouse(input_t &input, const touch_port_t &touch_port, float x, float y) { + void abs_mouse(input_t &input, const touch_port_t &touch_port, float x, float y) { INPUT i {}; i.type = INPUT_MOUSE; @@ -528,8 +526,7 @@ namespace platf { send_input(i); } - void - move_mouse(input_t &input, int deltaX, int deltaY) { + void move_mouse(input_t &input, int deltaX, int deltaY) { INPUT i {}; i.type = INPUT_MOUSE; @@ -542,13 +539,12 @@ namespace platf { send_input(i); } - util::point_t - get_mouse_loc(input_t &input) { + util::point_t get_mouse_loc(input_t &input) { throw std::runtime_error("not implemented yet, has to pass tests"); // TODO: Tests are failing, something wrong here? POINT p; if (!GetCursorPos(&p)) { - return util::point_t { 0.0, 0.0 }; + return util::point_t {0.0, 0.0}; } return util::point_t { @@ -557,8 +553,7 @@ namespace platf { }; } - void - button_mouse(input_t &input, int button, bool release) { + void button_mouse(input_t &input, int button, bool release) { INPUT i {}; i.type = INPUT_MOUSE; @@ -566,18 +561,14 @@ namespace platf { if (button == 1) { mi.dwFlags = release ? MOUSEEVENTF_LEFTUP : MOUSEEVENTF_LEFTDOWN; - } - else if (button == 2) { + } else if (button == 2) { mi.dwFlags = release ? MOUSEEVENTF_MIDDLEUP : MOUSEEVENTF_MIDDLEDOWN; - } - else if (button == 3) { + } else if (button == 3) { mi.dwFlags = release ? MOUSEEVENTF_RIGHTUP : MOUSEEVENTF_RIGHTDOWN; - } - else if (button == 4) { + } else if (button == 4) { mi.dwFlags = release ? MOUSEEVENTF_XUP : MOUSEEVENTF_XDOWN; mi.mouseData = XBUTTON1; - } - else { + } else { mi.dwFlags = release ? MOUSEEVENTF_XUP : MOUSEEVENTF_XDOWN; mi.mouseData = XBUTTON2; } @@ -585,8 +576,7 @@ namespace platf { send_input(i); } - void - scroll(input_t &input, int distance) { + void scroll(input_t &input, int distance) { INPUT i {}; i.type = INPUT_MOUSE; @@ -598,8 +588,7 @@ namespace platf { send_input(i); } - void - hscroll(input_t &input, int distance) { + void hscroll(input_t &input, int distance) { INPUT i {}; i.type = INPUT_MOUSE; @@ -611,8 +600,7 @@ namespace platf { send_input(i); } - void - keyboard_update(input_t &input, uint16_t modcode, bool release, uint8_t flags) { + void keyboard_update(input_t &input, uint16_t modcode, bool release, uint8_t flags) { INPUT i {}; i.type = INPUT_KEYBOARD; auto &ki = i.ki; @@ -623,8 +611,7 @@ namespace platf { if (!(flags & SS_KBE_FLAG_NON_NORMALIZED)) { // Mask off the extended key byte ki.wScan = VK_TO_SCANCODE_MAP[modcode & 0xFF]; - } - else if (config::input.always_send_scancodes && modcode != VK_LWIN && modcode != VK_RWIN && modcode != VK_PAUSE) { + } else if (config::input.always_send_scancodes && modcode != VK_LWIN && modcode != VK_RWIN && modcode != VK_PAUSE) { // For some reason, MapVirtualKey(VK_LWIN, MAPVK_VK_TO_VSC) doesn't seem to work :/ ki.wScan = MapVirtualKey(modcode, MAPVK_VK_TO_VSC); } @@ -632,8 +619,7 @@ namespace platf { // If we can map this to a scancode, send it as a scancode for maximum game compatibility. if (ki.wScan) { ki.dwFlags = KEYEVENTF_SCANCODE; - } - else { + } else { // If there is no scancode mapping or it's non-normalized, send it as a regular VK event. ki.wVk = modcode; } @@ -712,8 +698,7 @@ namespace platf { * @param input The global input context. * @return A unique pointer to a per-client input data context. */ - std::unique_ptr - allocate_client_input_context(input_t &input) { + std::unique_ptr allocate_client_input_context(input_t &input) { return std::make_unique(input); } @@ -722,8 +707,7 @@ namespace platf { * @details Since this swaps entries around, all slot pointers/references are invalid after compaction. * @param raw The client-specific input context. */ - void - perform_touch_compaction(client_input_raw_t *raw) { + void perform_touch_compaction(client_input_raw_t *raw) { // Windows requires all active touches be contiguous when fed into InjectSyntheticPointerInput(). UINT32 i; for (i = 0; i < ARRAYSIZE(raw->touchInfo); i++) { @@ -754,8 +738,7 @@ namespace platf { * @param eventType The LI_TOUCH_EVENT value from the client. * @return A pointer to the slot entry. */ - POINTER_TYPE_INFO * - pointer_by_id(client_input_raw_t *raw, uint32_t pointerId, uint8_t eventType) { + POINTER_TYPE_INFO *pointer_by_id(client_input_raw_t *raw, uint32_t pointerId, uint8_t eventType) { // Compact active touches into a single contiguous block perform_touch_compaction(raw); @@ -795,8 +778,7 @@ namespace platf { * @param x The normalized 0.0-1.0 X coordinate. * @param y The normalized 0.0-1.0 Y coordinate. */ - void - populate_common_pointer_info(POINTER_INFO &pointerInfo, const touch_port_t &touchPort, uint8_t eventType, float x, float y) { + void populate_common_pointer_info(POINTER_INFO &pointerInfo, const touch_port_t &touchPort, uint8_t eventType, float x, float y) { switch (eventType) { case LI_TOUCH_EVENT_HOVER: pointerInfo.pointerFlags &= ~POINTER_FLAG_INCONTACT; @@ -825,8 +807,7 @@ namespace platf { // we'll set POINTER_FLAG_UP, otherwise set POINTER_FLAG_UPDATE. if (pointerInfo.pointerFlags & POINTER_FLAG_INCONTACT) { pointerInfo.pointerFlags |= POINTER_FLAG_UP; - } - else { + } else { pointerInfo.pointerFlags |= POINTER_FLAG_UPDATE; } pointerInfo.pointerFlags &= ~(POINTER_FLAG_INCONTACT | POINTER_FLAG_INRANGE); @@ -857,8 +838,7 @@ namespace platf { * @brief Repeats the current touch state to avoid the interactions timing out. * @param raw The raw client-specific input context. */ - void - repeat_touch(client_input_raw_t *raw) { + void repeat_touch(client_input_raw_t *raw) { if (!inject_synthetic_pointer_input(raw->global, raw->touch, raw->touchInfo, raw->activeTouchSlots)) { auto err = GetLastError(); BOOST_LOG(warning) << "Failed to refresh virtual touch input: "sv << err; @@ -871,8 +851,7 @@ namespace platf { * @brief Repeats the current pen state to avoid the interactions timing out. * @param raw The raw client-specific input context. */ - void - repeat_pen(client_input_raw_t *raw) { + void repeat_pen(client_input_raw_t *raw) { if (!inject_synthetic_pointer_input(raw->global, raw->pen, &raw->penInfo, 1)) { auto err = GetLastError(); BOOST_LOG(warning) << "Failed to refresh virtual pen input: "sv << err; @@ -885,8 +864,7 @@ namespace platf { * @brief Cancels all active touches. * @param raw The raw client-specific input context. */ - void - cancel_all_active_touches(client_input_raw_t *raw) { + void cancel_all_active_touches(client_input_raw_t *raw) { // Cancel touch repeat callbacks if (raw->touchRepeatTask) { task_pool.cancel(raw->touchRepeatTask); @@ -922,8 +900,7 @@ namespace platf { * @param touch_port The current viewport for translating to screen coordinates. * @param touch The touch event. */ - void - touch_update(client_input_t *input, const touch_port_t &touch_port, const touch_input_t &touch) { + void touch_update(client_input_t *input, const touch_port_t &touch_port, const touch_input_t &touch) { auto raw = (client_input_raw_t *) input; // Bail if we're not running on an OS that supports virtual touch input @@ -944,8 +921,7 @@ namespace platf { BOOST_LOG(warning) << "Failed to create virtual touch device: "sv << err; return; } - } - else { + } else { // No need to cancel anything if we had no touch input device return; } @@ -991,8 +967,7 @@ namespace platf { // Convert the 0.0f..1.0f float to the 0..1024 range that Windows uses touchInfo.pressure = (UINT32) (touch.pressureOrDistance * 1024); - } - else { + } else { // The default touch pressure is 512 touchInfo.pressure = 512; } @@ -1018,8 +993,7 @@ namespace platf { touchInfo.touchMask |= TOUCH_MASK_CONTACTAREA; } - } - else { + } else { touchInfo.pressure = 0; touchInfo.rcContact = {}; } @@ -1027,8 +1001,7 @@ namespace platf { if (touch.rotation != LI_ROT_UNKNOWN) { touchInfo.touchMask |= TOUCH_MASK_ORIENTATION; touchInfo.orientation = touch.rotation; - } - else { + } else { touchInfo.orientation = 0; } @@ -1053,8 +1026,7 @@ namespace platf { * @param touch_port The current viewport for translating to screen coordinates. * @param pen The pen event. */ - void - pen_update(client_input_t *input, const touch_port_t &touch_port, const pen_input_t &pen) { + void pen_update(client_input_t *input, const touch_port_t &touch_port, const pen_input_t &pen) { auto raw = (client_input_raw_t *) input; // Bail if we're not running on an OS that supports virtual pen input @@ -1075,8 +1047,7 @@ namespace platf { BOOST_LOG(warning) << "Failed to create virtual pen device: "sv << err; return; } - } - else { + } else { // No need to cancel anything if we had no pen input device return; } @@ -1100,8 +1071,7 @@ namespace platf { // Windows only supports a single pen button, so send all buttons as the barrel button if (pen.penButtons) { penInfo.penFlags |= PEN_FLAG_BARREL; - } - else { + } else { penInfo.penFlags &= ~PEN_FLAG_BARREL; } @@ -1126,8 +1096,7 @@ namespace platf { // Convert the 0.0f..1.0f float to the 0..1024 range that Windows uses penInfo.pressure = (UINT32) (pen.pressureOrDistance * 1024); - } - else { + } else { // The default pen pressure is 0 penInfo.pressure = 0; } @@ -1135,8 +1104,7 @@ namespace platf { if (pen.rotation != LI_ROT_UNKNOWN) { penInfo.penMask |= PEN_MASK_ROTATION; penInfo.rotation = pen.rotation; - } - else { + } else { penInfo.rotation = 0; } @@ -1151,8 +1119,7 @@ namespace platf { penInfo.penMask |= PEN_MASK_TILT_X | PEN_MASK_TILT_Y; penInfo.tiltX = (INT32) (std::atan2(std::sin(-rotationRads) * r, z) * 180.f / M_PI); penInfo.tiltY = (INT32) (std::atan2(std::cos(-rotationRads) * r, z) * 180.f / M_PI); - } - else { + } else { penInfo.tiltX = 0; penInfo.tiltY = 0; } @@ -1172,8 +1139,7 @@ namespace platf { } } - void - unicode(input_t &input, char *utf8, int size) { + void unicode(input_t &input, char *utf8, int size) { // We can do no worse than one UTF-16 character per byte of UTF-8 WCHAR wide[size]; @@ -1201,8 +1167,7 @@ namespace platf { } } - int - alloc_gamepad(input_t &input, const gamepad_id_t &id, const gamepad_arrival_t &metadata, feedback_queue_t feedback_queue) { + int alloc_gamepad(input_t &input, const gamepad_id_t &id, const gamepad_arrival_t &metadata, feedback_queue_t feedback_queue) { auto raw = (input_raw_t *) input.get(); if (!raw->vigem) { @@ -1214,28 +1179,22 @@ namespace platf { if (config::input.gamepad == "x360"sv) { BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be Xbox 360 controller (manual selection)"sv; selectedGamepadType = Xbox360Wired; - } - else if (config::input.gamepad == "ds4"sv) { + } else if (config::input.gamepad == "ds4"sv) { BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be DualShock 4 controller (manual selection)"sv; selectedGamepadType = DualShock4Wired; - } - else if (metadata.type == LI_CTYPE_PS) { + } else if (metadata.type == LI_CTYPE_PS) { BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be DualShock 4 controller (auto-selected by client-reported type)"sv; selectedGamepadType = DualShock4Wired; - } - else if (metadata.type == LI_CTYPE_XBOX) { + } else if (metadata.type == LI_CTYPE_XBOX) { BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be Xbox 360 controller (auto-selected by client-reported type)"sv; selectedGamepadType = Xbox360Wired; - } - else if (config::input.motion_as_ds4 && (metadata.capabilities & (LI_CCAP_ACCEL | LI_CCAP_GYRO))) { + } else if (config::input.motion_as_ds4 && (metadata.capabilities & (LI_CCAP_ACCEL | LI_CCAP_GYRO))) { BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be DualShock 4 controller (auto-selected by motion sensor presence)"sv; selectedGamepadType = DualShock4Wired; - } - else if (config::input.touchpad_as_ds4 && (metadata.capabilities & LI_CCAP_TOUCHPAD)) { + } else if (config::input.touchpad_as_ds4 && (metadata.capabilities & LI_CCAP_TOUCHPAD)) { BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be DualShock 4 controller (auto-selected by touchpad presence)"sv; selectedGamepadType = DualShock4Wired; - } - else { + } else { BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be Xbox 360 controller (default)"sv; selectedGamepadType = Xbox360Wired; } @@ -1250,8 +1209,7 @@ namespace platf { if (metadata.capabilities & LI_CCAP_RGB_LED) { BOOST_LOG(warning) << "Gamepad " << id.globalIndex << " has an RGB LED, but it is not usable when emulating an Xbox 360 controller"sv; } - } - else if (selectedGamepadType == DualShock4Wired) { + } else if (selectedGamepadType == DualShock4Wired) { if (!(metadata.capabilities & (LI_CCAP_ACCEL | LI_CCAP_GYRO))) { BOOST_LOG(warning) << "Gamepad " << id.globalIndex << " is emulating a DualShock 4 controller, but the client gamepad doesn't have motion sensors active"sv; } @@ -1263,8 +1221,7 @@ namespace platf { return raw->vigem->alloc_gamepad_internal(id, feedback_queue, selectedGamepadType); } - void - free_gamepad(input_t &input, int nr) { + void free_gamepad(input_t &input, int nr) { auto raw = (input_raw_t *) input.get(); if (!raw->vigem) { @@ -1279,26 +1236,55 @@ namespace platf { * @param gamepad_state The gamepad button/axis state sent from the client. * @return XUSB_BUTTON flags. */ - static XUSB_BUTTON - x360_buttons(const gamepad_state_t &gamepad_state) { + static XUSB_BUTTON x360_buttons(const gamepad_state_t &gamepad_state) { int buttons {}; auto flags = gamepad_state.buttonFlags; - if (flags & DPAD_UP) buttons |= XUSB_GAMEPAD_DPAD_UP; - if (flags & DPAD_DOWN) buttons |= XUSB_GAMEPAD_DPAD_DOWN; - if (flags & DPAD_LEFT) buttons |= XUSB_GAMEPAD_DPAD_LEFT; - if (flags & DPAD_RIGHT) buttons |= XUSB_GAMEPAD_DPAD_RIGHT; - if (flags & START) buttons |= XUSB_GAMEPAD_START; - if (flags & BACK) buttons |= XUSB_GAMEPAD_BACK; - if (flags & LEFT_STICK) buttons |= XUSB_GAMEPAD_LEFT_THUMB; - if (flags & RIGHT_STICK) buttons |= XUSB_GAMEPAD_RIGHT_THUMB; - if (flags & LEFT_BUTTON) buttons |= XUSB_GAMEPAD_LEFT_SHOULDER; - if (flags & RIGHT_BUTTON) buttons |= XUSB_GAMEPAD_RIGHT_SHOULDER; - if (flags & (HOME | MISC_BUTTON)) buttons |= XUSB_GAMEPAD_GUIDE; - if (flags & A) buttons |= XUSB_GAMEPAD_A; - if (flags & B) buttons |= XUSB_GAMEPAD_B; - if (flags & X) buttons |= XUSB_GAMEPAD_X; - if (flags & Y) buttons |= XUSB_GAMEPAD_Y; + if (flags & DPAD_UP) { + buttons |= XUSB_GAMEPAD_DPAD_UP; + } + if (flags & DPAD_DOWN) { + buttons |= XUSB_GAMEPAD_DPAD_DOWN; + } + if (flags & DPAD_LEFT) { + buttons |= XUSB_GAMEPAD_DPAD_LEFT; + } + if (flags & DPAD_RIGHT) { + buttons |= XUSB_GAMEPAD_DPAD_RIGHT; + } + if (flags & START) { + buttons |= XUSB_GAMEPAD_START; + } + if (flags & BACK) { + buttons |= XUSB_GAMEPAD_BACK; + } + if (flags & LEFT_STICK) { + buttons |= XUSB_GAMEPAD_LEFT_THUMB; + } + if (flags & RIGHT_STICK) { + buttons |= XUSB_GAMEPAD_RIGHT_THUMB; + } + if (flags & LEFT_BUTTON) { + buttons |= XUSB_GAMEPAD_LEFT_SHOULDER; + } + if (flags & RIGHT_BUTTON) { + buttons |= XUSB_GAMEPAD_RIGHT_SHOULDER; + } + if (flags & (HOME | MISC_BUTTON)) { + buttons |= XUSB_GAMEPAD_GUIDE; + } + if (flags & A) { + buttons |= XUSB_GAMEPAD_A; + } + if (flags & B) { + buttons |= XUSB_GAMEPAD_B; + } + if (flags & X) { + buttons |= XUSB_GAMEPAD_X; + } + if (flags & Y) { + buttons |= XUSB_GAMEPAD_Y; + } return (XUSB_BUTTON) buttons; } @@ -1308,8 +1294,7 @@ namespace platf { * @param gamepad The gamepad to update. * @param gamepad_state The gamepad button/axis state sent from the client. */ - static void - x360_update_state(gamepad_context_t &gamepad, const gamepad_state_t &gamepad_state) { + static void x360_update_state(gamepad_context_t &gamepad, const gamepad_state_t &gamepad_state) { auto &report = gamepad.report.x360; report.wButtons = x360_buttons(gamepad_state); @@ -1321,17 +1306,14 @@ namespace platf { report.sThumbRY = gamepad_state.rsY; } - static DS4_DPAD_DIRECTIONS - ds4_dpad(const gamepad_state_t &gamepad_state) { + static DS4_DPAD_DIRECTIONS ds4_dpad(const gamepad_state_t &gamepad_state) { auto flags = gamepad_state.buttonFlags; if (flags & DPAD_UP) { if (flags & DPAD_RIGHT) { return DS4_BUTTON_DPAD_NORTHEAST; - } - else if (flags & DPAD_LEFT) { + } else if (flags & DPAD_LEFT) { return DS4_BUTTON_DPAD_NORTHWEST; - } - else { + } else { return DS4_BUTTON_DPAD_NORTH; } } @@ -1339,11 +1321,9 @@ namespace platf { else if (flags & DPAD_DOWN) { if (flags & DPAD_RIGHT) { return DS4_BUTTON_DPAD_SOUTHEAST; - } - else if (flags & DPAD_LEFT) { + } else if (flags & DPAD_LEFT) { return DS4_BUTTON_DPAD_SOUTHWEST; - } - else { + } else { return DS4_BUTTON_DPAD_SOUTH; } } @@ -1364,50 +1344,76 @@ namespace platf { * @param gamepad_state The gamepad button/axis state sent from the client. * @return DS4_BUTTONS flags. */ - static DS4_BUTTONS - ds4_buttons(const gamepad_state_t &gamepad_state) { + static DS4_BUTTONS ds4_buttons(const gamepad_state_t &gamepad_state) { int buttons {}; auto flags = gamepad_state.buttonFlags; - if (flags & LEFT_STICK) buttons |= DS4_BUTTON_THUMB_LEFT; - if (flags & RIGHT_STICK) buttons |= DS4_BUTTON_THUMB_RIGHT; - if (flags & LEFT_BUTTON) buttons |= DS4_BUTTON_SHOULDER_LEFT; - if (flags & RIGHT_BUTTON) buttons |= DS4_BUTTON_SHOULDER_RIGHT; - if (flags & START) buttons |= DS4_BUTTON_OPTIONS; - if (flags & BACK) buttons |= DS4_BUTTON_SHARE; - if (flags & A) buttons |= DS4_BUTTON_CROSS; - if (flags & B) buttons |= DS4_BUTTON_CIRCLE; - if (flags & X) buttons |= DS4_BUTTON_SQUARE; - if (flags & Y) buttons |= DS4_BUTTON_TRIANGLE; - - if (gamepad_state.lt > 0) buttons |= DS4_BUTTON_TRIGGER_LEFT; - if (gamepad_state.rt > 0) buttons |= DS4_BUTTON_TRIGGER_RIGHT; + if (flags & LEFT_STICK) { + buttons |= DS4_BUTTON_THUMB_LEFT; + } + if (flags & RIGHT_STICK) { + buttons |= DS4_BUTTON_THUMB_RIGHT; + } + if (flags & LEFT_BUTTON) { + buttons |= DS4_BUTTON_SHOULDER_LEFT; + } + if (flags & RIGHT_BUTTON) { + buttons |= DS4_BUTTON_SHOULDER_RIGHT; + } + if (flags & START) { + buttons |= DS4_BUTTON_OPTIONS; + } + if (flags & BACK) { + buttons |= DS4_BUTTON_SHARE; + } + if (flags & A) { + buttons |= DS4_BUTTON_CROSS; + } + if (flags & B) { + buttons |= DS4_BUTTON_CIRCLE; + } + if (flags & X) { + buttons |= DS4_BUTTON_SQUARE; + } + if (flags & Y) { + buttons |= DS4_BUTTON_TRIANGLE; + } + + if (gamepad_state.lt > 0) { + buttons |= DS4_BUTTON_TRIGGER_LEFT; + } + if (gamepad_state.rt > 0) { + buttons |= DS4_BUTTON_TRIGGER_RIGHT; + } return (DS4_BUTTONS) buttons; } - static DS4_SPECIAL_BUTTONS - ds4_special_buttons(const gamepad_state_t &gamepad_state) { + static DS4_SPECIAL_BUTTONS ds4_special_buttons(const gamepad_state_t &gamepad_state) { int buttons {}; - if (gamepad_state.buttonFlags & HOME) buttons |= DS4_SPECIAL_BUTTON_PS; + if (gamepad_state.buttonFlags & HOME) { + buttons |= DS4_SPECIAL_BUTTON_PS; + } // Allow either PS4/PS5 clickpad button or Xbox Series X share button to activate DS4 clickpad - if (gamepad_state.buttonFlags & (TOUCHPAD_BUTTON | MISC_BUTTON)) buttons |= DS4_SPECIAL_BUTTON_TOUCHPAD; + if (gamepad_state.buttonFlags & (TOUCHPAD_BUTTON | MISC_BUTTON)) { + buttons |= DS4_SPECIAL_BUTTON_TOUCHPAD; + } // Manual DS4 emulation: check if BACK button should also trigger DS4 touchpad click - if (config::input.gamepad == "ds4"sv && config::input.ds4_back_as_touchpad_click && (gamepad_state.buttonFlags & BACK)) buttons |= DS4_SPECIAL_BUTTON_TOUCHPAD; + if (config::input.gamepad == "ds4"sv && config::input.ds4_back_as_touchpad_click && (gamepad_state.buttonFlags & BACK)) { + buttons |= DS4_SPECIAL_BUTTON_TOUCHPAD; + } return (DS4_SPECIAL_BUTTONS) buttons; } - static std::uint8_t - to_ds4_triggerX(std::int16_t v) { + static std::uint8_t to_ds4_triggerX(std::int16_t v) { return (v + std::numeric_limits::max() / 2 + 1) / 257; } - static std::uint8_t - to_ds4_triggerY(std::int16_t v) { + static std::uint8_t to_ds4_triggerY(std::int16_t v) { auto new_v = -((std::numeric_limits::max() / 2 + v - 1)) / 257; return new_v == 0 ? 0xFF : (std::uint8_t) new_v; @@ -1418,8 +1424,7 @@ namespace platf { * @param gamepad The gamepad to update. * @param gamepad_state The gamepad button/axis state sent from the client. */ - static void - ds4_update_state(gamepad_context_t &gamepad, const gamepad_state_t &gamepad_state) { + static void ds4_update_state(gamepad_context_t &gamepad, const gamepad_state_t &gamepad_state) { auto &report = gamepad.report.ds4.Report; report.wButtons = static_cast(ds4_buttons(gamepad_state)) | static_cast(ds4_dpad(gamepad_state)); @@ -1441,8 +1446,7 @@ namespace platf { * @param vigem The global ViGEm context object. * @param nr The global gamepad index. */ - void - ds4_update_ts_and_send(vigem_t *vigem, int nr) { + void ds4_update_ts_and_send(vigem_t *vigem, int nr) { auto &gamepad = vigem->gamepads[nr]; // Cancel any pending updates. We will requeue one here when we're finished. @@ -1477,8 +1481,7 @@ namespace platf { * @param nr The gamepad index to update. * @param gamepad_state The gamepad button/axis state sent from the client. */ - void - gamepad_update(input_t &input, int nr, const gamepad_state_t &gamepad_state) { + void gamepad_update(input_t &input, int nr, const gamepad_state_t &gamepad_state) { auto vigem = ((input_raw_t *) input.get())->vigem; // If there is no gamepad support @@ -1499,8 +1502,7 @@ namespace platf { if (!VIGEM_SUCCESS(status)) { BOOST_LOG(warning) << "Couldn't send gamepad input to ViGEm ["sv << util::hex(status).to_string_view() << ']'; } - } - else { + } else { ds4_update_state(gamepad, gamepad_state); ds4_update_ts_and_send(vigem, nr); } @@ -1511,8 +1513,7 @@ namespace platf { * @param input The global input context. * @param touch The touch event. */ - void - gamepad_touch(input_t &input, const gamepad_touch_t &touch) { + void gamepad_touch(input_t &input, const gamepad_touch_t &touch) { auto vigem = ((input_raw_t *) input.get())->vigem; // If there is no gamepad support @@ -1542,8 +1543,7 @@ namespace platf { // Set pointer 0 down report.sCurrentTouch.bIsUpTrackingNum1 &= ~0x80; report.sCurrentTouch.bIsUpTrackingNum1++; - } - else if (gamepad.available_pointers & 0x2) { + } else if (gamepad.available_pointers & 0x2) { // Reserve pointer index 1 for this touch gamepad.pointer_id_map[touch.pointerId] = pointerIndex = 1; gamepad.available_pointers &= ~(1 << pointerIndex); @@ -1551,13 +1551,11 @@ namespace platf { // Set pointer 1 down report.sCurrentTouch.bIsUpTrackingNum2 &= ~0x80; report.sCurrentTouch.bIsUpTrackingNum2++; - } - else { + } else { BOOST_LOG(warning) << "No more free pointer indices! Did the client miss an touch up event?"sv; return; } - } - else if (touch.eventType == LI_TOUCH_EVENT_CANCEL_ALL) { + } else if (touch.eventType == LI_TOUCH_EVENT_CANCEL_ALL) { // Raise both pointers report.sCurrentTouch.bIsUpTrackingNum1 |= 0x80; report.sCurrentTouch.bIsUpTrackingNum2 |= 0x80; @@ -1567,8 +1565,7 @@ namespace platf { // All pointers are now available gamepad.available_pointers = 0x3; - } - else { + } else { auto i = gamepad.pointer_id_map.find(touch.pointerId); if (i == gamepad.pointer_id_map.end()) { BOOST_LOG(warning) << "Pointer ID not found! Did the client miss a touch down event?"sv; @@ -1584,15 +1581,13 @@ namespace platf { // Set pointer up if (pointerIndex == 0) { report.sCurrentTouch.bIsUpTrackingNum1 |= 0x80; - } - else { + } else { report.sCurrentTouch.bIsUpTrackingNum2 |= 0x80; } // Free the pointer index gamepad.available_pointers |= (1 << pointerIndex); - } - else if (touch.eventType != LI_TOUCH_EVENT_MOVE) { + } else if (touch.eventType != LI_TOUCH_EVENT_MOVE) { BOOST_LOG(warning) << "Unsupported touch event for gamepad: "sv << (uint32_t) touch.eventType; return; } @@ -1611,8 +1606,7 @@ namespace platf { if (touch.eventType != LI_TOUCH_EVENT_CANCEL_ALL) { if (pointerIndex == 0) { memcpy(report.sCurrentTouch.bTouchData1, touchData, sizeof(touchData)); - } - else { + } else { memcpy(report.sCurrentTouch.bTouchData2, touchData, sizeof(touchData)); } } @@ -1625,8 +1619,7 @@ namespace platf { * @param input The global input context. * @param motion The motion event. */ - void - gamepad_motion(input_t &input, const gamepad_motion_t &motion) { + void gamepad_motion(input_t &input, const gamepad_motion_t &motion) { auto vigem = ((input_raw_t *) input.get())->vigem; // If there is no gamepad support @@ -1653,8 +1646,7 @@ namespace platf { * @param input The global input context. * @param battery The battery event. */ - void - gamepad_battery(input_t &input, const gamepad_battery_t &battery) { + void gamepad_battery(input_t &input, const gamepad_battery_t &battery) { auto vigem = ((input_raw_t *) input.get())->vigem; // If there is no gamepad support @@ -1683,8 +1675,7 @@ namespace platf { case LI_BATTERY_STATE_DISCHARGING: if (battery.state == LI_BATTERY_STATE_CHARGING) { report.bBatteryLvlSpecial |= 0x10; // Connected via USB - } - else { + } else { report.bBatteryLvlSpecial &= ~0x10; // Not connected via USB } @@ -1723,20 +1714,18 @@ namespace platf { ds4_update_ts_and_send(vigem, battery.id.globalIndex); } - void - freeInput(void *p) { + void freeInput(void *p) { auto input = (input_raw_t *) p; delete input; } - std::vector & - supported_gamepads(input_t *input) { + std::vector &supported_gamepads(input_t *input) { if (!input) { static std::vector gps { - supported_gamepad_t { "auto", true, "" }, - supported_gamepad_t { "x360", false, "" }, - supported_gamepad_t { "ds4", false, "" }, + supported_gamepad_t {"auto", true, ""}, + supported_gamepad_t {"x360", false, ""}, + supported_gamepad_t {"ds4", false, ""}, }; return gps; @@ -1748,9 +1737,9 @@ namespace platf { // ds4 == ps4 static std::vector gps { - supported_gamepad_t { "auto", true, reason }, - supported_gamepad_t { "x360", enabled, reason }, - supported_gamepad_t { "ds4", enabled, reason } + supported_gamepad_t {"auto", true, reason}, + supported_gamepad_t {"x360", enabled, reason}, + supported_gamepad_t {"ds4", enabled, reason} }; for (auto &[name, is_enabled, reason_disabled] : gps) { @@ -1766,8 +1755,7 @@ namespace platf { * @brief Returns the supported platform capabilities to advertise to the client. * @return Capability flags. */ - platform_caps::caps_t - get_capabilities() { + platform_caps::caps_t get_capabilities() { platform_caps::caps_t caps = 0; // We support controller touchpad input as long as we're not emulating X360 @@ -1780,8 +1768,7 @@ namespace platf { if (config::input.native_pen_touch) { caps |= platform_caps::pen_touch; } - } - else { + } else { BOOST_LOG(warning) << "Touch input requires Windows 10 1809 or later"sv; } diff --git a/src/platform/windows/keylayout.h b/src/platform/windows/keylayout.h index 55dfa284e91..2d362ef2cf0 100644 --- a/src/platform/windows/keylayout.h +++ b/src/platform/windows/keylayout.h @@ -4,6 +4,7 @@ */ #pragma once +// standard includes #include #include diff --git a/src/platform/windows/misc.cpp b/src/platform/windows/misc.cpp index 419ae749261..c931b0ad175 100644 --- a/src/platform/windows/misc.cpp +++ b/src/platform/windows/misc.cpp @@ -2,12 +2,15 @@ * @file src/platform/windows/misc.cpp * @brief Miscellaneous definitions for Windows. */ +// standard includes #include #include #include +#include #include #include +// lib includes #include #include #include @@ -34,16 +37,14 @@ #define NTDDI_VERSION NTDDI_WIN10 #include +// local includes #include "misc.h" - +#include "nvprefs/nvprefs_interface.h" #include "src/entry_handler.h" #include "src/globals.h" #include "src/logging.h" #include "src/platform/common.h" #include "src/utility.h" -#include - -#include "nvprefs/nvprefs_interface.h" // UDP_SEND_MSG_SIZE was added in the Windows 10 20H1 SDK #ifndef UDP_SEND_MSG_SIZE @@ -63,16 +64,14 @@ #include extern "C" { -NTSTATUS NTAPI -NtSetTimerResolution(ULONG DesiredResolution, BOOLEAN SetResolution, PULONG CurrentResolution); + NTSTATUS NTAPI NtSetTimerResolution(ULONG DesiredResolution, BOOLEAN SetResolution, PULONG CurrentResolution); } namespace { std::atomic used_nt_set_timer_resolution = false; - bool - nt_set_timer_resolution_max() { + bool nt_set_timer_resolution_max() { ULONG minimum, maximum, current; if (!NT_SUCCESS(NtQueryTimerResolution(&minimum, &maximum, ¤t)) || !NT_SUCCESS(NtSetTimerResolution(maximum, TRUE, ¤t))) { @@ -81,8 +80,7 @@ namespace { return true; } - bool - nt_set_timer_resolution_min() { + bool nt_set_timer_resolution_min() { ULONG minimum, maximum, current; if (!NT_SUCCESS(NtQueryTimerResolution(&minimum, &maximum, ¤t)) || !NT_SUCCESS(NtSetTimerResolution(minimum, TRUE, ¤t))) { @@ -96,6 +94,7 @@ namespace { namespace bp = boost::process; using namespace std::literals; + namespace platf { using adapteraddrs_t = util::c_ptr; @@ -116,30 +115,26 @@ namespace platf { decltype(WlanEnumInterfaces) *fn_WlanEnumInterfaces = nullptr; decltype(WlanSetInterface) *fn_WlanSetInterface = nullptr; - std::filesystem::path - appdata() { + std::filesystem::path appdata() { WCHAR sunshine_path[MAX_PATH]; GetModuleFileNameW(NULL, sunshine_path, _countof(sunshine_path)); - return std::filesystem::path { sunshine_path }.remove_filename() / L"config"sv; + return std::filesystem::path {sunshine_path}.remove_filename() / L"config"sv; } - std::string - from_sockaddr(const sockaddr *const socket_address) { + std::string from_sockaddr(const sockaddr *const socket_address) { char data[INET6_ADDRSTRLEN] = {}; auto family = socket_address->sa_family; if (family == AF_INET6) { inet_ntop(AF_INET6, &((sockaddr_in6 *) socket_address)->sin6_addr, data, INET6_ADDRSTRLEN); - } - else if (family == AF_INET) { + } else if (family == AF_INET) { inet_ntop(AF_INET, &((sockaddr_in *) socket_address)->sin_addr, data, INET_ADDRSTRLEN); } - return std::string { data }; + return std::string {data}; } - std::pair - from_sockaddr_ex(const sockaddr *const ip_addr) { + std::pair from_sockaddr_ex(const sockaddr *const ip_addr) { char data[INET6_ADDRSTRLEN] = {}; auto family = ip_addr->sa_family; @@ -147,18 +142,16 @@ namespace platf { if (family == AF_INET6) { inet_ntop(AF_INET6, &((sockaddr_in6 *) ip_addr)->sin6_addr, data, INET6_ADDRSTRLEN); port = ((sockaddr_in6 *) ip_addr)->sin6_port; - } - else if (family == AF_INET) { + } else if (family == AF_INET) { inet_ntop(AF_INET, &((sockaddr_in *) ip_addr)->sin_addr, data, INET_ADDRSTRLEN); port = ((sockaddr_in *) ip_addr)->sin_port; } - return { port, std::string { data } }; + return {port, std::string {data}}; } - adapteraddrs_t - get_adapteraddrs() { - adapteraddrs_t info { nullptr }; + adapteraddrs_t get_adapteraddrs() { + adapteraddrs_t info {nullptr}; ULONG size = 0; while (GetAdaptersAddresses(AF_UNSPEC, 0, nullptr, info.get(), &size) == ERROR_BUFFER_OVERFLOW) { @@ -168,8 +161,7 @@ namespace platf { return info; } - std::string - get_mac_address(const std::string_view &address) { + std::string get_mac_address(const std::string_view &address) { adapteraddrs_t info = get_adapteraddrs(); for (auto adapter_pos = info.get(); adapter_pos != nullptr; adapter_pos = adapter_pos->Next) { for (auto addr_pos = adapter_pos->FirstUnicastAddress; addr_pos != nullptr; addr_pos = addr_pos->Next) { @@ -190,8 +182,7 @@ namespace platf { return "00:00:00:00:00:00"s; } - HDESK - syncThreadDesktop() { + HDESK syncThreadDesktop() { auto hDesk = OpenInputDesktop(DF_ALLOWOTHERACCOUNTHOOK, FALSE, GENERIC_ALL); if (!hDesk) { auto err = GetLastError(); @@ -210,23 +201,15 @@ namespace platf { return hDesk; } - void - print_status(const std::string_view &prefix, HRESULT status) { + void print_status(const std::string_view &prefix, HRESULT status) { char err_string[1024]; - DWORD bytes = FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, - nullptr, - status, - MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), - err_string, - sizeof(err_string), - nullptr); + DWORD bytes = FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, nullptr, status, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), err_string, sizeof(err_string), nullptr); - BOOST_LOG(error) << prefix << ": "sv << std::string_view { err_string, bytes }; + BOOST_LOG(error) << prefix << ": "sv << std::string_view {err_string, bytes}; } - bool - IsUserAdmin(HANDLE user_token) { + bool IsUserAdmin(HANDLE user_token) { WINBOOL ret; SID_IDENTIFIER_AUTHORITY NtAuthority = SECURITY_NT_AUTHORITY; PSID AdministratorsGroup; @@ -235,16 +218,21 @@ namespace platf { 2, SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS, - 0, 0, 0, 0, 0, 0, - &AdministratorsGroup); + 0, + 0, + 0, + 0, + 0, + 0, + &AdministratorsGroup + ); if (ret) { if (!CheckTokenMembership(user_token, AdministratorsGroup, &ret)) { ret = false; BOOST_LOG(error) << "Failed to verify token membership for administrative access: " << GetLastError(); } FreeSid(AdministratorsGroup); - } - else { + } else { BOOST_LOG(error) << "Unable to allocate SID to check administrative access: " << GetLastError(); } @@ -255,8 +243,7 @@ namespace platf { * @brief Obtain the current sessions user's primary token with elevated privileges. * @return The user's token. If user has admin capability it will be elevated, otherwise it will be a limited token. On error, `nullptr`. */ - HANDLE - retrieve_users_token(bool elevated) { + HANDLE retrieve_users_token(bool elevated) { DWORD consoleSessionId; HANDLE userToken; TOKEN_ELEVATION_TYPE elevationType; @@ -317,8 +304,7 @@ namespace platf { return userToken; } - bool - merge_user_environment_block(bp::environment &env, HANDLE shell_token) { + bool merge_user_environment_block(bp::environment &env, HANDLE shell_token) { // Get the target user's environment block PVOID env_block; if (!CreateEnvironmentBlock(&env_block, shell_token, FALSE)) { @@ -328,13 +314,14 @@ namespace platf { // Parse the environment block and populate env for (auto c = (PWCHAR) env_block; *c != UNICODE_NULL; c += wcslen(c) + 1) { // Environment variable entries end with a null-terminator, so std::wstring() will get an entire entry. - std::string env_tuple = to_utf8(std::wstring { c }); + std::string env_tuple = to_utf8(std::wstring {c}); std::string env_name = env_tuple.substr(0, env_tuple.find('=')); std::string env_val = env_tuple.substr(env_tuple.find('=') + 1); // Perform a case-insensitive search to see if this variable name already exists - auto itr = std::find_if(env.cbegin(), env.cend(), - [&](const auto &e) { return boost::iequals(e.get_name(), env_name); }); + auto itr = std::find_if(env.cbegin(), env.cend(), [&](const auto &e) { + return boost::iequals(e.get_name(), env_name); + }); if (itr != env.cend()) { // Use this existing name if it is already present to ensure we merge properly env_name = itr->get_name(); @@ -343,8 +330,7 @@ namespace platf { // For the PATH variable, we will merge the values together if (boost::iequals(env_name, "PATH")) { env[env_name] = env_val + ";" + env[env_name].to_string(); - } - else { + } else { // Other variables will be superseded by those in the user's environment block env[env_name] = env_val; } @@ -358,8 +344,7 @@ namespace platf { * @brief Check if the current process is running with system-level privileges. * @return `true` if the current process has system-level privileges, `false` otherwise. */ - bool - is_running_as_system() { + bool is_running_as_system() { BOOL ret; PSID SystemSid; DWORD dwSize = SECURITY_MAX_SID_SIZE; @@ -379,8 +364,7 @@ namespace platf { BOOST_LOG(error) << "Failed to check token membership: " << GetLastError(); ret = false; } - } - else { + } else { BOOST_LOG(error) << "Failed to create a SID for the local system account. This may happen if the system is out of memory or if the SID buffer is too small: " << GetLastError(); } @@ -390,14 +374,12 @@ namespace platf { } // Note: This does NOT append a null terminator - void - append_string_to_environment_block(wchar_t *env_block, int &offset, const std::wstring &wstr) { + void append_string_to_environment_block(wchar_t *env_block, int &offset, const std::wstring &wstr) { std::memcpy(&env_block[offset], wstr.data(), wstr.length() * sizeof(wchar_t)); offset += wstr.length(); } - std::wstring - create_environment_block(bp::environment &env) { + std::wstring create_environment_block(bp::environment &env) { int size = 0; for (const auto &entry : env) { auto name = entry.get_name(); @@ -426,8 +408,7 @@ namespace platf { return std::wstring(env_block, offset); } - LPPROC_THREAD_ATTRIBUTE_LIST - allocate_proc_thread_attr_list(DWORD attribute_count) { + LPPROC_THREAD_ATTRIBUTE_LIST allocate_proc_thread_attr_list(DWORD attribute_count) { SIZE_T size; InitializeProcThreadAttributeList(NULL, attribute_count, 0, &size); @@ -444,8 +425,7 @@ namespace platf { return list; } - void - free_proc_thread_attr_list(LPPROC_THREAD_ATTRIBUTE_LIST list) { + void free_proc_thread_attr_list(LPPROC_THREAD_ATTRIBUTE_LIST list) { DeleteProcThreadAttributeList(list); HeapFree(GetProcessHeap(), 0, list); } @@ -458,8 +438,7 @@ namespace platf { * @param process_info A reference to a `PROCESS_INFORMATION` structure that contains information about the new process. * @return A `bp::child` object representing the new process, or an empty `bp::child` object if the launch failed. */ - bp::child - create_boost_child_from_results(bool process_launched, const std::string &cmd, std::error_code &ec, PROCESS_INFORMATION &process_info) { + bp::child create_boost_child_from_results(bool process_launched, const std::string &cmd, std::error_code &ec, PROCESS_INFORMATION &process_info) { // Use RAII to ensure the process is closed when we're done with it, even if there was an error. auto close_process_handles = util::fail_guard([process_launched, process_info]() { if (process_launched) { @@ -478,8 +457,7 @@ namespace platf { auto child = bp::child((bp::pid_t) process_info.dwProcessId); BOOST_LOG(info) << cmd << " running with PID "sv << child.id(); return child; - } - else { + } else { auto winerror = GetLastError(); BOOST_LOG(error) << "Failed to launch process: "sv << winerror; ec = std::make_error_code(std::errc::invalid_argument); @@ -496,8 +474,7 @@ namespace platf { * @param callback A function that will be executed while impersonating the user. * @return Object that will store any error that occurred during the impersonation */ - std::error_code - impersonate_current_user(HANDLE user_token, std::function callback) { + std::error_code impersonate_current_user(HANDLE user_token, std::function callback) { std::error_code ec; // Impersonate the user when launching the process. This will ensure that appropriate access // checks are done against the user token, not our SYSTEM token. It will also allow network @@ -534,8 +511,7 @@ namespace platf { * @param ec A reference to a `std::error_code` object that will store any error that occurred during the creation of the structure. * @return A structure that contains information about how to launch the new process. */ - STARTUPINFOEXW - create_startup_info(FILE *file, HANDLE *job, std::error_code &ec) { + STARTUPINFOEXW create_startup_info(FILE *file, HANDLE *job, std::error_code &ec) { // Initialize a zeroed-out STARTUPINFOEXW structure and set its size STARTUPINFOEXW startup_info = {}; startup_info.StartupInfo.cb = sizeof(startup_info); @@ -563,13 +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), NULL, NULL); } if (job) { @@ -577,13 +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), NULL, NULL); } return startup_info; @@ -594,8 +558,7 @@ namespace platf { * @param token The primary token identifying the user to use, or `NULL` to restore original keys. * @return `true` if the override or restore operation was successful. */ - bool - override_per_user_predefined_keys(HANDLE token) { + bool override_per_user_predefined_keys(HANDLE token) { HKEY user_classes_root = NULL; if (token) { auto err = RegOpenUserClassesRoot(token, 0, GENERIC_ALL, &user_classes_root); @@ -651,8 +614,7 @@ namespace platf { * @param argument The raw argument to process. * @return An argument string suitable for use by CreateProcess(). */ - std::wstring - escape_argument(const std::wstring &argument) { + std::wstring escape_argument(const std::wstring &argument) { // If there are no characters requiring quoting/escaping, we're done if (argument.find_first_of(L" \t\n\v\"") == argument.npos) { return argument; @@ -672,11 +634,9 @@ namespace platf { if (it == argument.end()) { escaped_arg.append(backslash_count * 2, L'\\'); break; - } - else if (*it == L'"') { + } else if (*it == L'"') { escaped_arg.append(backslash_count * 2 + 1, L'\\'); - } - else { + } else { escaped_arg.append(backslash_count, L'\\'); } @@ -691,8 +651,7 @@ namespace platf { * @param argument An argument already escaped by `escape_argument()`. * @return An argument string suitable for use by cmd.exe. */ - std::wstring - escape_argument_for_cmd(const std::wstring &argument) { + std::wstring escape_argument_for_cmd(const std::wstring &argument) { // Start with the original string and modify from there std::wstring escaped_arg = argument; @@ -716,8 +675,7 @@ namespace platf { * @param creation_flags The creation flags for CreateProcess(), which may be modified by this function. * @return A command string suitable for use by CreateProcess(). */ - std::wstring - resolve_command_string(const std::string &raw_cmd, const std::wstring &working_dir, HANDLE token, DWORD &creation_flags) { + std::wstring resolve_command_string(const std::string &raw_cmd, const std::wstring &working_dir, HANDLE token, DWORD &creation_flags) { std::wstring raw_cmd_w = from_utf8(raw_cmd); // First, convert the given command into parts so we can get the executable/file/URL without parameters @@ -744,16 +702,14 @@ namespace platf { // If the target is a URL, the class is found using the URL scheme (prior to and not including the ':') lookup_string = scheme.data(); - } - else { + } else { // If the target is not a URL, assume it's a regular file path auto extension = PathFindExtensionW(raw_target.c_str()); if (extension == nullptr || *extension == 0) { // If the file has no extension, assume it's a command and allow CreateProcess() // to try to find it via PATH return from_utf8(raw_cmd); - } - else if (boost::iequals(extension, L".exe")) { + } else if (boost::iequals(extension, L".exe")) { // If the file has an .exe extension, we will bypass the resolution here and // directly pass the unmodified command string to CreateProcess(). The argument // escaping rules are subtly different between CreateProcess() and ShellExecute(), @@ -814,7 +770,7 @@ namespace platf { // uncommon ones that are unsupported here. // // https://web.archive.org/web/20111002101214/http://msdn.microsoft.com/en-us/library/windows/desktop/cc144101(v=vs.85).aspx - std::wstring cmd_string { shell_command_string.data() }; + std::wstring cmd_string {shell_command_string.data()}; size_t match_pos = 0; while ((match_pos = cmd_string.find_first_of(L'%', match_pos)) != std::wstring::npos) { std::wstring match_replacement; @@ -843,19 +799,20 @@ namespace platf { case L'6': case L'7': case L'8': - case L'9': { - // Arguments numbers are 1-based, except for %0 which is equivalent to %1 - int index = next_char - L'0'; - if (next_char != L'0') { - index--; - } + case L'9': + { + // Arguments numbers are 1-based, except for %0 which is equivalent to %1 + int index = next_char - L'0'; + if (next_char != L'0') { + index--; + } - // Replace with the matching argument, or nothing if the index is invalid - if (index < raw_cmd_parts.size()) { - match_replacement = raw_cmd_parts.at(index); + // Replace with the matching argument, or nothing if the index is invalid + if (index < raw_cmd_parts.size()) { + match_replacement = raw_cmd_parts.at(index); + } + break; } - break; - } // All arguments following the target case L'*': @@ -878,29 +835,29 @@ namespace platf { // Long file path of target case L'l': case L'd': - case L'v': { - std::array path; - std::array other_dirs { working_dir.c_str(), nullptr }; - - // PathFindOnPath() is a little gross because it uses the same - // buffer for input and output, so we need to copy our input - // into the path array. - std::wcsncpy(path.data(), raw_target.c_str(), path.size()); - if (path[path.size() - 1] != 0) { - // The path was so long it was truncated by this copy. We'll - // assume it was an absolute path (likely) and use it unmodified. - match_replacement = raw_target; - } - // See if we can find the path on our search path or working directory - else if (PathFindOnPathW(path.data(), other_dirs.data())) { - match_replacement = std::wstring { path.data() }; - } - else { - // We couldn't find the target, so we'll just hope for the best - match_replacement = raw_target; + case L'v': + { + std::array path; + std::array other_dirs {working_dir.c_str(), nullptr}; + + // PathFindOnPath() is a little gross because it uses the same + // buffer for input and output, so we need to copy our input + // into the path array. + std::wcsncpy(path.data(), raw_target.c_str(), path.size()); + if (path[path.size() - 1] != 0) { + // The path was so long it was truncated by this copy. We'll + // assume it was an absolute path (likely) and use it unmodified. + match_replacement = raw_target; + } + // See if we can find the path on our search path or working directory + else if (PathFindOnPathW(path.data(), other_dirs.data())) { + match_replacement = std::wstring {path.data()}; + } else { + // We couldn't find the target, so we'll just hope for the best + match_replacement = raw_target; + } + break; } - break; - } // Working directory case L'w': @@ -938,8 +895,7 @@ namespace platf { * @param group A pointer to a `bp::group` object to which the new process should belong (may be `nullptr`). * @return A `bp::child` object representing the new process, or an empty `bp::child` object if the launch fails. */ - bp::child - run_command(bool elevated, bool interactive, const std::string &cmd, boost::filesystem::path &working_dir, const bp::environment &env, FILE *file, std::error_code &ec, bp::group *group) { + bp::child run_command(bool elevated, bool interactive, const std::string &cmd, boost::filesystem::path &working_dir, const bp::environment &env, FILE *file, std::error_code &ec, bp::group *group) { std::wstring start_dir = from_utf8(working_dir.string()); HANDLE job = group ? group->native_handle() : nullptr; STARTUPINFOEXW startup_info = create_startup_info(file, job ? &job : nullptr, ec); @@ -967,9 +923,11 @@ namespace platf { // Find the PATH variable in our environment block using a case-insensitive search auto sunshine_wenv = boost::this_process::wenvironment(); - std::wstring path_var_name { L"PATH" }; + std::wstring path_var_name {L"PATH"}; std::wstring old_path_val; - auto itr = std::find_if(sunshine_wenv.cbegin(), sunshine_wenv.cend(), [&](const auto &e) { return boost::iequals(e.get_name(), path_var_name); }); + auto itr = std::find_if(sunshine_wenv.cbegin(), sunshine_wenv.cend(), [&](const auto &e) { + return boost::iequals(e.get_name(), path_var_name); + }); if (itr != sunshine_wenv.cend()) { // Use the existing variable if it exists, since Boost treats these as case-sensitive. path_var_name = itr->get_name(); @@ -984,8 +942,7 @@ namespace platf { auto restore_path = util::fail_guard([&]() { if (old_path_val.empty()) { sunshine_wenv[path_var_name].clear(); - } - else { + } else { sunshine_wenv[path_var_name].assign(old_path_val); } }); @@ -1015,17 +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, 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); }); } // Otherwise, launch the process using CreateProcessW() @@ -1049,16 +996,7 @@ 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); + 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); } // Use the results of the launch to create a bp::child object @@ -1069,8 +1007,7 @@ namespace platf { * @brief Open a url in the default web browser. * @param url The url to open. */ - void - open_url(const std::string &url) { + void open_url(const std::string &url) { boost::process::v1::environment _env = boost::this_process::environment(); auto working_dir = boost::filesystem::path(); std::error_code ec; @@ -1078,15 +1015,13 @@ namespace platf { auto child = run_command(false, false, url, working_dir, _env, nullptr, ec, nullptr); if (ec) { BOOST_LOG(warning) << "Couldn't open url ["sv << url << "]: System: "sv << ec.message(); - } - else { + } else { BOOST_LOG(info) << "Opened url ["sv << url << "]"sv; child.detach(); } } - void - adjust_thread_priority(thread_priority_e priority) { + void adjust_thread_priority(thread_priority_e priority) { int win32_priority; switch (priority) { @@ -1113,8 +1048,7 @@ namespace platf { } } - void - streaming_will_start() { + void streaming_will_start() { static std::once_flag load_wlanapi_once_flag; std::call_once(load_wlanapi_once_flag, []() { // wlanapi.dll is not installed by default on Windows Server, so we load it dynamically @@ -1150,8 +1084,7 @@ namespace platf { // Reduce timer period to 0.5ms if (nt_set_timer_resolution_max()) { used_nt_set_timer_resolution = true; - } - else { + } else { BOOST_LOG(error) << "NtSetTimerResolution() failed, falling back to timeBeginPeriod()"; timeBeginPeriod(1); used_nt_set_timer_resolution = false; @@ -1186,8 +1119,7 @@ namespace platf { // https://docs.microsoft.com/en-us/windows-hardware/drivers/network/oid-wdi-set-connection-quality // https://docs.microsoft.com/en-us/previous-versions/windows/hardware/wireless/native-802-11-media-streaming BOOL value = TRUE; - auto error = fn_WlanSetInterface(wlan_handle, &wlan_interface_list->InterfaceInfo[i].InterfaceGuid, - wlan_intf_opcode_media_streaming_mode, sizeof(value), &value, nullptr); + auto error = fn_WlanSetInterface(wlan_handle, &wlan_interface_list->InterfaceInfo[i].InterfaceGuid, wlan_intf_opcode_media_streaming_mode, sizeof(value), &value, nullptr); if (error == ERROR_SUCCESS) { BOOST_LOG(info) << "WLAN interface "sv << i << " is now in low latency mode"sv; } @@ -1195,8 +1127,7 @@ namespace platf { } fn_WlanFreeMemory(wlan_interface_list); - } - else { + } else { fn_WlanCloseHandle(wlan_handle, nullptr); wlan_handle = NULL; } @@ -1220,21 +1151,18 @@ namespace platf { if (SystemParametersInfoW(SPI_SETMOUSEKEYS, 0, &new_mouse_keys_state, 0)) { // Remember to restore the previous settings when we stop streaming enabled_mouse_keys = true; - } - else { + } else { auto winerr = GetLastError(); BOOST_LOG(warning) << "Unable to enable Mouse Keys: "sv << winerr; } - } - else { + } else { auto winerr = GetLastError(); BOOST_LOG(warning) << "Unable to get current state of Mouse Keys: "sv << winerr; } } } - void - streaming_will_stop() { + void streaming_will_stop() { // Demote ourselves back to normal priority class SetPriorityClass(GetCurrentProcess(), NORMAL_PRIORITY_CLASS); @@ -1244,8 +1172,7 @@ namespace platf { if (!nt_set_timer_resolution_min()) { BOOST_LOG(error) << "nt_set_timer_resolution_min() failed even though nt_set_timer_resolution_max() succeeded"; } - } - else { + } else { timeEndPeriod(1); } @@ -1268,8 +1195,7 @@ namespace platf { } } - void - restart_on_exit() { + void restart_on_exit() { STARTUPINFOEXW startup_info {}; startup_info.StartupInfo.cb = sizeof(startup_info); @@ -1281,16 +1207,7 @@ namespace platf { } PROCESS_INFORMATION process_info; - if (!CreateProcessW(executable, - GetCommandLineW(), - nullptr, - nullptr, - false, - CREATE_UNICODE_ENVIRONMENT | EXTENDED_STARTUPINFO_PRESENT, - nullptr, - nullptr, - (LPSTARTUPINFOW) &startup_info, - &process_info)) { + if (!CreateProcessW(executable, GetCommandLineW(), nullptr, nullptr, false, CREATE_UNICODE_ENVIRONMENT | EXTENDED_STARTUPINFO_PRESENT, nullptr, nullptr, (LPSTARTUPINFOW) &startup_info, &process_info)) { auto winerr = GetLastError(); BOOST_LOG(fatal) << "Unable to restart Sunshine: "sv << winerr; return; @@ -1300,8 +1217,7 @@ namespace platf { CloseHandle(process_info.hThread); } - void - restart() { + 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) { @@ -1313,13 +1229,11 @@ namespace platf { lifetime::exit_sunshine(0, true); } - int - set_env(const std::string &name, const std::string &value) { + int set_env(const std::string &name, const std::string &value) { return _putenv_s(name.c_str(), value.c_str()); } - int - unset_env(const std::string &name) { + int unset_env(const std::string &name) { return _putenv_s(name.c_str(), ""); } @@ -1328,8 +1242,7 @@ namespace platf { bool requested_exit; }; - static BOOL CALLBACK - prgrp_enum_windows(HWND hwnd, LPARAM lParam) { + static BOOL CALLBACK prgrp_enum_windows(HWND hwnd, LPARAM lParam) { auto enum_ctx = (enum_wnd_context_t *) lParam; // Find the owner PID of this window @@ -1345,8 +1258,7 @@ namespace platf { if (SendNotifyMessageW(hwnd, WM_CLOSE, 0, 0)) { BOOST_LOG(debug) << "Sent WM_CLOSE to PID: "sv << wnd_process_id; enum_ctx->requested_exit = true; - } - else { + } else { auto error = GetLastError(); BOOST_LOG(warning) << "Failed to send WM_CLOSE to PID ["sv << wnd_process_id << "]: " << error; } @@ -1356,8 +1268,7 @@ namespace platf { return TRUE; } - bool - request_process_group_exit(std::uintptr_t native_handle) { + bool request_process_group_exit(std::uintptr_t native_handle) { auto job_handle = (HANDLE) native_handle; // Get list of all processes in our job object @@ -1367,8 +1278,7 @@ namespace platf { auto fg = util::fail_guard([&process_id_list]() { free(process_id_list); }); - while (!(success = QueryInformationJobObject(job_handle, JobObjectBasicProcessIdList, - process_id_list, required_length, &required_length)) && + while (!(success = QueryInformationJobObject(job_handle, JobObjectBasicProcessIdList, process_id_list, required_length, &required_length)) && GetLastError() == ERROR_MORE_DATA) { free(process_id_list); process_id_list = (PJOBOBJECT_BASIC_PROCESS_ID_LIST) calloc(1, required_length); @@ -1381,8 +1291,7 @@ namespace platf { auto err = GetLastError(); BOOST_LOG(warning) << "Failed to enumerate processes in group: "sv << err; return false; - } - else if (process_id_list->NumberOfProcessIdsInList == 0) { + } else if (process_id_list->NumberOfProcessIdsInList == 0) { // If all processes are already dead, treat it as a success return true; } @@ -1400,8 +1309,7 @@ namespace platf { return enum_ctx.requested_exit; } - bool - process_group_running(std::uintptr_t native_handle) { + bool process_group_running(std::uintptr_t native_handle) { JOBOBJECT_BASIC_ACCOUNTING_INFORMATION accounting_info; if (!QueryInformationJobObject((HANDLE) native_handle, JobObjectBasicAccountingInformation, &accounting_info, sizeof(accounting_info), nullptr)) { @@ -1413,8 +1321,7 @@ namespace platf { return accounting_info.ActiveProcesses != 0; } - SOCKADDR_IN - to_sockaddr(boost::asio::ip::address_v4 address, uint16_t port) { + SOCKADDR_IN to_sockaddr(boost::asio::ip::address_v4 address, uint16_t port) { SOCKADDR_IN saddr_v4 = {}; saddr_v4.sin_family = AF_INET; @@ -1426,8 +1333,7 @@ namespace platf { return saddr_v4; } - SOCKADDR_IN6 - to_sockaddr(boost::asio::ip::address_v6 address, uint16_t port) { + SOCKADDR_IN6 to_sockaddr(boost::asio::ip::address_v6 address, uint16_t port) { SOCKADDR_IN6 saddr_v6 = {}; saddr_v6.sin6_family = AF_INET6; @@ -1442,8 +1348,7 @@ namespace platf { // Use UDP segmentation offload if it is supported by the OS. If the NIC is capable, this will use // hardware acceleration to reduce CPU usage. Support for USO was introduced in Windows 10 20H1. - bool - send_batch(batched_send_info_t &send_info) { + bool send_batch(batched_send_info_t &send_info) { WSAMSG msg; // Convert the target address into a SOCKADDR @@ -1454,8 +1359,7 @@ namespace platf { msg.name = (PSOCKADDR) &taddr_v6; msg.namelen = sizeof(taddr_v6); - } - else { + } else { taddr_v4 = to_sockaddr(send_info.target_address.to_v4(), send_info.target_port); msg.name = (PSOCKADDR) &taddr_v4; @@ -1477,8 +1381,7 @@ namespace platf { bufs[bufcount].len = send_info.payload_size; bufcount++; } - } - else { + } else { // Translate buffer descriptors into WSABUFs auto payload_offset = send_info.block_offset * send_info.payload_size; auto payload_length = payload_offset + (send_info.block_count * send_info.payload_size); @@ -1496,8 +1399,7 @@ namespace platf { msg.dwFlags = 0; // At most, one DWORD option and one PKTINFO option - char cmbuf[WSA_CMSG_SPACE(sizeof(DWORD)) + - std::max(WSA_CMSG_SPACE(sizeof(IN6_PKTINFO)), WSA_CMSG_SPACE(sizeof(IN_PKTINFO)))] = {}; + char cmbuf[WSA_CMSG_SPACE(sizeof(DWORD)) + std::max(WSA_CMSG_SPACE(sizeof(IN6_PKTINFO)), WSA_CMSG_SPACE(sizeof(IN_PKTINFO)))] = {}; ULONG cmbuflen = 0; msg.Control.buf = cmbuf; @@ -1517,8 +1419,7 @@ namespace platf { cm->cmsg_type = IPV6_PKTINFO; cm->cmsg_len = WSA_CMSG_LEN(sizeof(pktInfo)); memcpy(WSA_CMSG_DATA(cm), &pktInfo, sizeof(pktInfo)); - } - else { + } else { IN_PKTINFO pktInfo; SOCKADDR_IN saddr_v4 = to_sockaddr(send_info.source_address.to_v4(), 0); @@ -1550,8 +1451,7 @@ namespace platf { return WSASendMsg((SOCKET) send_info.native_socket, &msg, 0, &bytes_sent, nullptr, nullptr) != SOCKET_ERROR; } - bool - send(send_info_t &send_info) { + bool send(send_info_t &send_info) { WSAMSG msg; // Convert the target address into a SOCKADDR @@ -1562,8 +1462,7 @@ namespace platf { msg.name = (PSOCKADDR) &taddr_v6; msg.namelen = sizeof(taddr_v6); - } - else { + } else { taddr_v4 = to_sockaddr(send_info.target_address.to_v4(), send_info.target_port); msg.name = (PSOCKADDR) &taddr_v4; @@ -1605,8 +1504,7 @@ namespace platf { cm->cmsg_type = IPV6_PKTINFO; cm->cmsg_len = WSA_CMSG_LEN(sizeof(pktInfo)); memcpy(WSA_CMSG_DATA(cm), &pktInfo, sizeof(pktInfo)); - } - else { + } else { IN_PKTINFO pktInfo; SOCKADDR_IN saddr_v4 = to_sockaddr(send_info.source_address.to_v4(), 0); @@ -1636,7 +1534,8 @@ namespace platf { class qos_t: public deinit_t { public: qos_t(QOS_FLOWID flow_id): - flow_id(flow_id) {} + flow_id(flow_id) { + } virtual ~qos_t() { if (!fn_QOSRemoveSocketFromFlow(qos_handle, (SOCKET) NULL, flow_id, 0)) { @@ -1657,8 +1556,7 @@ namespace platf { * @param data_type The type of traffic sent on this socket. * @param dscp_tagging Specifies whether to enable DSCP tagging on outgoing traffic. */ - std::unique_ptr - enable_socket_qos(uintptr_t native_socket, boost::asio::ip::address &address, uint16_t port, qos_data_type_e data_type, bool dscp_tagging) { + std::unique_ptr enable_socket_qos(uintptr_t native_socket, boost::asio::ip::address &address, uint16_t port, qos_data_type_e data_type, bool dscp_tagging) { SOCKADDR_IN saddr_v4; SOCKADDR_IN6 saddr_v6; PSOCKADDR dest_addr; @@ -1693,7 +1591,7 @@ namespace platf { return; } - QOS_VERSION qos_version { 1, 0 }; + QOS_VERSION qos_version {1, 0}; if (!fn_QOSCreateHandle(&qos_version, &qos_handle)) { auto winerr = GetLastError(); BOOST_LOG(warning) << "QOSCreateHandle() failed: "sv << winerr; @@ -1733,15 +1631,13 @@ namespace platf { if (connect((SOCKET) native_socket, (PSOCKADDR) &saddr_v6, sizeof(saddr_v6)) < 0) { auto wsaerr = WSAGetLastError(); BOOST_LOG(error) << "qWAVE dual-stack workaround failed: "sv << wsaerr; - } - else { + } else { BOOST_LOG(debug) << "Using qWAVE connect() workaround for QoS tagging"sv; using_connect_hack = true; dest_addr = nullptr; } } - } - else { + } else { saddr_v4 = to_sockaddr(address.to_v4(), port); dest_addr = (PSOCKADDR) &saddr_v4; } @@ -1768,15 +1664,16 @@ namespace platf { return std::make_unique(flow_id); } - int64_t - qpc_counter() { + + int64_t qpc_counter() { LARGE_INTEGER performance_counter; - if (QueryPerformanceCounter(&performance_counter)) return performance_counter.QuadPart; + if (QueryPerformanceCounter(&performance_counter)) { + return performance_counter.QuadPart; + } return 0; } - std::chrono::nanoseconds - qpc_time_difference(int64_t performance_counter1, int64_t performance_counter2) { + std::chrono::nanoseconds qpc_time_difference(int64_t performance_counter1, int64_t performance_counter2) { auto get_frequency = []() { LARGE_INTEGER frequency; frequency.QuadPart = 0; @@ -1790,8 +1687,7 @@ namespace platf { return {}; } - std::wstring - from_utf8(const std::string &string) { + std::wstring from_utf8(const std::string &string) { // No conversion needed if the string is empty if (string.empty()) { return {}; @@ -1817,16 +1713,14 @@ namespace platf { return output; } - std::string - to_utf8(const std::wstring &string) { + std::string to_utf8(const std::wstring &string) { // No conversion needed if the string is empty if (string.empty()) { return {}; } // Get the output size required to store the string - auto output_size = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, string.data(), string.size(), - nullptr, 0, nullptr, nullptr); + auto output_size = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, string.data(), string.size(), nullptr, 0, nullptr, nullptr); if (output_size == 0) { auto winerr = GetLastError(); BOOST_LOG(error) << "Failed to get UTF-8 buffer size: "sv << winerr; @@ -1835,8 +1729,7 @@ namespace platf { // Perform the conversion std::string output(output_size, '\0'); - output_size = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, string.data(), string.size(), - output.data(), output.size(), nullptr, nullptr); + output_size = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, string.data(), string.size(), output.data(), output.size(), nullptr, nullptr); if (output_size == 0) { auto winerr = GetLastError(); BOOST_LOG(error) << "Failed to convert string to UTF-8: "sv << winerr; @@ -1846,8 +1739,7 @@ namespace platf { return output; } - std::string - get_host_name() { + std::string get_host_name() { WCHAR hostname[256]; if (GetHostNameW(hostname, ARRAYSIZE(hostname)) == SOCKET_ERROR) { BOOST_LOG(error) << "GetHostNameW() failed: "sv << WSAGetLastError(); @@ -1870,11 +1762,12 @@ namespace platf { } ~win32_high_precision_timer() { - if (timer) CloseHandle(timer); + if (timer) { + CloseHandle(timer); + } } - void - sleep_for(const std::chrono::nanoseconds &duration) override { + void sleep_for(const std::chrono::nanoseconds &duration) override { if (!timer) { BOOST_LOG(error) << "Attempting high_precision_timer::sleep_for() with uninitialized timer"; return; @@ -1902,8 +1795,7 @@ namespace platf { HANDLE timer = NULL; }; - std::unique_ptr - create_high_precision_timer() { + std::unique_ptr create_high_precision_timer() { return std::make_unique(); } } // namespace platf diff --git a/src/platform/windows/misc.h b/src/platform/windows/misc.h index b045104f57b..30d85376604 100644 --- a/src/platform/windows/misc.h +++ b/src/platform/windows/misc.h @@ -4,36 +4,33 @@ */ #pragma once +// standard includes #include #include + +// platform includes #include #include namespace platf { - void - print_status(const std::string_view &prefix, HRESULT status); - HDESK - syncThreadDesktop(); + void print_status(const std::string_view &prefix, HRESULT status); + HDESK syncThreadDesktop(); - int64_t - qpc_counter(); + int64_t qpc_counter(); - std::chrono::nanoseconds - qpc_time_difference(int64_t performance_counter1, int64_t performance_counter2); + std::chrono::nanoseconds qpc_time_difference(int64_t performance_counter1, int64_t performance_counter2); /** * @brief Convert a UTF-8 string into a UTF-16 wide string. * @param string The UTF-8 string. * @return The converted UTF-16 wide string. */ - std::wstring - from_utf8(const std::string &string); + std::wstring from_utf8(const std::string &string); /** * @brief Convert a UTF-16 wide string into a UTF-8 string. * @param string The UTF-16 wide string. * @return The converted UTF-8 string. */ - std::string - to_utf8(const std::wstring &string); + std::string to_utf8(const std::wstring &string); } // namespace platf diff --git a/src/platform/windows/nvprefs/driver_settings.cpp b/src/platform/windows/nvprefs/driver_settings.cpp index 2cea9fa4c58..57aa809fde0 100644 --- a/src/platform/windows/nvprefs/driver_settings.cpp +++ b/src/platform/windows/nvprefs/driver_settings.cpp @@ -2,8 +2,10 @@ * @file src/platform/windows/nvprefs/driver_settings.cpp * @brief Definitions for nvidia driver settings. */ -// local includes +// this include #include "driver_settings.h" + +// local includes #include "nvprefs_common.h" namespace { @@ -11,15 +13,13 @@ namespace { const auto sunshine_application_profile_name = L"SunshineStream"; const auto sunshine_application_path = L"sunshine.exe"; - void - nvapi_error_message(NvAPI_Status status) { + void nvapi_error_message(NvAPI_Status status) { NvAPI_ShortString message = {}; NvAPI_GetErrorMessage(status, message); nvprefs::error_message(std::string("NvAPI error: ") + message); } - void - fill_nvapi_string(NvAPI_UnicodeString &dest, const wchar_t *src) { + void fill_nvapi_string(NvAPI_UnicodeString &dest, const wchar_t *src) { static_assert(sizeof(NvU16) == sizeof(wchar_t)); memcpy_s(dest, NVAPI_UNICODE_STRING_MAX * sizeof(NvU16), src, (wcslen(src) + 1) * sizeof(wchar_t)); } @@ -34,9 +34,10 @@ namespace nvprefs { } } - bool - driver_settings_t::init() { - if (session_handle) return true; + bool driver_settings_t::init() { + if (session_handle) { + return true; + } NvAPI_Status status; @@ -56,8 +57,7 @@ namespace nvprefs { return load_settings(); } - void - driver_settings_t::destroy() { + void driver_settings_t::destroy() { if (session_handle) { NvAPI_DRS_DestroySession(session_handle); session_handle = 0; @@ -65,9 +65,10 @@ namespace nvprefs { NvAPI_Unload(); } - bool - driver_settings_t::load_settings() { - if (!session_handle) return false; + bool driver_settings_t::load_settings() { + if (!session_handle) { + return false; + } NvAPI_Status status = NvAPI_DRS_LoadSettings(session_handle); if (status != NVAPI_OK) { @@ -80,9 +81,10 @@ namespace nvprefs { return true; } - bool - driver_settings_t::save_settings() { - if (!session_handle) return false; + bool driver_settings_t::save_settings() { + if (!session_handle) { + return false; + } NvAPI_Status status = NvAPI_DRS_SaveSettings(session_handle); if (status != NVAPI_OK) { @@ -94,9 +96,10 @@ namespace nvprefs { return true; } - bool - driver_settings_t::restore_global_profile_to_undo(const undo_data_t &undo_data) { - if (!session_handle) return false; + bool driver_settings_t::restore_global_profile_to_undo(const undo_data_t &undo_data) { + if (!session_handle) { + return false; + } const auto &swapchain_data = undo_data.get_opengl_swapchain(); if (swapchain_data) { @@ -130,8 +133,7 @@ namespace nvprefs { error_message("NvAPI_DRS_SetSetting() OGL_CPL_PREFER_DXPRESENT failed"); return false; } - } - else { + } else { status = NvAPI_DRS_DeleteProfileSetting(session_handle, profile_handle, OGL_CPL_PREFER_DXPRESENT_ID); if (status != NVAPI_OK && status != NVAPI_SETTING_NOT_FOUND) { @@ -142,11 +144,9 @@ namespace nvprefs { } info_message("Restored OGL_CPL_PREFER_DXPRESENT for base profile"); - } - else if (status == NVAPI_OK || status == NVAPI_SETTING_NOT_FOUND) { + } else if (status == NVAPI_OK || status == NVAPI_SETTING_NOT_FOUND) { info_message("OGL_CPL_PREFER_DXPRESENT has been changed from our value in base profile, not restoring"); - } - else { + } else { error_message("NvAPI_DRS_GetSetting() OGL_CPL_PREFER_DXPRESENT failed"); return false; } @@ -155,9 +155,10 @@ namespace nvprefs { return true; } - bool - driver_settings_t::check_and_modify_global_profile(std::optional &undo_data) { - if (!session_handle) return false; + bool driver_settings_t::check_and_modify_global_profile(std::optional &undo_data) { + if (!session_handle) { + return false; + } undo_data.reset(); NvAPI_Status status; @@ -184,8 +185,7 @@ namespace nvprefs { undo_data = undo_data_t(); if (status == NVAPI_OK) { undo_data->set_opengl_swapchain(OGL_CPL_PREFER_DXPRESENT_PREFER_ENABLED, setting.u32CurrentValue); - } - else { + } else { undo_data->set_opengl_swapchain(OGL_CPL_PREFER_DXPRESENT_PREFER_ENABLED, std::nullopt); } @@ -204,8 +204,7 @@ namespace nvprefs { } info_message("Changed OGL_CPL_PREFER_DXPRESENT to OGL_CPL_PREFER_DXPRESENT_PREFER_ENABLED for base profile"); - } - else if (status != NVAPI_OK) { + } else if (status != NVAPI_OK) { nvapi_error_message(status); error_message("NvAPI_DRS_GetSetting() OGL_CPL_PREFER_DXPRESENT failed"); return false; @@ -214,9 +213,10 @@ namespace nvprefs { return true; } - bool - driver_settings_t::check_and_modify_application_profile(bool &modified) { - if (!session_handle) return false; + bool driver_settings_t::check_and_modify_application_profile(bool &modified) { + if (!session_handle) { + return false; + } modified = false; NvAPI_Status status; @@ -285,10 +285,9 @@ namespace nvprefs { info_message(std::wstring(L"Removed PREFERRED_PSTATE for ") + sunshine_application_path); } - } - else if (status != NVAPI_OK || - setting.settingLocation != NVDRS_CURRENT_PROFILE_LOCATION || - setting.u32CurrentValue != PREFERRED_PSTATE_PREFER_MAX) { + } else if (status != NVAPI_OK || + setting.settingLocation != NVDRS_CURRENT_PROFILE_LOCATION || + setting.u32CurrentValue != PREFERRED_PSTATE_PREFER_MAX) { // Set power setting if needed setting = {}; setting.version = NVDRS_SETTING_VER1; diff --git a/src/platform/windows/nvprefs/driver_settings.h b/src/platform/windows/nvprefs/driver_settings.h index c2ef8bec2fe..e4649d1d502 100644 --- a/src/platform/windows/nvprefs/driver_settings.h +++ b/src/platform/windows/nvprefs/driver_settings.h @@ -21,26 +21,19 @@ namespace nvprefs { public: ~driver_settings_t(); - bool - init(); + bool init(); - void - destroy(); + void destroy(); - bool - load_settings(); + bool load_settings(); - bool - save_settings(); + bool save_settings(); - bool - restore_global_profile_to_undo(const undo_data_t &undo_data); + bool restore_global_profile_to_undo(const undo_data_t &undo_data); - bool - check_and_modify_global_profile(std::optional &undo_data); + bool check_and_modify_global_profile(std::optional &undo_data); - bool - check_and_modify_application_profile(bool &modified); + bool check_and_modify_application_profile(bool &modified); private: NvDRSSessionHandle session_handle = 0; diff --git a/src/platform/windows/nvprefs/nvapi_opensource_wrapper.cpp b/src/platform/windows/nvprefs/nvapi_opensource_wrapper.cpp index 4227882c48e..b4c04f6aadd 100644 --- a/src/platform/windows/nvprefs/nvapi_opensource_wrapper.cpp +++ b/src/platform/windows/nvprefs/nvapi_opensource_wrapper.cpp @@ -2,7 +2,7 @@ * @file src/platform/windows/nvprefs/nvapi_opensource_wrapper.cpp * @brief Definitions for the NVAPI wrapper. */ -// standard library headers +// standard includes #include // local includes @@ -17,9 +17,8 @@ namespace { std::map interfaces; HMODULE dll = NULL; - template - NvAPI_Status - call_interface(const char *name, Args... args) { + template + NvAPI_Status call_interface(const char *name, Args... args) { auto func = (Func *) interfaces[name]; if (!func) { @@ -38,7 +37,9 @@ extern void *__cdecl nvapi_QueryInterface(NvU32 id); NVAPI_INTERFACE NvAPI_Initialize() { - if (dll) return NVAPI_OK; + if (dll) { + return NVAPI_OK; + } #ifdef _WIN64 auto dll_name = "nvapi64.dll"; @@ -59,8 +60,7 @@ NvAPI_Initialize() { return NVAPI_LIBRARY_NOT_FOUND; } -NVAPI_INTERFACE -NvAPI_Unload() { +NVAPI_INTERFACE NvAPI_Unload() { if (dll) { interfaces.clear(); FreeLibrary(dll); @@ -69,69 +69,56 @@ NvAPI_Unload() { return NVAPI_OK; } -NVAPI_INTERFACE -NvAPI_GetErrorMessage(NvAPI_Status nr, NvAPI_ShortString szDesc) { +NVAPI_INTERFACE NvAPI_GetErrorMessage(NvAPI_Status nr, NvAPI_ShortString szDesc) { return call_interface("NvAPI_GetErrorMessage", nr, szDesc); } // This is only a subset of NvAPI_DRS_* functions, more can be added if needed -NVAPI_INTERFACE -NvAPI_DRS_CreateSession(NvDRSSessionHandle *phSession) { +NVAPI_INTERFACE NvAPI_DRS_CreateSession(NvDRSSessionHandle *phSession) { return call_interface("NvAPI_DRS_CreateSession", phSession); } -NVAPI_INTERFACE -NvAPI_DRS_DestroySession(NvDRSSessionHandle hSession) { +NVAPI_INTERFACE NvAPI_DRS_DestroySession(NvDRSSessionHandle hSession) { return call_interface("NvAPI_DRS_DestroySession", hSession); } -NVAPI_INTERFACE -NvAPI_DRS_LoadSettings(NvDRSSessionHandle hSession) { +NVAPI_INTERFACE NvAPI_DRS_LoadSettings(NvDRSSessionHandle hSession) { return call_interface("NvAPI_DRS_LoadSettings", hSession); } -NVAPI_INTERFACE -NvAPI_DRS_SaveSettings(NvDRSSessionHandle hSession) { +NVAPI_INTERFACE NvAPI_DRS_SaveSettings(NvDRSSessionHandle hSession) { return call_interface("NvAPI_DRS_SaveSettings", hSession); } -NVAPI_INTERFACE -NvAPI_DRS_CreateProfile(NvDRSSessionHandle hSession, NVDRS_PROFILE *pProfileInfo, NvDRSProfileHandle *phProfile) { +NVAPI_INTERFACE NvAPI_DRS_CreateProfile(NvDRSSessionHandle hSession, NVDRS_PROFILE *pProfileInfo, NvDRSProfileHandle *phProfile) { return call_interface("NvAPI_DRS_CreateProfile", hSession, pProfileInfo, phProfile); } -NVAPI_INTERFACE -NvAPI_DRS_FindProfileByName(NvDRSSessionHandle hSession, NvAPI_UnicodeString profileName, NvDRSProfileHandle *phProfile) { +NVAPI_INTERFACE NvAPI_DRS_FindProfileByName(NvDRSSessionHandle hSession, NvAPI_UnicodeString profileName, NvDRSProfileHandle *phProfile) { return call_interface("NvAPI_DRS_FindProfileByName", hSession, profileName, phProfile); } -NVAPI_INTERFACE -NvAPI_DRS_CreateApplication(NvDRSSessionHandle hSession, NvDRSProfileHandle hProfile, NVDRS_APPLICATION *pApplication) { +NVAPI_INTERFACE NvAPI_DRS_CreateApplication(NvDRSSessionHandle hSession, NvDRSProfileHandle hProfile, NVDRS_APPLICATION *pApplication) { return call_interface("NvAPI_DRS_CreateApplication", hSession, hProfile, pApplication); } -NVAPI_INTERFACE -NvAPI_DRS_GetApplicationInfo(NvDRSSessionHandle hSession, NvDRSProfileHandle hProfile, NvAPI_UnicodeString appName, NVDRS_APPLICATION *pApplication) { +NVAPI_INTERFACE NvAPI_DRS_GetApplicationInfo(NvDRSSessionHandle hSession, NvDRSProfileHandle hProfile, NvAPI_UnicodeString appName, NVDRS_APPLICATION *pApplication) { return call_interface("NvAPI_DRS_GetApplicationInfo", hSession, hProfile, appName, pApplication); } -NVAPI_INTERFACE -NvAPI_DRS_SetSetting(NvDRSSessionHandle hSession, NvDRSProfileHandle hProfile, NVDRS_SETTING *pSetting) { +NVAPI_INTERFACE NvAPI_DRS_SetSetting(NvDRSSessionHandle hSession, NvDRSProfileHandle hProfile, NVDRS_SETTING *pSetting) { return call_interface("NvAPI_DRS_SetSetting", hSession, hProfile, pSetting); } -NVAPI_INTERFACE -NvAPI_DRS_GetSetting(NvDRSSessionHandle hSession, NvDRSProfileHandle hProfile, NvU32 settingId, NVDRS_SETTING *pSetting) { +NVAPI_INTERFACE NvAPI_DRS_GetSetting(NvDRSSessionHandle hSession, NvDRSProfileHandle hProfile, NvU32 settingId, NVDRS_SETTING *pSetting) { return call_interface("NvAPI_DRS_GetSetting", hSession, hProfile, settingId, pSetting); } -NVAPI_INTERFACE -NvAPI_DRS_DeleteProfileSetting(NvDRSSessionHandle hSession, NvDRSProfileHandle hProfile, NvU32 settingId) { +NVAPI_INTERFACE NvAPI_DRS_DeleteProfileSetting(NvDRSSessionHandle hSession, NvDRSProfileHandle hProfile, NvU32 settingId) { return call_interface("NvAPI_DRS_DeleteProfileSetting", hSession, hProfile, settingId); } -NVAPI_INTERFACE -NvAPI_DRS_GetBaseProfile(NvDRSSessionHandle hSession, NvDRSProfileHandle *phProfile) { +NVAPI_INTERFACE NvAPI_DRS_GetBaseProfile(NvDRSSessionHandle hSession, NvDRSProfileHandle *phProfile) { return call_interface("NvAPI_DRS_GetBaseProfile", hSession, phProfile); } diff --git a/src/platform/windows/nvprefs/nvprefs_common.cpp b/src/platform/windows/nvprefs/nvprefs_common.cpp index 902ff81ae65..649ef60cc21 100644 --- a/src/platform/windows/nvprefs/nvprefs_common.cpp +++ b/src/platform/windows/nvprefs/nvprefs_common.cpp @@ -2,37 +2,32 @@ * @file src/platform/windows/nvprefs/nvprefs_common.cpp * @brief Definitions for common nvidia preferences. */ -// local includes +// this include #include "nvprefs_common.h" -#include "src/logging.h" -// read user override preferences from global sunshine config +// local includes #include "src/config.h" +#include "src/logging.h" namespace nvprefs { - void - info_message(const std::wstring &message) { + void info_message(const std::wstring &message) { BOOST_LOG(info) << "nvprefs: " << message; } - void - info_message(const std::string &message) { + void info_message(const std::string &message) { BOOST_LOG(info) << "nvprefs: " << message; } - void - error_message(const std::wstring &message) { + void error_message(const std::wstring &message) { BOOST_LOG(error) << "nvprefs: " << message; } - void - error_message(const std::string &message) { + void error_message(const std::string &message) { BOOST_LOG(error) << "nvprefs: " << message; } - nvprefs_options - get_nvprefs_options() { + nvprefs_options get_nvprefs_options() { nvprefs_options options; options.opengl_vulkan_on_dxgi = config::video.nv_opengl_vulkan_on_dxgi; options.sunshine_high_power_mode = config::video.nv_sunshine_high_power_mode; diff --git a/src/platform/windows/nvprefs/nvprefs_common.h b/src/platform/windows/nvprefs/nvprefs_common.h index aa6c00fa3b3..baf4c0fbdbf 100644 --- a/src/platform/windows/nvprefs/nvprefs_common.h +++ b/src/platform/windows/nvprefs/nvprefs_common.h @@ -4,57 +4,51 @@ */ #pragma once -// sunshine utility header for generic smart pointers -#include "src/utility.h" - -// winapi headers +// platform includes // disable clang-format header reordering // clang-format off #include #include // clang-format on +// local includes +#include "src/utility.h" + namespace nvprefs { struct safe_handle: public util::safe_ptr_v2 { using util::safe_ptr_v2::safe_ptr_v2; - explicit - operator bool() const { + + explicit operator bool() const { auto handle = get(); return handle != NULL && handle != INVALID_HANDLE_VALUE; } }; struct safe_hlocal_deleter { - void - operator()(void *p) { + void operator()(void *p) { LocalFree(p); } }; - template + template using safe_hlocal = util::uniq_ptr, safe_hlocal_deleter>; using safe_sid = util::safe_ptr_v2; - void - info_message(const std::wstring &message); + void info_message(const std::wstring &message); - void - info_message(const std::string &message); + void info_message(const std::string &message); - void - error_message(const std::wstring &message); + void error_message(const std::wstring &message); - void - error_message(const std::string &message); + void error_message(const std::string &message); struct nvprefs_options { bool opengl_vulkan_on_dxgi = true; bool sunshine_high_power_mode = true; }; - nvprefs_options - get_nvprefs_options(); + nvprefs_options get_nvprefs_options(); } // namespace nvprefs diff --git a/src/platform/windows/nvprefs/nvprefs_interface.cpp b/src/platform/windows/nvprefs/nvprefs_interface.cpp index ad366cd548c..632ffa45b87 100644 --- a/src/platform/windows/nvprefs/nvprefs_interface.cpp +++ b/src/platform/windows/nvprefs/nvprefs_interface.cpp @@ -39,8 +39,7 @@ namespace nvprefs { unload(); } - bool - nvprefs_interface::load() { + bool nvprefs_interface::load() { if (!pimpl->loaded) { // Check %ProgramData% variable, need it for storing undo file wchar_t program_data_env[MAX_PATH]; @@ -61,8 +60,7 @@ namespace nvprefs { return pimpl->loaded; } - void - nvprefs_interface::unload() { + void nvprefs_interface::unload() { if (pimpl->loaded) { // Unload dynamically loaded nvapi library pimpl->driver_settings.destroy(); @@ -70,9 +68,10 @@ namespace nvprefs { } } - bool - nvprefs_interface::restore_from_and_delete_undo_file_if_exists() { - if (!pimpl->loaded) return false; + bool nvprefs_interface::restore_from_and_delete_undo_file_if_exists() { + if (!pimpl->loaded) { + return false; + } // Check for undo file from previous improper termination bool access_denied = false; @@ -82,12 +81,10 @@ namespace nvprefs { if (auto undo_data = undo_file->read_undo_data()) { if (pimpl->driver_settings.restore_global_profile_to_undo(*undo_data) && pimpl->driver_settings.save_settings()) { info_message("Restored global profile settings from undo file - deleting the file"); - } - else { + } else { error_message("Failed to restore global profile settings from undo file, deleting the file anyway"); } - } - else { + } else { error_message("Coulnd't read undo file, deleting the file anyway"); } @@ -95,8 +92,7 @@ namespace nvprefs { error_message("Couldn't delete undo file"); return false; } - } - else if (access_denied) { + } else if (access_denied) { error_message("Couldn't open undo file from previous improper termination, or confirm that there's no such file"); return false; } @@ -104,43 +100,41 @@ namespace nvprefs { return true; } - bool - nvprefs_interface::modify_application_profile() { - if (!pimpl->loaded) return false; + bool nvprefs_interface::modify_application_profile() { + if (!pimpl->loaded) { + return false; + } // Modify and save sunshine.exe application profile settings, if needed bool modified = false; if (!pimpl->driver_settings.check_and_modify_application_profile(modified)) { error_message("Failed to modify application profile settings"); return false; - } - else if (modified) { + } else if (modified) { if (pimpl->driver_settings.save_settings()) { info_message("Modified application profile settings"); - } - else { + } else { error_message("Couldn't save application profile settings"); return false; } - } - else { + } else { info_message("No need to modify application profile settings"); } return true; } - bool - nvprefs_interface::modify_global_profile() { - if (!pimpl->loaded) return false; + bool nvprefs_interface::modify_global_profile() { + if (!pimpl->loaded) { + return false; + } // Modify but not save global profile settings, if needed std::optional undo_data; if (!pimpl->driver_settings.check_and_modify_global_profile(undo_data)) { error_message("Couldn't modify global profile settings"); return false; - } - else if (!undo_data) { + } else if (!undo_data) { info_message("No need to modify global profile settings"); return true; } @@ -166,8 +160,7 @@ namespace nvprefs { if (pimpl->undo_data) { // Merge undo data if settings has been modified externally since our last modification pimpl->undo_data->merge(*undo_data); - } - else { + } else { pimpl->undo_data = undo_data; } @@ -198,14 +191,14 @@ namespace nvprefs { return true; } - bool - nvprefs_interface::owning_undo_file() { + bool nvprefs_interface::owning_undo_file() { return pimpl->undo_file.has_value(); } - bool - nvprefs_interface::restore_global_profile() { - if (!pimpl->loaded || !pimpl->undo_data || !pimpl->undo_file) return false; + bool nvprefs_interface::restore_global_profile() { + if (!pimpl->loaded || !pimpl->undo_data || !pimpl->undo_file) { + return false; + } // Restore global profile settings with undo data if (pimpl->driver_settings.restore_global_profile_to_undo(*pimpl->undo_data) && @@ -217,8 +210,7 @@ namespace nvprefs { } pimpl->undo_data = std::nullopt; pimpl->undo_file = std::nullopt; - } - else { + } else { error_message("Couldn't restore global profile settings"); return false; } diff --git a/src/platform/windows/nvprefs/nvprefs_interface.h b/src/platform/windows/nvprefs/nvprefs_interface.h index 73235877db9..655f114f419 100644 --- a/src/platform/windows/nvprefs/nvprefs_interface.h +++ b/src/platform/windows/nvprefs/nvprefs_interface.h @@ -4,7 +4,7 @@ */ #pragma once -// standard library headers +// standard includes #include namespace nvprefs { @@ -14,26 +14,19 @@ namespace nvprefs { nvprefs_interface(); ~nvprefs_interface(); - bool - load(); + bool load(); - void - unload(); + void unload(); - bool - restore_from_and_delete_undo_file_if_exists(); + bool restore_from_and_delete_undo_file_if_exists(); - bool - modify_application_profile(); + bool modify_application_profile(); - bool - modify_global_profile(); + bool modify_global_profile(); - bool - owning_undo_file(); + bool owning_undo_file(); - bool - restore_global_profile(); + bool restore_global_profile(); private: struct impl; diff --git a/src/platform/windows/nvprefs/undo_data.cpp b/src/platform/windows/nvprefs/undo_data.cpp index e75b92b880b..5a092815307 100644 --- a/src/platform/windows/nvprefs/undo_data.cpp +++ b/src/platform/windows/nvprefs/undo_data.cpp @@ -2,7 +2,7 @@ * @file src/platform/windows/nvprefs/undo_data.cpp * @brief Definitions for undoing changes to nvidia preferences. */ -// external includes +// lib includes #include // local includes @@ -17,54 +17,46 @@ namespace nlohmann { using data_t = nvprefs::undo_data_t::data_t; using opengl_swapchain_t = data_t::opengl_swapchain_t; - template + template struct adl_serializer> { - static void - to_json(json &j, const std::optional &opt) { + static void to_json(json &j, const std::optional &opt) { if (opt == std::nullopt) { j = nullptr; - } - else { + } else { j = *opt; } } - static void - from_json(const json &j, std::optional &opt) { + static void from_json(const json &j, std::optional &opt) { if (j.is_null()) { opt = std::nullopt; - } - else { + } else { opt = j.template get(); } } }; - template <> + template<> struct adl_serializer { - static void - to_json(json &j, const data_t &data) { - j = json { { "opengl_swapchain", data.opengl_swapchain } }; + static void to_json(json &j, const data_t &data) { + j = json {{"opengl_swapchain", data.opengl_swapchain}}; } - static void - from_json(const json &j, data_t &data) { + static void from_json(const json &j, data_t &data) { j.at("opengl_swapchain").get_to(data.opengl_swapchain); } }; - template <> + template<> struct adl_serializer { - static void - to_json(json &j, const opengl_swapchain_t &opengl_swapchain) { + static void to_json(json &j, const opengl_swapchain_t &opengl_swapchain) { j = json { - { "our_value", opengl_swapchain.our_value }, - { "undo_value", opengl_swapchain.undo_value } + {"our_value", opengl_swapchain.our_value}, + {"undo_value", opengl_swapchain.undo_value} }; } - static void - from_json(const json &j, opengl_swapchain_t &opengl_swapchain) { + static void from_json(const json &j, opengl_swapchain_t &opengl_swapchain) { j.at("our_value").get_to(opengl_swapchain.our_value); j.at("undo_value").get_to(opengl_swapchain.undo_value); } @@ -73,46 +65,39 @@ namespace nlohmann { namespace nvprefs { - void - undo_data_t::set_opengl_swapchain(uint32_t our_value, std::optional undo_value) { + void undo_data_t::set_opengl_swapchain(uint32_t our_value, std::optional undo_value) { data.opengl_swapchain = data_t::opengl_swapchain_t { our_value, undo_value }; } - std::optional - undo_data_t::get_opengl_swapchain() const { + std::optional undo_data_t::get_opengl_swapchain() const { return data.opengl_swapchain; } - std::string - undo_data_t::write() const { + std::string undo_data_t::write() const { try { // Keep this assignment otherwise data will be treated as an array due to // initializer list shenanigangs. const json json_data = data; return json_data.dump(); - } - catch (const std::exception &err) { - error_message(std::string { "failed to serialize json data" }); + } catch (const std::exception &err) { + error_message(std::string {"failed to serialize json data"}); return {}; } } - void - undo_data_t::read(const std::vector &buffer) { + void undo_data_t::read(const std::vector &buffer) { try { data = json::parse(std::begin(buffer), std::end(buffer)); - } - catch (const std::exception &err) { - error_message(std::string { "failed to parse json data: " } + err.what()); + } catch (const std::exception &err) { + error_message(std::string {"failed to parse json data: "} + err.what()); data = {}; } } - void - undo_data_t::merge(const undo_data_t &newer_data) { + void undo_data_t::merge(const undo_data_t &newer_data) { const auto &swapchain_data = newer_data.get_opengl_swapchain(); if (swapchain_data) { set_opengl_swapchain(swapchain_data->our_value, swapchain_data->undo_value); diff --git a/src/platform/windows/nvprefs/undo_data.h b/src/platform/windows/nvprefs/undo_data.h index 5bd10ad9e9b..aa359273303 100644 --- a/src/platform/windows/nvprefs/undo_data.h +++ b/src/platform/windows/nvprefs/undo_data.h @@ -4,7 +4,7 @@ */ #pragma once -// standard library headers +// standard includes #include #include #include @@ -23,20 +23,15 @@ namespace nvprefs { std::optional opengl_swapchain; }; - void - set_opengl_swapchain(uint32_t our_value, std::optional undo_value); + void set_opengl_swapchain(uint32_t our_value, std::optional undo_value); - std::optional - get_opengl_swapchain() const; + std::optional get_opengl_swapchain() const; - std::string - write() const; + std::string write() const; - void - read(const std::vector &buffer); + void read(const std::vector &buffer); - void - merge(const undo_data_t &newer_data); + void merge(const undo_data_t &newer_data); private: data_t data; diff --git a/src/platform/windows/nvprefs/undo_file.cpp b/src/platform/windows/nvprefs/undo_file.cpp index 5897834f2c6..ada737c8208 100644 --- a/src/platform/windows/nvprefs/undo_file.cpp +++ b/src/platform/windows/nvprefs/undo_file.cpp @@ -9,13 +9,14 @@ namespace { using namespace nvprefs; - DWORD - relax_permissions(HANDLE file_handle) { + DWORD relax_permissions(HANDLE file_handle) { PACL old_dacl = nullptr; safe_hlocal sd; DWORD status = GetSecurityInfo(file_handle, SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, nullptr, nullptr, &old_dacl, nullptr, &sd); - if (status != ERROR_SUCCESS) return status; + if (status != ERROR_SUCCESS) { + return status; + } safe_sid users_sid; SID_IDENTIFIER_AUTHORITY nt_authorithy = SECURITY_NT_AUTHORITY; @@ -32,10 +33,14 @@ namespace { safe_hlocal new_dacl; status = SetEntriesInAcl(1, &ea, old_dacl, &new_dacl); - if (status != ERROR_SUCCESS) return status; + if (status != ERROR_SUCCESS) { + return status; + } status = SetSecurityInfo(file_handle, SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, nullptr, nullptr, new_dacl.get(), nullptr); - if (status != ERROR_SUCCESS) return status; + if (status != ERROR_SUCCESS) { + return status; + } return 0; } @@ -44,23 +49,20 @@ namespace { namespace nvprefs { - std::optional - undo_file_t::open_existing_file(std::filesystem::path file_path, bool &access_denied) { + std::optional undo_file_t::open_existing_file(std::filesystem::path file_path, bool &access_denied) { undo_file_t file; file.file_handle.reset(CreateFileW(file_path.c_str(), GENERIC_READ | DELETE, 0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL)); if (file.file_handle) { access_denied = false; return file; - } - else { + } else { auto last_error = GetLastError(); access_denied = (last_error != ERROR_FILE_NOT_FOUND && last_error != ERROR_PATH_NOT_FOUND); return std::nullopt; } } - std::optional - undo_file_t::create_new_file(std::filesystem::path file_path) { + 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)); @@ -70,35 +72,34 @@ namespace nvprefs { error_message("Failed to relax permissions on undo file"); } return file; - } - else { + } else { return std::nullopt; } } - bool - undo_file_t::delete_file() { - if (!file_handle) return false; + bool undo_file_t::delete_file() { + if (!file_handle) { + return false; + } - FILE_DISPOSITION_INFO delete_file_info = { TRUE }; + FILE_DISPOSITION_INFO delete_file_info = {TRUE}; if (SetFileInformationByHandle(file_handle.get(), FileDispositionInfo, &delete_file_info, sizeof(delete_file_info))) { file_handle.reset(); return true; - } - else { + } else { return false; } } - bool - undo_file_t::write_undo_data(const undo_data_t &undo_data) { - if (!file_handle) return false; + bool undo_file_t::write_undo_data(const undo_data_t &undo_data) { + if (!file_handle) { + return false; + } std::string buffer; try { buffer = undo_data.write(); - } - catch (...) { + } catch (...) { error_message("Couldn't serialize undo data"); return false; } @@ -121,9 +122,10 @@ namespace nvprefs { return true; } - std::optional - undo_file_t::read_undo_data() { - if (!file_handle) return std::nullopt; + std::optional undo_file_t::read_undo_data() { + if (!file_handle) { + return std::nullopt; + } LARGE_INTEGER file_size; if (!GetFileSizeEx(file_handle.get(), &file_size)) { @@ -146,8 +148,7 @@ namespace nvprefs { undo_data_t undo_data; try { undo_data.read(buffer); - } - catch (...) { + } catch (...) { error_message("Couldn't parse undo file"); return std::nullopt; } diff --git a/src/platform/windows/nvprefs/undo_file.h b/src/platform/windows/nvprefs/undo_file.h index 2c20a9ab94c..39689953f86 100644 --- a/src/platform/windows/nvprefs/undo_file.h +++ b/src/platform/windows/nvprefs/undo_file.h @@ -4,7 +4,7 @@ */ #pragma once -// standard library headers +// standard includes #include // local includes @@ -15,20 +15,15 @@ namespace nvprefs { class undo_file_t { public: - static std::optional - open_existing_file(std::filesystem::path file_path, bool &access_denied); + static std::optional open_existing_file(std::filesystem::path file_path, bool &access_denied); - static std::optional - create_new_file(std::filesystem::path file_path); + static std::optional create_new_file(std::filesystem::path file_path); - bool - delete_file(); + bool delete_file(); - bool - write_undo_data(const undo_data_t &undo_data); + bool write_undo_data(const undo_data_t &undo_data); - std::optional - read_undo_data(); + std::optional read_undo_data(); private: undo_file_t() = default; diff --git a/src/platform/windows/publish.cpp b/src/platform/windows/publish.cpp index 780d31eb9a8..4c3c6f2421c 100644 --- a/src/platform/windows/publish.cpp +++ b/src/platform/windows/publish.cpp @@ -2,13 +2,16 @@ * @file src/platform/windows/publish.cpp * @brief Definitions for Windows mDNS service registration. */ +// platform includes +// winsock2.h must be included before windows.h +// clang-format off #include - #include - +// clang-format on #include #include +// local includes #include "misc.h" #include "src/config.h" #include "src/logging.h" @@ -17,7 +20,7 @@ #include "src/platform/common.h" #include "src/thread_safe.h" -#define _FN(x, ret, args) \ +#define _FN(x, ret, args) \ typedef ret(*x##_fn) args; \ static x##_fn x @@ -28,69 +31,69 @@ using namespace std::literals; extern "C" { #ifndef __MINGW32__ -constexpr auto DNS_REQUEST_PENDING = 9506L; -constexpr auto DNS_QUERY_REQUEST_VERSION1 = 0x1; -constexpr auto DNS_QUERY_RESULTS_VERSION1 = 0x1; + constexpr auto DNS_REQUEST_PENDING = 9506L; + constexpr auto DNS_QUERY_REQUEST_VERSION1 = 0x1; + constexpr auto DNS_QUERY_RESULTS_VERSION1 = 0x1; #endif #define SERVICE_DOMAIN "local" -constexpr auto SERVICE_TYPE_DOMAIN = SV(SERVICE_TYPE "." SERVICE_DOMAIN); + constexpr auto SERVICE_TYPE_DOMAIN = SV(SERVICE_TYPE "." SERVICE_DOMAIN); #ifndef __MINGW32__ -typedef struct _DNS_SERVICE_INSTANCE { - LPWSTR pszInstanceName; - LPWSTR pszHostName; + typedef struct _DNS_SERVICE_INSTANCE { + LPWSTR pszInstanceName; + LPWSTR pszHostName; - IP4_ADDRESS *ip4Address; - IP6_ADDRESS *ip6Address; + IP4_ADDRESS *ip4Address; + IP6_ADDRESS *ip6Address; - WORD wPort; - WORD wPriority; - WORD wWeight; + WORD wPort; + WORD wPriority; + WORD wWeight; - // Property list - DWORD dwPropertyCount; + // Property list + DWORD dwPropertyCount; - PWSTR *keys; - PWSTR *values; + PWSTR *keys; + PWSTR *values; - DWORD dwInterfaceIndex; -} DNS_SERVICE_INSTANCE, *PDNS_SERVICE_INSTANCE; + DWORD dwInterfaceIndex; + } DNS_SERVICE_INSTANCE, *PDNS_SERVICE_INSTANCE; #endif -typedef VOID WINAPI -DNS_SERVICE_REGISTER_COMPLETE( - _In_ DWORD Status, - _In_ PVOID pQueryContext, - _In_ PDNS_SERVICE_INSTANCE pInstance); + typedef VOID WINAPI + DNS_SERVICE_REGISTER_COMPLETE( + _In_ DWORD Status, + _In_ PVOID pQueryContext, + _In_ PDNS_SERVICE_INSTANCE pInstance + ); -typedef DNS_SERVICE_REGISTER_COMPLETE *PDNS_SERVICE_REGISTER_COMPLETE; + typedef DNS_SERVICE_REGISTER_COMPLETE *PDNS_SERVICE_REGISTER_COMPLETE; #ifndef __MINGW32__ -typedef struct _DNS_SERVICE_CANCEL { - PVOID reserved; -} DNS_SERVICE_CANCEL, *PDNS_SERVICE_CANCEL; - -typedef struct _DNS_SERVICE_REGISTER_REQUEST { - ULONG Version; - ULONG InterfaceIndex; - PDNS_SERVICE_INSTANCE pServiceInstance; - PDNS_SERVICE_REGISTER_COMPLETE pRegisterCompletionCallback; - PVOID pQueryContext; - HANDLE hCredentials; - BOOL unicastEnabled; -} DNS_SERVICE_REGISTER_REQUEST, *PDNS_SERVICE_REGISTER_REQUEST; + typedef struct _DNS_SERVICE_CANCEL { + PVOID reserved; + } DNS_SERVICE_CANCEL, *PDNS_SERVICE_CANCEL; + + typedef struct _DNS_SERVICE_REGISTER_REQUEST { + ULONG Version; + ULONG InterfaceIndex; + PDNS_SERVICE_INSTANCE pServiceInstance; + PDNS_SERVICE_REGISTER_COMPLETE pRegisterCompletionCallback; + PVOID pQueryContext; + HANDLE hCredentials; + BOOL unicastEnabled; + } DNS_SERVICE_REGISTER_REQUEST, *PDNS_SERVICE_REGISTER_REQUEST; #endif -_FN(_DnsServiceFreeInstance, VOID, (_In_ PDNS_SERVICE_INSTANCE pInstance)); -_FN(_DnsServiceDeRegister, DWORD, (_In_ PDNS_SERVICE_REGISTER_REQUEST pRequest, _Inout_opt_ PDNS_SERVICE_CANCEL pCancel)); -_FN(_DnsServiceRegister, DWORD, (_In_ PDNS_SERVICE_REGISTER_REQUEST pRequest, _Inout_opt_ PDNS_SERVICE_CANCEL pCancel)); + _FN(_DnsServiceFreeInstance, VOID, (_In_ PDNS_SERVICE_INSTANCE pInstance)); + _FN(_DnsServiceDeRegister, DWORD, (_In_ PDNS_SERVICE_REGISTER_REQUEST pRequest, _Inout_opt_ PDNS_SERVICE_CANCEL pCancel)); + _FN(_DnsServiceRegister, DWORD, (_In_ PDNS_SERVICE_REGISTER_REQUEST pRequest, _Inout_opt_ PDNS_SERVICE_CANCEL pCancel)); } /* extern "C" */ namespace platf::publish { - VOID WINAPI - register_cb(DWORD status, PVOID pQueryContext, PDNS_SERVICE_INSTANCE pInstance) { + VOID WINAPI register_cb(DWORD status, PVOID pQueryContext, PDNS_SERVICE_INSTANCE pInstance) { auto alarm = (safe::alarm_t::element_type *) pQueryContext; if (status) { @@ -100,11 +103,10 @@ namespace platf::publish { alarm->ring(pInstance); } - static int - service(bool enable, PDNS_SERVICE_INSTANCE &existing_instance) { + static int service(bool enable, PDNS_SERVICE_INSTANCE &existing_instance) { auto alarm = safe::make_alarm(); - std::wstring domain { SERVICE_TYPE_DOMAIN.data(), SERVICE_TYPE_DOMAIN.size() }; + std::wstring domain {SERVICE_TYPE_DOMAIN.data(), SERVICE_TYPE_DOMAIN.size()}; auto hostname = platf::get_host_name(); auto name = from_utf8(net::mdns_instance_name(hostname) + '.') + domain; @@ -124,8 +126,8 @@ namespace platf::publish { // Most clients aren't strictly checking TXT record compliance with RFC 1035, // but Apple's mDNS resolver does and rejects the entire answer if an invalid // TXT record is present. - PWCHAR keys[] = { nullptr }; - PWCHAR values[] = { nullptr }; + PWCHAR keys[] = {nullptr}; + PWCHAR values[] = {nullptr}; instance.dwPropertyCount = 1; instance.keys = keys; instance.values = values; @@ -144,8 +146,7 @@ namespace platf::publish { print_status("DnsServiceRegister()"sv, status); return -1; } - } - else { + } else { status = _DnsServiceDeRegister(&req, nullptr); if (status != DNS_REQUEST_PENDING) { print_status("DnsServiceDeRegister()"sv, status); @@ -159,8 +160,7 @@ namespace platf::publish { if (enable) { // Store this instance for later deregistration existing_instance = registered_instance; - } - else if (registered_instance) { + } else if (registered_instance) { // Deregistration was successful _DnsServiceFreeInstance(registered_instance); existing_instance = nullptr; @@ -196,8 +196,7 @@ namespace platf::publish { PDNS_SERVICE_INSTANCE existing_instance; }; - int - load_funcs(HMODULE handle) { + int load_funcs(HMODULE handle) { auto fg = util::fail_guard([handle]() { FreeLibrary(handle); }); @@ -215,8 +214,7 @@ namespace platf::publish { return 0; } - std::unique_ptr<::platf::deinit_t> - start() { + std::unique_ptr<::platf::deinit_t> start() { HMODULE handle = LoadLibrary("dnsapi.dll"); if (!handle || load_funcs(handle)) { diff --git a/src/platform/windows/windows.rs.in b/src/platform/windows/windows.rc.in similarity index 93% rename from src/platform/windows/windows.rs.in rename to src/platform/windows/windows.rc.in index 384d54044a2..c7f8cebefb7 100644 --- a/src/platform/windows/windows.rs.in +++ b/src/platform/windows/windows.rc.in @@ -1,7 +1,7 @@ /** - * @file src/platform/windows/windows.rs.in + * @file src/platform/windows/windows.rc.in * @brief Windows resource file template. - * @note The final `windows.rs` is generated from this file during the CMake build. + * @note The final `windows.rc` is generated from this file during the CMake build. * @todo Use CMake definitions directly, instead of configuring this file. */ #include "winver.h" diff --git a/src/process.cpp b/src/process.cpp index 1c78ff4d9c8..c009fda3ca1 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -4,27 +4,29 @@ */ #define BOOST_BIND_GLOBAL_PLACEHOLDERS -#include "process.h" - +// standard includes #include #include #include #include +// lib includes #include #include #include #include #include #include - #include #include +// local includes #include "config.h" #include "crypto.h" +#include "display_device.h" #include "logging.h" #include "platform/common.h" +#include "process.h" #include "system_tray.h" #include "utility.h" @@ -51,13 +53,11 @@ namespace proc { } }; - std::unique_ptr - init() { + std::unique_ptr init() { return std::make_unique(); } - void - terminate_process_group(boost::process::v1::child &proc, boost::process::v1::group &group, std::chrono::seconds exit_timeout) { + void terminate_process_group(boost::process::v1::child &proc, boost::process::v1::group &group, std::chrono::seconds exit_timeout) { if (group.valid() && platf::process_group_running((std::uintptr_t) group.native_handle())) { if (exit_timeout.count() > 0) { // Request processes in the group to exit gracefully @@ -72,16 +72,13 @@ namespace proc { if (exit_timeout.count() < 0) { BOOST_LOG(warning) << "App did not fully exit within the timeout. Terminating the app's remaining processes."sv; - } - else { + } else { BOOST_LOG(info) << "All app processes have successfully exited."sv; } - } - else { + } else { BOOST_LOG(info) << "App did not respond to a graceful termination request. Forcefully terminating the app's processes."sv; } - } - else { + } else { BOOST_LOG(info) << "No graceful exit timeout was specified for this app. Forcefully terminating the app's processes."sv; } @@ -98,8 +95,7 @@ namespace proc { } } - boost::filesystem::path - find_working_directory(const std::string &cmd, boost::process::v1::environment &env) { + 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 #ifdef _WIN32 auto parts = boost::program_options::split_winmain(cmd); @@ -134,8 +130,7 @@ namespace proc { return cmd_path.parent_path(); } - int - proc_t::execute(int app_id, std::shared_ptr launch_session) { + int proc_t::execute(int app_id, std::shared_ptr launch_session) { // Ensure starting from a clean slate terminate(); @@ -238,8 +233,7 @@ namespace proc { auto child = platf::run_command(_app.elevated, true, cmd, working_dir, _env, _pipe.get(), ec, nullptr); if (ec) { BOOST_LOG(warning) << "Couldn't spawn ["sv << cmd << "]: System: "sv << ec.message(); - } - else { + } else { child.detach(); } } @@ -247,8 +241,7 @@ namespace proc { if (_app.cmd.empty()) { BOOST_LOG(info) << "Executing [Desktop]"sv; placebo = true; - } - else { + } else { boost::filesystem::path working_dir = _app.working_dir.empty() ? find_working_directory(_app.cmd, _env) : boost::filesystem::path(_app.working_dir); @@ -267,8 +260,7 @@ namespace proc { return 0; } - int - proc_t::running() { + int proc_t::running() { #ifndef _WIN32 // On POSIX OSes, we must periodically wait for our children to avoid // them becoming zombies. This must be synchronized carefully with @@ -281,17 +273,14 @@ namespace proc { if (placebo) { return _app_id; - } - else if (_app.wait_all && _process_group && platf::process_group_running((std::uintptr_t) _process_group.native_handle())) { + } else if (_app.wait_all && _process_group && platf::process_group_running((std::uintptr_t) _process_group.native_handle())) { // The app is still running if any process in the group is still running return _app_id; - } - else if (_process.running()) { + } else if (_process.running()) { // The app is still running only if the initial process launched is still running return _app_id; - } - else if (_app.auto_detach && _process.native_exit_code() == 0 && - std::chrono::steady_clock::now() - _app_launch_time < 5s) { + } else if (_app.auto_detach && _process.native_exit_code() == 0 && + std::chrono::steady_clock::now() - _app_launch_time < 5s) { BOOST_LOG(info) << "App exited gracefully within 5 seconds of launch. Treating the app as a detached command."sv; BOOST_LOG(info) << "Adjust this behavior in the Applications tab or apps.json if this is not what you want."sv; placebo = true; @@ -307,8 +296,7 @@ namespace proc { return 0; } - void - proc_t::terminate() { + void proc_t::terminate() { std::error_code ec; placebo = false; terminate_process_group(_process, _process_group, _app.exit_timeout); @@ -341,25 +329,27 @@ namespace proc { } _pipe.reset(); -#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 + bool has_run = _app_id > 0; // Only show the Stopped notification if we actually have an app to stop // Since terminate() is always run when a new app has started if (proc::proc.get_last_run_app_name().length() > 0 && has_run) { +#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 system_tray::update_tray_stopped(proc::proc.get_last_run_app_name()); - } #endif + display_device::revert_configuration(); + } + _app_id = -1; } - const std::vector & - proc_t::get_apps() const { + const std::vector &proc_t::get_apps() const { return _apps; } - std::vector & - proc_t::get_apps() { + + std::vector &proc_t::get_apps() { return _apps; } @@ -367,8 +357,7 @@ namespace proc { // Returns image from assets directory if found there. // Returns default image if image configuration is not set. // Returns http content-type header compatible image type. - std::string - proc_t::get_app_image(int app_id) { + std::string proc_t::get_app_image(int app_id) { auto iter = std::find_if(_apps.begin(), _apps.end(), [&app_id](const auto app) { return app.id == std::to_string(app_id); }); @@ -377,8 +366,7 @@ namespace proc { return validate_app_image_path(app_image_path); } - std::string - proc_t::get_last_run_app_name() { + std::string proc_t::get_last_run_app_name() { return _app.name; } @@ -391,8 +379,7 @@ namespace proc { assert(!_process.running()); } - std::string_view::iterator - find_match(std::string_view::iterator begin, std::string_view::iterator end) { + std::string_view::iterator find_match(std::string_view::iterator begin, std::string_view::iterator end) { int stack = 0; --begin; @@ -413,8 +400,7 @@ namespace proc { return begin; } - std::string - parse_env_val(boost::process::v1::native_environment &env, const std::string_view &val_raw) { + std::string parse_env_val(boost::process::v1::native_environment &env, const std::string_view &val_raw) { auto pos = std::begin(val_raw); auto dollar = std::find(pos, std::end(val_raw), '$'); @@ -424,31 +410,33 @@ namespace proc { auto next = dollar + 1; if (next != std::end(val_raw)) { switch (*next) { - case '(': { - ss.write(pos, (dollar - pos)); - auto var_begin = next + 1; - auto var_end = find_match(next, std::end(val_raw)); - auto var_name = std::string { var_begin, var_end }; + case '(': + { + ss.write(pos, (dollar - pos)); + auto var_begin = next + 1; + auto var_end = find_match(next, std::end(val_raw)); + auto var_name = std::string {var_begin, var_end}; #ifdef _WIN32 - // Windows treats environment variable names in a case-insensitive manner, - // so we look for a case-insensitive match here. This is critical for - // correctly appending to PATH on Windows. - auto itr = std::find_if(env.cbegin(), env.cend(), - [&](const auto &e) { return boost::iequals(e.get_name(), var_name); }); - if (itr != env.cend()) { - // Use an existing case-insensitive match - var_name = itr->get_name(); - } + // Windows treats environment variable names in a case-insensitive manner, + // so we look for a case-insensitive match here. This is critical for + // correctly appending to PATH on Windows. + auto itr = std::find_if(env.cbegin(), env.cend(), [&](const auto &e) { + return boost::iequals(e.get_name(), var_name); + }); + if (itr != env.cend()) { + // Use an existing case-insensitive match + var_name = itr->get_name(); + } #endif - ss << env[var_name].to_string(); + ss << env[var_name].to_string(); - pos = var_end + 1; - next = var_end; + pos = var_end + 1; + next = var_end; - break; - } + break; + } case '$': ss.write(pos, (next - pos)); pos = next + 1; @@ -457,8 +445,7 @@ namespace proc { } dollar = std::find(next, std::end(val_raw), '$'); - } - else { + } else { dollar = next; } } @@ -468,8 +455,7 @@ namespace proc { return ss.str(); } - std::string - validate_app_image_path(std::string app_image_path) { + std::string validate_app_image_path(std::string app_image_path) { if (app_image_path.empty()) { return DEFAULT_APP_IMAGE_PATH; } @@ -487,8 +473,7 @@ namespace proc { auto full_image_path = std::filesystem::path(SUNSHINE_ASSETS_DIR) / app_image_path; if (std::filesystem::exists(full_image_path)) { return full_image_path.string(); - } - else if (app_image_path == "./assets/steam.png") { + } else if (app_image_path == "./assets/steam.png") { // handle old default steam image definition return SUNSHINE_ASSETS_DIR "/steam.png"; } @@ -506,9 +491,8 @@ namespace proc { return app_image_path; } - std::optional - calculate_sha256(const std::string &filename) { - crypto::md_ctx_t ctx { EVP_MD_CTX_create() }; + std::optional calculate_sha256(const std::string &filename) { + crypto::md_ctx_t ctx {EVP_MD_CTX_create()}; if (!ctx) { return std::nullopt; } @@ -542,15 +526,13 @@ namespace proc { return ss.str(); } - uint32_t - calculate_crc32(const std::string &input) { + uint32_t calculate_crc32(const std::string &input) { boost::crc_32_type result; result.process_bytes(input.data(), input.length()); return result.checksum(); } - std::tuple - calculate_app_id(const std::string &app_name, std::string app_image_path, int index) { + std::tuple calculate_app_id(const std::string &app_name, std::string app_image_path, int index) { // Generate id by hashing name with image data if present std::vector to_hash; to_hash.push_back(app_name); @@ -559,8 +541,7 @@ namespace proc { auto file_hash = calculate_sha256(file_path); if (file_hash) { to_hash.push_back(file_hash.value()); - } - else { + } else { // Fallback to just hashing image path to_hash.push_back(file_path); } @@ -568,7 +549,9 @@ namespace proc { // Create combined strings for hash std::stringstream ss; - for_each(to_hash.begin(), to_hash.end(), [&ss](const std::string &s) { ss << s; }); + for_each(to_hash.begin(), to_hash.end(), [&ss](const std::string &s) { + ss << s; + }); auto input_no_index = ss.str(); ss << index; auto input_with_index = ss.str(); @@ -580,8 +563,7 @@ namespace proc { return std::make_tuple(id_no_index, id_with_index); } - std::optional - parse(const std::string &file_name) { + std::optional parse(const std::string &file_name) { pt::ptree tree; try { @@ -625,7 +607,8 @@ namespace proc { prep_cmds.emplace_back( std::move(do_cmd), std::move(undo_cmd), - std::move(prep_cmd.elevated)); + std::move(prep_cmd.elevated) + ); } } @@ -641,7 +624,8 @@ namespace proc { prep_cmds.emplace_back( parse_env_val(this_env, do_cmd.value_or("")), parse_env_val(this_env, undo_cmd.value_or("")), - std::move(elevated.value_or(false))); + std::move(elevated.value_or(false)) + ); } } @@ -680,14 +664,13 @@ namespace proc { ctx.elevated = elevated.value_or(false); ctx.auto_detach = auto_detach.value_or(true); ctx.wait_all = wait_all.value_or(true); - ctx.exit_timeout = std::chrono::seconds { exit_timeout.value_or(5) }; + ctx.exit_timeout = std::chrono::seconds {exit_timeout.value_or(5)}; auto possible_ids = calculate_app_id(name, ctx.image_path, i++); if (ids.count(std::get<0>(possible_ids)) == 0) { // Avoid using index to generate id if possible ctx.id = std::get<0>(possible_ids); - } - else { + } else { // Fallback to include index on collision ctx.id = std::get<1>(possible_ids); } @@ -701,18 +684,17 @@ namespace proc { } return proc::proc_t { - std::move(this_env), std::move(apps) + std::move(this_env), + std::move(apps) }; - } - catch (std::exception &e) { + } catch (std::exception &e) { BOOST_LOG(error) << e.what(); } return std::nullopt; } - void - refresh(const std::string &file_name) { + void refresh(const std::string &file_name) { auto proc_opt = proc::parse(file_name); if (proc_opt) { diff --git a/src/process.h b/src/process.h index 2b0ba8b9002..f5a81e900c6 100644 --- a/src/process.h +++ b/src/process.h @@ -8,11 +8,14 @@ #define __kernel_entry #endif +// standard includes #include #include +// lib includes #include +// local includes #include "config.h" #include "platform/common.h" #include "rtsp.h" @@ -22,6 +25,7 @@ namespace proc { using file_t = util::safe_ptr_v2; typedef config::prep_cmd_t cmd_t; + /** * pre_cmds -- guaranteed to be executed unless any of the commands fail. * detached -- commands detached from Sunshine @@ -69,32 +73,27 @@ namespace proc { proc_t( boost::process::v1::environment &&env, - std::vector &&apps): + std::vector &&apps + ): _app_id(0), _env(std::move(env)), - _apps(std::move(apps)) {} + _apps(std::move(apps)) { + } - int - execute(int app_id, std::shared_ptr launch_session); + int execute(int app_id, std::shared_ptr launch_session); /** * @return `_app_id` if a process is running, otherwise returns `0` */ - int - running(); + int running(); ~proc_t(); - const std::vector & - get_apps() const; - std::vector & - get_apps(); - std::string - get_app_image(int app_id); - std::string - get_last_run_app_name(); - void - terminate(); + const std::vector &get_apps() const; + std::vector &get_apps(); + std::string get_app_image(int app_id); + std::string get_last_run_app_name(); + void terminate(); private: int _app_id; @@ -119,22 +118,17 @@ namespace proc { * @brief Calculate a stable id based on name and image data * @return Tuple of id calculated without index (for use if no collision) and one with. */ - std::tuple - calculate_app_id(const std::string &app_name, std::string app_image_path, int index); + std::tuple calculate_app_id(const std::string &app_name, std::string app_image_path, int index); - std::string - validate_app_image_path(std::string app_image_path); - void - refresh(const std::string &file_name); - std::optional - parse(const std::string &file_name); + std::string validate_app_image_path(std::string app_image_path); + void refresh(const std::string &file_name); + std::optional parse(const std::string &file_name); /** * @brief Initialize proc functions * @return Unique pointer to `deinit_t` to manage cleanup */ - std::unique_ptr - init(); + std::unique_ptr init(); /** * @brief Terminates all child processes in a process group. @@ -142,8 +136,7 @@ namespace proc { * @param group The group of all children in the process tree. * @param exit_timeout The timeout to wait for the process group to gracefully exit. */ - void - terminate_process_group(boost::process::v1::child &proc, boost::process::v1::group &group, std::chrono::seconds exit_timeout); + void terminate_process_group(boost::process::v1::child &proc, boost::process::v1::group &group, std::chrono::seconds exit_timeout); extern proc_t proc; } // namespace proc diff --git a/src/round_robin.h b/src/round_robin.h index d7be2593633..20536955072 100644 --- a/src/round_robin.h +++ b/src/round_robin.h @@ -4,6 +4,7 @@ */ #pragma once +// standard includes #include /** @@ -12,7 +13,7 @@ * @tparam T The iterator type. */ namespace round_robin_util { - template + template class it_wrap_t { public: using iterator_category = std::random_access_iterator_tag; @@ -26,8 +27,7 @@ namespace round_robin_util { typedef T iterator; typedef std::ptrdiff_t diff_t; - iterator - operator+=(diff_t step) { + iterator operator+=(diff_t step) { while (step-- > 0) { ++_this(); } @@ -35,8 +35,7 @@ namespace round_robin_util { return _this(); } - iterator - operator-=(diff_t step) { + iterator operator-=(diff_t step) { while (step-- > 0) { --_this(); } @@ -44,22 +43,19 @@ namespace round_robin_util { return _this(); } - iterator - operator+(diff_t step) { + iterator operator+(diff_t step) { iterator new_ = _this(); return new_ += step; } - iterator - operator-(diff_t step) { + iterator operator-(diff_t step) { iterator new_ = _this(); return new_ -= step; } - diff_t - operator-(iterator first) { + diff_t operator-(iterator first) { diff_t step = 0; while (first != _this()) { ++step; @@ -69,19 +65,17 @@ namespace round_robin_util { return step; } - iterator - operator++() { + iterator operator++() { _this().inc(); return _this(); } - iterator - operator--() { + + iterator operator--() { _this().dec(); return _this(); } - iterator - operator++(int) { + iterator operator++(int) { iterator new_ = _this(); ++_this(); @@ -89,8 +83,7 @@ namespace round_robin_util { return new_; } - iterator - operator--(int) { + iterator operator--(int) { iterator new_ = _this(); --_this(); @@ -98,59 +91,69 @@ namespace round_robin_util { return new_; } - reference - operator*() { return *_this().get(); } - const_reference - operator*() const { return *_this().get(); } + reference operator*() { + return *_this().get(); + } + + const_reference operator*() const { + return *_this().get(); + } - pointer - operator->() { return &*_this(); } - const_pointer - operator->() const { return &*_this(); } + pointer operator->() { + return &*_this(); + } + + const_pointer operator->() const { + return &*_this(); + } - bool - operator!=(const iterator &other) const { + bool operator!=(const iterator &other) const { return !(_this() == other); } - bool - operator<(const iterator &other) const { + bool operator<(const iterator &other) const { return !(_this() >= other); } - bool - operator>=(const iterator &other) const { + bool operator>=(const iterator &other) const { return _this() == other || _this() > other; } - bool - operator<=(const iterator &other) const { + bool operator<=(const iterator &other) const { return _this() == other || _this() < other; } - bool - operator==(const iterator &other) const { return _this().eq(other); }; - bool - operator>(const iterator &other) const { return _this().gt(other); } + bool operator==(const iterator &other) const { + return _this().eq(other); + }; + + bool operator>(const iterator &other) const { + return _this().gt(other); + } private: - iterator & - _this() { return *static_cast(this); } - const iterator & - _this() const { return *static_cast(this); } + iterator &_this() { + return *static_cast(this); + } + + const iterator &_this() const { + return *static_cast(this); + } }; - template + template class round_robin_t: public it_wrap_t> { public: using iterator = It; using pointer = V *; round_robin_t(iterator begin, iterator end): - _begin(begin), _end(end), _pos(begin) {} + _begin(begin), + _end(end), + _pos(begin) { + } - void - inc() { + void inc() { ++_pos; if (_pos == _end) { @@ -158,8 +161,7 @@ namespace round_robin_util { } } - void - dec() { + void dec() { if (_pos == _begin) { _pos = _end; } @@ -167,13 +169,11 @@ namespace round_robin_util { --_pos; } - bool - eq(const round_robin_t &other) const { + bool eq(const round_robin_t &other) const { return *_pos == *other._pos; } - pointer - get() const { + pointer get() const { return &*_pos; } @@ -184,9 +184,8 @@ namespace round_robin_util { It _pos; }; - template - round_robin_t - make_round_robin(It begin, It end) { + template + round_robin_t make_round_robin(It begin, It end) { return round_robin_t(begin, end); } } // namespace round_robin_util diff --git a/src/rswrapper.c b/src/rswrapper.c index b554bc29a2d..953ba477ee4 100644 --- a/src/rswrapper.c +++ b/src/rswrapper.c @@ -121,8 +121,7 @@ reed_solomon_decode_t reed_solomon_decode_fn; * @brief This initializes the RS function pointers to the best vectorized version available. * @details The streaming code will directly invoke these function pointers during encoding. */ -void -reed_solomon_init(void) { +void reed_solomon_init(void) { #if defined(__x86_64__) || defined(__i386__) if (__builtin_cpu_supports("avx512f") && __builtin_cpu_supports("avx512bw")) { reed_solomon_new_fn = reed_solomon_new_avx512; @@ -130,22 +129,19 @@ reed_solomon_init(void) { reed_solomon_encode_fn = reed_solomon_encode_avx512; reed_solomon_decode_fn = reed_solomon_decode_avx512; reed_solomon_init_avx512(); - } - else if (__builtin_cpu_supports("avx2")) { + } else if (__builtin_cpu_supports("avx2")) { reed_solomon_new_fn = reed_solomon_new_avx2; reed_solomon_release_fn = reed_solomon_release_avx2; reed_solomon_encode_fn = reed_solomon_encode_avx2; reed_solomon_decode_fn = reed_solomon_decode_avx2; reed_solomon_init_avx2(); - } - else if (__builtin_cpu_supports("ssse3")) { + } else if (__builtin_cpu_supports("ssse3")) { reed_solomon_new_fn = reed_solomon_new_ssse3; reed_solomon_release_fn = reed_solomon_release_ssse3; reed_solomon_encode_fn = reed_solomon_encode_ssse3; reed_solomon_decode_fn = reed_solomon_decode_ssse3; reed_solomon_init_ssse3(); - } - else + } else #endif { reed_solomon_new_fn = reed_solomon_new_def; diff --git a/src/rswrapper.h b/src/rswrapper.h index d9a4c01dca5..6a3e3878472 100644 --- a/src/rswrapper.h +++ b/src/rswrapper.h @@ -5,6 +5,7 @@ */ #pragma once +// standard includes #include typedef struct _reed_solomon reed_solomon; @@ -28,5 +29,4 @@ extern reed_solomon_decode_t reed_solomon_decode_fn; * @brief This initializes the RS function pointers to the best vectorized version available. * @details The streaming code will directly invoke these function pointers during encoding. */ -void -reed_solomon_init(void); +void reed_solomon_init(void); diff --git a/src/rtsp.cpp b/src/rtsp.cpp index be739313a63..d6f6fbbb481 100644 --- a/src/rtsp.cpp +++ b/src/rtsp.cpp @@ -9,13 +9,18 @@ extern "C" { #include } +// standard includes #include #include +#include +#include #include +// lib includes #include #include +// local includes #include "config.h" #include "globals.h" #include "input.h" @@ -26,9 +31,6 @@ extern "C" { #include "sync.h" #include "video.h" -#include -#include - namespace asio = boost::asio; using asio::ip::tcp; @@ -37,8 +39,7 @@ using asio::ip::udp; using namespace std::literals; namespace rtsp_stream { - void - free_msg(PRTSP_MESSAGE msg) { + void free_msg(PRTSP_MESSAGE msg) { freeMessage(msg); delete msg; @@ -51,18 +52,15 @@ namespace rtsp_stream { // parsing code to be able to tell encrypted from plaintext messages. static constexpr std::uint32_t ENCRYPTED_MESSAGE_TYPE_BIT = 0x80000000; - uint8_t * - payload() { + uint8_t *payload() { return (uint8_t *) (this + 1); } - std::uint32_t - payload_length() { + std::uint32_t payload_length() { return util::endian::big(typeAndLength) & ~ENCRYPTED_MESSAGE_TYPE_BIT; } - bool - is_encrypted() { + bool is_encrypted() { return !!(util::endian::big(typeAndLength) & ENCRYPTED_MESSAGE_TYPE_BIT); } @@ -83,23 +81,21 @@ namespace rtsp_stream { using msg_t = util::safe_ptr; using cmd_func_t = std::function; - void - print_msg(PRTSP_MESSAGE msg); - void - cmd_not_found(tcp::socket &sock, launch_session_t &, msg_t &&req); - void - respond(tcp::socket &sock, launch_session_t &session, POPTION_ITEM options, int statuscode, const char *status_msg, int seqn, const std::string_view &payload); + void print_msg(PRTSP_MESSAGE msg); + void cmd_not_found(tcp::socket &sock, launch_session_t &, msg_t &&req); + void respond(tcp::socket &sock, launch_session_t &session, POPTION_ITEM options, int statuscode, const char *status_msg, int seqn, const std::string_view &payload); class socket_t: public std::enable_shared_from_this { public: - socket_t(boost::asio::io_service &ios, std::function &&handle_data_fn): - handle_data_fn { std::move(handle_data_fn) }, sock { ios } {} + socket_t(boost::asio::io_context &io_context, std::function &&handle_data_fn): + handle_data_fn {std::move(handle_data_fn)}, + sock {io_context} { + } /** * @brief Queue an asynchronous read to begin the next message. */ - void - read() { + void read() { if (begin == std::end(msg_buf) || (session->rtsp_cipher && begin + sizeof(encrypted_rtsp_header_t) >= std::end(msg_buf))) { BOOST_LOG(error) << "RTSP: read(): Exceeded maximum rtsp packet size: "sv << msg_buf.size(); @@ -113,20 +109,17 @@ namespace rtsp_stream { if (session->rtsp_cipher) { // For encrypted RTSP, we will read the the entire header first - boost::asio::async_read(sock, - boost::asio::buffer(begin, sizeof(encrypted_rtsp_header_t)), - boost::bind( - &socket_t::handle_read_encrypted_header, shared_from_this(), - boost::asio::placeholders::error, - boost::asio::placeholders::bytes_transferred)); - } - else { + boost::asio::async_read(sock, boost::asio::buffer(begin, sizeof(encrypted_rtsp_header_t)), boost::bind(&socket_t::handle_read_encrypted_header, shared_from_this(), boost::asio::placeholders::error, boost::asio::placeholders::bytes_transferred)); + } else { sock.async_read_some( - boost::asio::buffer(begin, (std::size_t)(std::end(msg_buf) - begin)), + boost::asio::buffer(begin, (std::size_t) (std::end(msg_buf) - begin)), boost::bind( - &socket_t::handle_read_plaintext, shared_from_this(), + &socket_t::handle_read_plaintext, + shared_from_this(), boost::asio::placeholders::error, - boost::asio::placeholders::bytes_transferred)); + boost::asio::placeholders::bytes_transferred + ) + ); } } @@ -136,8 +129,7 @@ namespace rtsp_stream { * @param ec The error code of the read operation. * @param bytes The number of bytes read. */ - static void - handle_read_encrypted_header(std::shared_ptr &socket, const boost::system::error_code &ec, std::size_t bytes) { + static void handle_read_encrypted_header(std::shared_ptr &socket, const boost::system::error_code &ec, std::size_t bytes) { BOOST_LOG(debug) << "handle_read_encrypted_header(): Handle read of size: "sv << bytes << " bytes"sv; auto sock_close = util::fail_guard([&socket]() { @@ -177,12 +169,7 @@ namespace rtsp_stream { sock_close.disable(); // Read the remainder of the header and full encrypted payload - boost::asio::async_read(socket->sock, - boost::asio::buffer(socket->begin + bytes, payload_length), - boost::bind( - &socket_t::handle_read_encrypted_message, socket->shared_from_this(), - boost::asio::placeholders::error, - boost::asio::placeholders::bytes_transferred)); + boost::asio::async_read(socket->sock, boost::asio::buffer(socket->begin + bytes, payload_length), boost::bind(&socket_t::handle_read_encrypted_message, socket->shared_from_this(), boost::asio::placeholders::error, boost::asio::placeholders::bytes_transferred)); } /** @@ -191,8 +178,7 @@ namespace rtsp_stream { * @param ec The error code of the read operation. * @param bytes The number of bytes read. */ - static void - handle_read_encrypted_message(std::shared_ptr &socket, const boost::system::error_code &ec, std::size_t bytes) { + static void handle_read_encrypted_message(std::shared_ptr &socket, const boost::system::error_code &ec, std::size_t bytes) { BOOST_LOG(debug) << "handle_read_encrypted(): Handle read of size: "sv << bytes << " bytes"sv; auto sock_close = util::fail_guard([&socket]() { @@ -229,14 +215,14 @@ namespace rtsp_stream { iv[11] = 'R'; // RTSP std::vector plaintext; - if (socket->session->rtsp_cipher->decrypt(std::string_view { (const char *) header->tag, sizeof(header->tag) + bytes }, plaintext, &iv)) { + if (socket->session->rtsp_cipher->decrypt(std::string_view {(const char *) header->tag, sizeof(header->tag) + bytes}, plaintext, &iv)) { BOOST_LOG(error) << "Failed to verify RTSP message tag"sv; respond(socket->sock, *socket->session, nullptr, 400, "BAD REQUEST", 0, {}); return; } - msg_t req { new msg_t::element_type {} }; + msg_t req {new msg_t::element_type {}}; if (auto status = parseRtspMessage(req.get(), (char *) plaintext.data(), plaintext.size())) { BOOST_LOG(error) << "Malformed RTSP message: ["sv << status << ']'; @@ -254,8 +240,7 @@ namespace rtsp_stream { /** * @brief Queue an asynchronous read of the payload portion of a plaintext message. */ - void - read_plaintext_payload() { + void read_plaintext_payload() { if (begin == std::end(msg_buf)) { BOOST_LOG(error) << "RTSP: read_plaintext_payload(): Exceeded maximum rtsp packet size: "sv << msg_buf.size(); @@ -268,11 +253,14 @@ namespace rtsp_stream { } sock.async_read_some( - boost::asio::buffer(begin, (std::size_t)(std::end(msg_buf) - begin)), + boost::asio::buffer(begin, (std::size_t) (std::end(msg_buf) - begin)), boost::bind( - &socket_t::handle_plaintext_payload, shared_from_this(), + &socket_t::handle_plaintext_payload, + shared_from_this(), boost::asio::placeholders::error, - boost::asio::placeholders::bytes_transferred)); + boost::asio::placeholders::bytes_transferred + ) + ); } /** @@ -281,8 +269,7 @@ namespace rtsp_stream { * @param ec The error code of the read operation. * @param bytes The number of bytes read. */ - static void - handle_plaintext_payload(std::shared_ptr &socket, const boost::system::error_code &ec, std::size_t bytes) { + static void handle_plaintext_payload(std::shared_ptr &socket, const boost::system::error_code &ec, std::size_t bytes) { BOOST_LOG(debug) << "handle_plaintext_payload(): Handle read of size: "sv << bytes << " bytes"sv; auto sock_close = util::fail_guard([&socket]() { @@ -301,8 +288,8 @@ namespace rtsp_stream { } auto end = socket->begin + bytes; - msg_t req { new msg_t::element_type {} }; - if (auto status = parseRtspMessage(req.get(), socket->msg_buf.data(), (std::size_t)(end - socket->msg_buf.data()))) { + msg_t req {new msg_t::element_type {}}; + if (auto status = parseRtspMessage(req.get(), socket->msg_buf.data(), (std::size_t) (end - socket->msg_buf.data()))) { BOOST_LOG(error) << "Malformed RTSP message: ["sv << status << ']'; respond(socket->sock, *socket->session, nullptr, 400, "BAD REQUEST", 0, {}); @@ -322,8 +309,10 @@ namespace rtsp_stream { // If content_length > bytes read, then we need to store current data read, // to be appended by the next read. - std::string_view content { option->content }; - auto begin = std::find_if(std::begin(content), std::end(content), [](auto ch) { return (bool) std::isdigit(ch); }); + std::string_view content {option->content}; + auto begin = std::find_if(std::begin(content), std::end(content), [](auto ch) { + return (bool) std::isdigit(ch); + }); content_length = util::from_chars(begin, std::end(content)); break; @@ -332,7 +321,7 @@ namespace rtsp_stream { if (end - socket->crlf >= content_length) { if (end - socket->crlf > content_length) { - BOOST_LOG(warning) << "(end - socket->crlf) > content_length -- "sv << (std::size_t)(end - socket->crlf) << " > "sv << content_length; + BOOST_LOG(warning) << "(end - socket->crlf) > content_length -- "sv << (std::size_t) (end - socket->crlf) << " > "sv << content_length; } fg.disable(); @@ -350,8 +339,7 @@ namespace rtsp_stream { * @param ec The error code of the read operation. * @param bytes The number of bytes read. */ - static void - handle_read_plaintext(std::shared_ptr &socket, const boost::system::error_code &ec, std::size_t bytes) { + static void handle_read_plaintext(std::shared_ptr &socket, const boost::system::error_code &ec, std::size_t bytes) { BOOST_LOG(debug) << "handle_read_plaintext(): Handle read of size: "sv << bytes << " bytes"sv; if (ec) { @@ -393,8 +381,7 @@ namespace rtsp_stream { handle_plaintext_payload(socket, ec, buf_size); } - void - handle_data(msg_t &&req) { + void handle_data(msg_t &&req) { handle_data_fn(sock, *session, std::move(req)); } @@ -416,14 +403,13 @@ namespace rtsp_stream { clear(); } - int - bind(net::af_e af, std::uint16_t port, boost::system::error_code &ec) { + int bind(net::af_e af, std::uint16_t port, boost::system::error_code &ec) { acceptor.open(af == net::IPV4 ? tcp::v4() : tcp::v6(), ec); if (ec) { return -1; } - acceptor.set_option(boost::asio::socket_base::reuse_address { true }); + acceptor.set_option(boost::asio::socket_base::reuse_address {true}); acceptor.bind(tcp::endpoint(af == net::IPV4 ? tcp::v4() : tcp::v6(), port), ec); if (ec) { @@ -435,7 +421,7 @@ namespace rtsp_stream { return -1; } - next_socket = std::make_shared(ios, [this](tcp::socket &sock, launch_session_t &session, msg_t &&msg) { + next_socket = std::make_shared(io_context, [this](tcp::socket &sock, launch_session_t &session, msg_t &&msg) { handle_msg(sock, session, std::move(msg)); }); @@ -446,19 +432,16 @@ namespace rtsp_stream { return 0; } - template - void - iterate(std::chrono::duration timeout) { - ios.run_one_for(timeout); + template + void iterate(std::chrono::duration timeout) { + io_context.run_one_for(timeout); } - void - handle_msg(tcp::socket &sock, launch_session_t &session, msg_t &&req) { + void handle_msg(tcp::socket &sock, launch_session_t &session, msg_t &&req) { auto func = _map_cmd_cb.find(req->message.request.command); if (func != std::end(_map_cmd_cb)) { func->second(this, sock, session, std::move(req)); - } - else { + } else { cmd_not_found(sock, session, std::move(req)); } @@ -466,8 +449,7 @@ namespace rtsp_stream { sock.shutdown(boost::asio::socket_base::shutdown_type::shutdown_both, ec); } - void - handle_accept(const boost::system::error_code &ec) { + void handle_accept(const boost::system::error_code &ec) { if (ec) { BOOST_LOG(error) << "Couldn't accept incoming connections: "sv << ec.message(); @@ -478,13 +460,12 @@ namespace rtsp_stream { auto socket = std::move(next_socket); - auto launch_session { launch_event.view(0s) }; + auto launch_session {launch_event.view(0s)}; if (launch_session) { // Associate the current RTSP session with this socket and start reading socket->session = launch_session; socket->read(); - } - else { + } else { // This can happen due to normal things like port scanning, so let's not make these visible by default BOOST_LOG(debug) << "No pending session for incoming RTSP connection"sv; @@ -494,7 +475,7 @@ namespace rtsp_stream { } // Queue another asynchronous accept for the next incoming connection - next_socket = std::make_shared(ios, [this](tcp::socket &sock, launch_session_t &session, msg_t &&msg) { + next_socket = std::make_shared(io_context, [this](tcp::socket &sock, launch_session_t &session, msg_t &&msg) { handle_msg(sock, session, std::move(msg)); }); acceptor.async_accept(next_socket->sock, [this](const auto &ec) { @@ -502,8 +483,7 @@ namespace rtsp_stream { }); } - void - map(const std::string_view &type, cmd_func_t cb) { + void map(const std::string_view &type, cmd_func_t cb) { _map_cmd_cb.emplace(type, std::move(cb)); } @@ -513,8 +493,7 @@ namespace rtsp_stream { * the session will be discarded. * @param launch_session Streaming session information. */ - void - session_raise(std::shared_ptr launch_session) { + void session_raise(std::shared_ptr launch_session) { auto now = std::chrono::steady_clock::now(); // If a launch event is still pending, don't overwrite it. @@ -530,16 +509,14 @@ namespace rtsp_stream { * @brief Clear state for the oldest launch session. * @param launch_session_id The ID of the session to clear. */ - void - session_clear(uint32_t launch_session_id) { + void session_clear(uint32_t launch_session_id) { // We currently only support a single pending RTSP session, // so the ID should always match the one for that session. auto launch_session = launch_event.view(0s); if (launch_session) { if (launch_session->id != launch_session_id) { BOOST_LOG(error) << "Attempted to clear unexpected session: "sv << launch_session_id << " vs "sv << launch_session->id; - } - else { + } else { launch_event.pop(); } } @@ -549,8 +526,7 @@ namespace rtsp_stream { * @brief Get the number of active sessions. * @return Count of active sessions. */ - int - session_count() { + int session_count() { auto lg = _session_slots.lock(); return _session_slots->size(); } @@ -564,8 +540,7 @@ namespace rtsp_stream { * clear(false); * @examples_end */ - void - clear(bool all = true) { + void clear(bool all = true) { // if a launch event timed out --> Remove it. if (raised_timeout < std::chrono::steady_clock::now()) { auto discarded = launch_event.pop(0s); @@ -583,8 +558,7 @@ namespace rtsp_stream { stream::session::join(slot); i = _session_slots->erase(i); - } - else { + } else { i++; } } @@ -594,8 +568,7 @@ namespace rtsp_stream { * @brief Removes the provided session from the set of sessions. * @param session The session to remove. */ - void - remove(const std::shared_ptr &session) { + void remove(const std::shared_ptr &session) { auto lg = _session_slots.lock(); _session_slots->erase(session); } @@ -604,8 +577,7 @@ namespace rtsp_stream { * @brief Inserts the provided session into the set of sessions. * @param session The session to insert. */ - void - insert(const std::shared_ptr &session) { + void insert(const std::shared_ptr &session) { auto lg = _session_slots.lock(); _session_slots->emplace(session); BOOST_LOG(info) << "New streaming session started [active sessions: "sv << _session_slots->size() << ']'; @@ -618,39 +590,34 @@ namespace rtsp_stream { std::chrono::steady_clock::time_point raised_timeout; - boost::asio::io_service ios; - tcp::acceptor acceptor { ios }; + boost::asio::io_context io_context; + tcp::acceptor acceptor {io_context}; std::shared_ptr next_socket; }; rtsp_server_t server {}; - void - launch_session_raise(std::shared_ptr launch_session) { + void launch_session_raise(std::shared_ptr launch_session) { server.session_raise(std::move(launch_session)); } - void - launch_session_clear(uint32_t launch_session_id) { + void launch_session_clear(uint32_t launch_session_id) { server.session_clear(launch_session_id); } - int - session_count() { + int session_count() { // Ensure session_count is up-to-date server.clear(false); return server.session_count(); } - void - terminate_sessions() { + void terminate_sessions() { server.clear(true); } - int - send(tcp::socket &sock, const std::string_view &sv) { + int send(tcp::socket &sock, const std::string_view &sv) { std::size_t bytes_send = 0; while (bytes_send != sv.size()) { @@ -666,8 +633,7 @@ namespace rtsp_stream { return 0; } - void - respond(tcp::socket &sock, launch_session_t &session, msg_t &resp) { + void respond(tcp::socket &sock, launch_session_t &session, msg_t &resp) { auto payload = std::make_pair(resp->payload, resp->payloadLength); // Restore response message for proper destruction @@ -680,11 +646,11 @@ namespace rtsp_stream { resp->payloadLength = 0; int serialized_len; - util::c_ptr raw_resp { serializeRtspMessage(resp.get(), &serialized_len) }; + util::c_ptr raw_resp {serializeRtspMessage(resp.get(), &serialized_len)}; BOOST_LOG(debug) << "---Begin Response---"sv << std::endl - << std::string_view { raw_resp.get(), (std::size_t) serialized_len } << std::endl - << std::string_view { payload.first, (std::size_t) payload.second } << std::endl + << std::string_view {raw_resp.get(), (std::size_t) serialized_len} << std::endl + << std::string_view {payload.first, (std::size_t) payload.second} << std::endl << "---End Response---"sv << std::endl; // Encrypt the RTSP message if encryption is enabled @@ -718,13 +684,12 @@ namespace rtsp_stream { header->sequenceNumber = util::endian::big(session.rtsp_iv_counter); // Encrypt the RTSP message in place - session.rtsp_cipher->encrypt(std::string_view { (const char *) header->payload(), (std::size_t) payload_length }, header->tag, &iv); + session.rtsp_cipher->encrypt(std::string_view {(const char *) header->payload(), (std::size_t) payload_length}, header->tag, &iv); // Send the full encrypted message - send(sock, std::string_view { (char *) message.data(), message.size() }); - } - else { - std::string_view tmp_resp { raw_resp.get(), (size_t) serialized_len }; + send(sock, std::string_view {(char *) message.data(), message.size()}); + } else { + std::string_view tmp_resp {raw_resp.get(), (size_t) serialized_len}; // Send the plaintext RTSP message header if (send(sock, tmp_resp)) { @@ -732,25 +697,22 @@ namespace rtsp_stream { } // Send the plaintext RTSP message payload (if present) - send(sock, std::string_view { payload.first, (std::size_t) payload.second }); + send(sock, std::string_view {payload.first, (std::size_t) payload.second}); } } - void - respond(tcp::socket &sock, launch_session_t &session, POPTION_ITEM options, int statuscode, const char *status_msg, int seqn, const std::string_view &payload) { - msg_t resp { new msg_t::element_type }; + void respond(tcp::socket &sock, launch_session_t &session, POPTION_ITEM options, int statuscode, const char *status_msg, int seqn, const std::string_view &payload) { + msg_t resp {new msg_t::element_type}; createRtspResponse(resp.get(), nullptr, 0, const_cast("RTSP/1.0"), statuscode, const_cast(status_msg), seqn, options, const_cast(payload.data()), (int) payload.size()); respond(sock, session, resp); } - void - cmd_not_found(tcp::socket &sock, launch_session_t &session, msg_t &&req) { + void cmd_not_found(tcp::socket &sock, launch_session_t &session, msg_t &&req) { respond(sock, session, nullptr, 404, "NOT FOUND", req->sequenceNumber, {}); } - void - cmd_option(rtsp_server_t *server, tcp::socket &sock, launch_session_t &session, msg_t &&req) { + void cmd_option(rtsp_server_t *server, tcp::socket &sock, launch_session_t &session, msg_t &&req) { OPTION_ITEM option {}; // I know these string literals will not be modified @@ -762,8 +724,7 @@ namespace rtsp_stream { respond(sock, session, &option, 200, "OK", req->sequenceNumber, {}); } - void - cmd_describe(rtsp_server_t *server, tcp::socket &sock, launch_session_t &session, msg_t &&req) { + void cmd_describe(rtsp_server_t *server, tcp::socket &sock, launch_session_t &session, msg_t &&req) { OPTION_ITEM option {}; // I know these string literals will not be modified @@ -846,8 +807,7 @@ namespace rtsp_stream { respond(sock, session, &option, 200, "OK", req->sequenceNumber, ss.str()); } - void - cmd_setup(rtsp_server_t *server, tcp::socket &sock, launch_session_t &session, msg_t &&req) { + void cmd_setup(rtsp_server_t *server, tcp::socket &sock, launch_session_t &session, msg_t &&req) { OPTION_ITEM options[4] {}; auto &seqn = options[0]; @@ -860,22 +820,19 @@ namespace rtsp_stream { auto seqn_str = std::to_string(req->sequenceNumber); seqn.content = const_cast(seqn_str.c_str()); - std::string_view target { req->message.request.target }; + std::string_view target {req->message.request.target}; auto begin = std::find(std::begin(target), std::end(target), '=') + 1; auto end = std::find(begin, std::end(target), '/'); - std::string_view type { begin, (size_t) std::distance(begin, end) }; + std::string_view type {begin, (size_t) std::distance(begin, end)}; std::uint16_t port; if (type == "audio"sv) { port = net::map_port(stream::AUDIO_STREAM_PORT); - } - else if (type == "video"sv) { + } else if (type == "video"sv) { port = net::map_port(stream::VIDEO_STREAM_PORT); - } - else if (type == "control"sv) { + } else if (type == "control"sv) { port = net::map_port(stream::CONTROL_PORT); - } - else { + } else { cmd_not_found(sock, session, std::move(req)); return; @@ -899,8 +856,7 @@ namespace rtsp_stream { if (type == "control"sv) { payload_option.option = const_cast("X-SS-Connect-Data"); payload_option.content = connect_data.data(); - } - else { + } else { payload_option.option = const_cast("X-SS-Ping-Payload"); payload_option.content = session.av_ping_payload.data(); } @@ -910,8 +866,7 @@ namespace rtsp_stream { respond(sock, session, &seqn, 200, "OK", req->sequenceNumber, {}); } - void - cmd_announce(rtsp_server_t *server, tcp::socket &sock, launch_session_t &session, msg_t &&req) { + void cmd_announce(rtsp_server_t *server, tcp::socket &sock, launch_session_t &session, msg_t &&req) { OPTION_ITEM option {}; // I know these string literals will not be modified @@ -920,7 +875,7 @@ namespace rtsp_stream { auto seqn_str = std::to_string(req->sequenceNumber); option.content = const_cast(seqn_str.c_str()); - std::string_view payload { req->payload, (size_t) req->payloadLength }; + std::string_view payload {req->payload, (size_t) req->payloadLength}; std::vector lines; @@ -935,7 +890,9 @@ namespace rtsp_stream { if (whitespace(*pos++)) { lines.emplace_back(begin, pos - begin - 1); - while (pos != std::end(payload) && whitespace(*pos)) { ++pos; } + while (pos != std::end(payload) && whitespace(*pos)) { + ++pos; + } begin = pos; } } @@ -948,8 +905,7 @@ namespace rtsp_stream { auto type = line.substr(0, 2); if (type == "s="sv) { client = line.substr(2); - } - else if (type == "a=") { + } else if (type == "a=") { auto pos = line.find(':'); auto name = line.substr(2, pos - 2); @@ -976,6 +932,7 @@ namespace rtsp_stream { args.try_emplace("x-ml-video.configuredBitrateKbps"sv, "0"sv); 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); stream::config_t config; @@ -1012,10 +969,10 @@ namespace rtsp_stream { config.monitor.videoFormat = util::from_view(args.at("x-nv-vqos[0].bitStreamFormat"sv)); config.monitor.dynamicRange = util::from_view(args.at("x-nv-video[0].dynamicRangeMode"sv)); config.monitor.chromaSamplingType = util::from_view(args.at("x-ss-video[0].chromaSamplingType"sv)); + config.monitor.enableIntraRefresh = util::from_view(args.at("x-ss-video[0].intraRefresh"sv)); configuredBitrateKbps = util::from_view(args.at("x-ml-video.configuredBitrateKbps"sv)); - } - catch (std::out_of_range &) { + } catch (std::out_of_range &) { respond(sock, session, &option, 400, "BAD REQUEST", req->sequenceNumber, {}); return; } @@ -1026,13 +983,12 @@ namespace rtsp_stream { if (config.audio.channels == 2) { for (auto option = req->options; option != nullptr; option = option->next) { if ("Host"sv == option->option) { - std::string_view content { option->content }; + std::string_view content {option->content}; BOOST_LOG(debug) << "Found Host: "sv << content; config.audio.flags[audio::config_t::HIGH_QUALITY] = (content.find("0.0.0.0"sv) == std::string::npos); } } - } - else if (session.surround_params.length() > 3) { + } else if (session.surround_params.length() > 3) { // Channels std::uint8_t c = session.surround_params[0] - '0'; // Streams @@ -1120,8 +1076,7 @@ namespace rtsp_stream { respond(sock, session, &option, 200, "OK", req->sequenceNumber, {}); } - void - cmd_play(rtsp_server_t *server, tcp::socket &sock, launch_session_t &session, msg_t &&req) { + void cmd_play(rtsp_server_t *server, tcp::socket &sock, launch_session_t &session, msg_t &&req) { OPTION_ITEM option {}; // I know these string literals will not be modified @@ -1133,8 +1088,7 @@ namespace rtsp_stream { respond(sock, session, &option, 200, "OK", req->sequenceNumber, {}); } - void - rtpThread() { + void rtpThread() { auto shutdown_event = mail::man->event(mail::shutdown); auto broadcast_shutdown_event = mail::man->event(mail::broadcast_shutdown); @@ -1157,8 +1111,7 @@ namespace rtsp_stream { if (broadcast_shutdown_event->peek()) { server.clear(); - } - else { + } else { // cleanup all stopped sessions server.clear(false); } @@ -1167,14 +1120,13 @@ namespace rtsp_stream { server.clear(); } - void - print_msg(PRTSP_MESSAGE msg) { + void print_msg(PRTSP_MESSAGE msg) { std::string_view type = msg->type == TYPE_RESPONSE ? "RESPONSE"sv : "REQUEST"sv; - std::string_view payload { msg->payload, (size_t) msg->payloadLength }; - std::string_view protocol { msg->protocol }; + std::string_view payload {msg->payload, (size_t) msg->payloadLength}; + std::string_view protocol {msg->protocol}; auto seqnm = msg->sequenceNumber; - std::string_view messageBuffer { msg->messageBuffer }; + std::string_view messageBuffer {msg->messageBuffer}; BOOST_LOG(debug) << "type ["sv << type << ']'; BOOST_LOG(debug) << "sequence number ["sv << seqnm << ']'; @@ -1185,24 +1137,23 @@ namespace rtsp_stream { auto &resp = msg->message.response; auto statuscode = resp.statusCode; - std::string_view status { resp.statusString }; + std::string_view status {resp.statusString}; BOOST_LOG(debug) << "statuscode :: "sv << statuscode; BOOST_LOG(debug) << "status :: "sv << status; - } - else { + } else { auto &req = msg->message.request; - std::string_view command { req.command }; - std::string_view target { req.target }; + std::string_view command {req.command}; + std::string_view target {req.target}; BOOST_LOG(debug) << "command :: "sv << command; BOOST_LOG(debug) << "target :: "sv << target; } for (auto option = msg->options; option != nullptr; option = option->next) { - std::string_view content { option->content }; - std::string_view name { option->option }; + std::string_view content {option->content}; + std::string_view name {option->option}; BOOST_LOG(debug) << name << " :: "sv << content; } diff --git a/src/rtsp.h b/src/rtsp.h index 0ea78a9dcd0..7e71567790f 100644 --- a/src/rtsp.h +++ b/src/rtsp.h @@ -4,8 +4,10 @@ */ #pragma once +// standard includes #include +// local includes #include "crypto.h" #include "thread_safe.h" @@ -38,30 +40,25 @@ namespace rtsp_stream { uint32_t rtsp_iv_counter; }; - void - launch_session_raise(std::shared_ptr launch_session); + void launch_session_raise(std::shared_ptr launch_session); /** * @brief Clear state for the specified launch session. * @param launch_session_id The ID of the session to clear. */ - void - launch_session_clear(uint32_t launch_session_id); + void launch_session_clear(uint32_t launch_session_id); /** * @brief Get the number of active sessions. * @return Count of active sessions. */ - int - session_count(); + int session_count(); /** * @brief Terminates all running streaming sessions. */ - void - terminate_sessions(); + void terminate_sessions(); - void - rtpThread(); + void rtpThread(); } // namespace rtsp_stream diff --git a/src/stat_trackers.cpp b/src/stat_trackers.cpp index a5daf6fe097..13cc4170bff 100644 --- a/src/stat_trackers.cpp +++ b/src/stat_trackers.cpp @@ -1,19 +1,18 @@ -/** - * @file src/stat_trackers.cpp - * @brief Definitions for streaming statistic tracking. - */ -#include "stat_trackers.h" - -namespace stat_trackers { - - boost::format - one_digit_after_decimal() { - return boost::format("%1$.1f"); - } - - boost::format - two_digits_after_decimal() { - return boost::format("%1$.2f"); - } - -} // namespace stat_trackers +/** + * @file src/stat_trackers.cpp + * @brief Definitions for streaming statistic tracking. + */ +// local includes +#include "stat_trackers.h" + +namespace stat_trackers { + + boost::format one_digit_after_decimal() { + return boost::format("%1$.1f"); + } + + boost::format two_digits_after_decimal() { + return boost::format("%1$.2f"); + } + +} // namespace stat_trackers diff --git a/src/stat_trackers.h b/src/stat_trackers.h index 47bdca3753a..2e3873d4208 100644 --- a/src/stat_trackers.h +++ b/src/stat_trackers.h @@ -1,56 +1,53 @@ -/** - * @file src/stat_trackers.h - * @brief Declarations for streaming statistic tracking. - */ -#pragma once - -#include -#include -#include - -#include - -namespace stat_trackers { - - boost::format - one_digit_after_decimal(); - - boost::format - two_digits_after_decimal(); - - template - class min_max_avg_tracker { - public: - using callback_function = std::function; - - void - collect_and_callback_on_interval(T stat, const callback_function &callback, std::chrono::seconds interval_in_seconds) { - if (data.calls == 0) { - data.last_callback_time = std::chrono::steady_clock::now(); - } - else if (std::chrono::steady_clock::now() > data.last_callback_time + interval_in_seconds) { - callback(data.stat_min, data.stat_max, data.stat_total / data.calls); - data = {}; - } - data.stat_min = std::min(data.stat_min, stat); - data.stat_max = std::max(data.stat_max, stat); - data.stat_total += stat; - data.calls += 1; - } - - void - reset() { - data = {}; - } - - private: - struct { - std::chrono::steady_clock::time_point last_callback_time = std::chrono::steady_clock::now(); - T stat_min = std::numeric_limits::max(); - T stat_max = std::numeric_limits::min(); - double stat_total = 0; - uint32_t calls = 0; - } data; - }; - -} // namespace stat_trackers +/** + * @file src/stat_trackers.h + * @brief Declarations for streaming statistic tracking. + */ +#pragma once + +// standard includes +#include +#include +#include + +// lib includes +#include + +namespace stat_trackers { + + boost::format one_digit_after_decimal(); + + boost::format two_digits_after_decimal(); + + template + class min_max_avg_tracker { + public: + using callback_function = std::function; + + void collect_and_callback_on_interval(T stat, const callback_function &callback, std::chrono::seconds interval_in_seconds) { + if (data.calls == 0) { + data.last_callback_time = std::chrono::steady_clock::now(); + } else if (std::chrono::steady_clock::now() > data.last_callback_time + interval_in_seconds) { + callback(data.stat_min, data.stat_max, data.stat_total / data.calls); + data = {}; + } + data.stat_min = std::min(data.stat_min, stat); + data.stat_max = std::max(data.stat_max, stat); + data.stat_total += stat; + data.calls += 1; + } + + void reset() { + data = {}; + } + + private: + struct { + std::chrono::steady_clock::time_point last_callback_time = std::chrono::steady_clock::now(); + T stat_min = std::numeric_limits::max(); + T stat_max = std::numeric_limits::min(); + double stat_total = 0; + uint32_t calls = 0; + } data; + }; + +} // namespace stat_trackers diff --git a/src/stream.cpp b/src/stream.cpp index e6729a2197e..2572d555b38 100644 --- a/src/stream.cpp +++ b/src/stream.cpp @@ -2,36 +2,38 @@ * @file src/stream.cpp * @brief Definitions for the streaming protocols. */ -#include "process.h" +// standard includes +#include #include #include -#include -#include - +// lib includes #include +#include extern "C" { -// clang-format off + // clang-format off #include #include "rswrapper.h" -// clang-format on + // clang-format on } +// local includes #include "config.h" +#include "display_device.h" #include "globals.h" #include "input.h" #include "logging.h" #include "network.h" +#include "platform/common.h" +#include "process.h" #include "stream.h" #include "sync.h" #include "system_tray.h" #include "thread_safe.h" #include "utility.h" -#include "platform/common.h" - #define IDX_START_A 0 #define IDX_START_B 1 #define IDX_INVALIDATE_REF_FRAMES 2 @@ -46,6 +48,7 @@ extern "C" { #define IDX_RUMBLE_TRIGGER_DATA 12 #define IDX_SET_MOTION_EVENT 13 #define IDX_SET_RGB_LED 14 +#define IDX_SET_ADAPTIVE_TRIGGERS 15 static const short packetTypes[] = { 0x0305, // Start A @@ -63,6 +66,7 @@ static const short packetTypes[] = { 0x5500, // Rumble triggers (Sunshine protocol extension) 0x5501, // Set motion event (Sunshine protocol extension) 0x5502, // Set RGB LED (Sunshine protocol extension) + 0x5503, // Set Adaptive triggers (Sunshine protocol extension) }; namespace asio = boost::asio; @@ -83,8 +87,7 @@ namespace stream { #pragma pack(push, 1) struct video_short_frame_header_t { - uint8_t * - payload() { + uint8_t *payload() { return (uint8_t *) (this + 1); } @@ -111,11 +114,11 @@ namespace stream { static_assert( sizeof(video_short_frame_header_t) == 8, - "Short frame header must be 8 bytes"); + "Short frame header must be 8 bytes" + ); struct video_packet_raw_t { - uint8_t * - payload() { + uint8_t *payload() { return (uint8_t *) (this + 1); } @@ -139,8 +142,7 @@ namespace stream { std::uint16_t type; std::uint16_t payloadLength; - uint8_t * - payload() { + uint8_t *payload() { return (uint8_t *) (this + 1); } }; @@ -186,6 +188,21 @@ namespace stream { std::uint8_t b; }; + struct control_adaptive_triggers_t { + control_header_v2 header; + + std::uint16_t id; + /** + * 0x04 - Right trigger + * 0x08 - Left trigger + */ + std::uint8_t event_flags; + std::uint8_t type_left; + std::uint8_t type_right; + std::uint8_t left[DS_EFFECT_PAYLOAD_SIZE]; + std::uint8_t right[DS_EFFECT_PAYLOAD_SIZE]; + }; + struct control_hdr_mode_t { control_header_v2 header; @@ -202,10 +219,10 @@ namespace stream { // seq is accepted as an arbitrary value in Moonlight std::uint32_t seq; // Monotonically increasing sequence number (used as IV for AES-GCM) - uint8_t * - payload() { + uint8_t *payload() { return (uint8_t *) (this + 1); } + // encrypted control_header_v2 and payload data follow } *control_encrypted_p; @@ -216,10 +233,10 @@ namespace stream { #pragma pack(pop) - constexpr std::size_t - round_to_pkcs7_padded(std::size_t size) { + constexpr std::size_t round_to_pkcs7_padded(std::size_t size) { return ((size + 15) / 16) * 16; } + constexpr std::size_t MAX_AUDIO_PACKET_SIZE = 1400; using audio_aes_t = std::array; @@ -230,19 +247,17 @@ namespace stream { // return bytes written on success // return -1 on error - static inline int - encode_audio(bool encrypted, const audio::buffer_t &plaintext, uint8_t *destination, crypto::aes_t &iv, crypto::cipher::cbc_t &cbc) { + static inline int encode_audio(bool encrypted, const audio::buffer_t &plaintext, uint8_t *destination, crypto::aes_t &iv, crypto::cipher::cbc_t &cbc) { // If encryption isn't enabled if (!encrypted) { std::copy(std::begin(plaintext), std::end(plaintext), destination); return plaintext.size(); } - return cbc.encrypt(std::string_view { (char *) std::begin(plaintext), plaintext.size() }, destination, &iv); + return cbc.encrypt(std::string_view {(char *) std::begin(plaintext), plaintext.size()}, destination, &iv); } - static inline void - while_starting_do_nothing(std::atomic &state) { + static inline void while_starting_do_nothing(std::atomic &state) { while (state.load(std::memory_order_acquire) == session::state_e::STARTING) { std::this_thread::sleep_for(1ms); } @@ -250,8 +265,7 @@ namespace stream { class control_server_t { public: - int - bind(net::af_e address_family, std::uint16_t port) { + int bind(net::af_e address_family, std::uint16_t port) { _host = net::host_create(address_family, _addr, port); return !(bool) _host; @@ -260,16 +274,14 @@ namespace stream { // Get session associated with address. // If none are found, try to find a session not yet claimed. (It will be marked by a port of value 0 // If none of those are found, return nullptr - session_t * - get_session(const net::peer_t peer, uint32_t connect_data); + session_t *get_session(const net::peer_t peer, uint32_t connect_data); // Circular dependency: // iterate refers to session // session refers to broadcast_ctx_t // broadcast_ctx_t refers to control_server_t // Therefore, iterate is implemented further down the source file - void - iterate(std::chrono::milliseconds timeout); + void iterate(std::chrono::milliseconds timeout); /** * @brief Call the handler for a given control stream message. @@ -278,16 +290,13 @@ namespace stream { * @param payload The payload of the message. * @param reinjected `true` if this message is being reprocessed after decryption. */ - void - call(std::uint16_t type, session_t *session, const std::string_view &payload, bool reinjected); + void call(std::uint16_t type, session_t *session, const std::string_view &payload, bool reinjected); - void - map(uint16_t type, std::function cb) { + void map(uint16_t type, std::function cb) { _map_type_cb.emplace(type, std::move(cb)); } - int - send(const std::string_view &payload, net::peer_t peer) { + int send(const std::string_view &payload, net::peer_t peer) { auto packet = enet_packet_create(payload.data(), payload.size(), ENET_PACKET_FLAG_RELIABLE); if (enet_peer_send(peer, 0, packet)) { enet_packet_destroy(packet); @@ -298,8 +307,7 @@ namespace stream { return 0; } - void - flush() { + void flush() { enet_host_flush(_host.get()); } @@ -324,10 +332,10 @@ namespace stream { std::thread audio_thread; std::thread control_thread; - asio::io_service io; + asio::io_context io_context; - udp::socket video_sock { io }; - udp::socket audio_sock { io }; + udp::socket video_sock {io_context}; + udp::socket audio_sock {io_context}; control_server_t control_server; }; @@ -410,12 +418,12 @@ namespace stream { * returns empty string_view on failure * returns string_view pointing to payload data */ - template - static inline std::string_view - encode_control(session_t *session, const std::string_view &plaintext, std::array &tagged_cipher) { + template + static inline std::string_view encode_control(session_t *session, const std::string_view &plaintext, std::array &tagged_cipher) { static_assert( max_payload_size >= sizeof(control_encrypted_t) + sizeof(crypto::cipher::tag_size), - "max_payload_size >= sizeof(control_encrypted_t) + sizeof(crypto::cipher::tag_size)"); + "max_payload_size >= sizeof(control_encrypted_t) + sizeof(crypto::cipher::tag_size)" + ); if (session->config.controlProtocolType != 13) { return plaintext; @@ -437,8 +445,7 @@ namespace stream { std::copy_n((uint8_t *) &seq, sizeof(seq), std::begin(iv)); iv[10] = 'H'; // Host originated iv[11] = 'C'; // Control stream - } - else { + } else { // Nvidia's old style encryption uses a 16-byte IV iv.resize(16); @@ -459,18 +466,15 @@ namespace stream { packet->length = util::endian::little(packet_length); packet->seq = util::endian::little(seq); - return std::string_view { (char *) tagged_cipher.data(), packet_length + sizeof(control_encrypted_t) - sizeof(control_encrypted_t::seq) }; + return std::string_view {(char *) tagged_cipher.data(), packet_length + sizeof(control_encrypted_t) - sizeof(control_encrypted_t::seq)}; } - int - start_broadcast(broadcast_ctx_t &ctx); - void - end_broadcast(broadcast_ctx_t &ctx); + int start_broadcast(broadcast_ctx_t &ctx); + void end_broadcast(broadcast_ctx_t &ctx); static auto broadcast = safe::make_shared(start_broadcast, end_broadcast); - session_t * - control_server_t::get_session(const net::peer_t peer, uint32_t connect_data) { + session_t *control_server_t::get_session(const net::peer_t peer, uint32_t connect_data) { { // Fast path - look up existing session by peer auto lg = _peer_to_session.lock(); @@ -496,16 +500,13 @@ namespace stream { if (session_p->config.mlFeatureFlags & ML_FF_SESSION_ID_V1) { if (session_p->control.connect_data != connect_data) { continue; - } - else { + } else { BOOST_LOG(debug) << "Initialized new control stream session by connect data match [v2]"sv; } - } - else { + } else { if (session_p->control.expected_peer_address != peer_addr) { continue; - } - else { + } else { BOOST_LOG(debug) << "Initialized new control stream session by IP address match [v1]"sv; } } @@ -540,8 +541,7 @@ namespace stream { * @param payload The payload of the message. * @param reinjected `true` if this message is being reprocessed after decryption. */ - void - control_server_t::call(std::uint16_t type, session_t *session, const std::string_view &payload, bool reinjected) { + void control_server_t::call(std::uint16_t type, session_t *session, const std::string_view &payload, bool reinjected) { // If we are using the encrypted control stream protocol, drop any messages that come off the wire unencrypted if (session->config.controlProtocolType == 13 && !reinjected && type != packetTypes[IDX_ENCRYPTED]) { BOOST_LOG(error) << "Dropping unencrypted message on encrypted control stream: "sv << util::hex(type).to_string_view(); @@ -555,14 +555,12 @@ namespace stream { << "---data---"sv << std::endl << util::hex_vec(payload) << std::endl << "---end data---"sv; - } - else { + } else { cb->second(session, payload); } } - void - control_server_t::iterate(std::chrono::milliseconds timeout) { + void control_server_t::iterate(std::chrono::milliseconds timeout) { ENetEvent event; auto res = enet_host_service(_host.get(), &event, timeout.count()); @@ -578,14 +576,16 @@ namespace stream { session->pingTimeout = std::chrono::steady_clock::now() + config::stream.ping_timeout; switch (event.type) { - case ENET_EVENT_TYPE_RECEIVE: { - net::packet_t packet { event.packet }; + case ENET_EVENT_TYPE_RECEIVE: + { + net::packet_t packet {event.packet}; - auto type = *(std::uint16_t *) packet->data; - std::string_view payload { (char *) packet->data + sizeof(type), packet->dataLength - sizeof(type) }; + auto type = *(std::uint16_t *) packet->data; + std::string_view payload {(char *) packet->data + sizeof(type), packet->dataLength - sizeof(type)}; - call(type, session, payload, false); - } break; + call(type, session, payload, false); + } + break; case ENET_EVENT_TYPE_CONNECT: BOOST_LOG(info) << "CLIENT CONNECTED"sv; break; @@ -603,7 +603,9 @@ namespace stream { } namespace fec { - using rs_t = util::safe_ptr; + using rs_t = util::safe_ptr; struct fec_t { size_t data_shards; @@ -618,24 +620,20 @@ namespace stream { std::vector payload_buffers; - char * - data(size_t el) { + char *data(size_t el) { return (char *) shards_p[el]; } - char * - prefix(size_t el) { + char *prefix(size_t el) { return prefixsize ? &headers[el * prefixsize] : nullptr; } - size_t - size() const { + size_t size() const { return nr_shards; } }; - static fec_t - encode(const std::string_view &payload, size_t blocksize, size_t fecpercentage, size_t minparityshards, size_t prefixsize) { + static fec_t encode(const std::string_view &payload, size_t blocksize, size_t fecpercentage, size_t minparityshards, size_t prefixsize) { auto payload_size = payload.size(); auto pad = payload_size % blocksize != 0; @@ -657,8 +655,8 @@ namespace stream { // If we need to store a zero-padded data shard, allocate that first to // to keep the shards in order and reduce buffer fragmentation auto parity_shard_offset = pad ? 1 : 0; - util::buffer_t shards { (parity_shard_offset + parity_shards) * blocksize }; - util::buffer_t shards_p { nr_shards }; + util::buffer_t shards {(parity_shard_offset + parity_shards) * blocksize}; + util::buffer_t shards_p {nr_shards}; std::vector payload_buffers; payload_buffers.reserve(2); @@ -695,7 +693,7 @@ namespace stream { } // packets = parity_shards + data_shards - rs_t rs { reed_solomon_new(data_shards, parity_shards) }; + rs_t rs {reed_solomon_new(data_shards, parity_shards)}; reed_solomon_encode(rs.get(), shards_p.begin(), nr_shards, blocksize); } @@ -707,7 +705,7 @@ namespace stream { blocksize, prefixsize, std::move(shards), - util::buffer_t { nr_shards * prefixsize }, + util::buffer_t {nr_shards * prefixsize}, std::move(shards_p), std::move(payload_buffers), }; @@ -721,8 +719,7 @@ namespace stream { * @param data1 The first data buffer. * @param data2 The second data buffer. */ - std::vector - concat_and_insert(uint64_t insert_size, uint64_t slice_size, const std::string_view &data1, const std::string_view &data2) { + std::vector concat_and_insert(uint64_t insert_size, uint64_t slice_size, const std::string_view &data1, const std::string_view &data2) { auto data_size = data1.size() + data2.size(); auto pad = data_size % slice_size != 0; auto elements = data_size / slice_size + (pad ? 1 : 0); @@ -751,8 +748,7 @@ namespace stream { end = std::end(data2); std::copy(next, next + (slice_size - copy_len), (char *) p + copy_len + insert_size); next += slice_size - copy_len; - } - else { + } else { std::copy(next, next + slice_size, (char *) p + insert_size); next += slice_size; } @@ -761,8 +757,7 @@ namespace stream { return result; } - std::vector - replace(const std::string_view &original, const std::string_view &old, const std::string_view &_new) { + std::vector replace(const std::string_view &original, const std::string_view &old, const std::string_view &_new) { std::vector replaced; replaced.reserve(original.size() + _new.size() - old.size()); @@ -785,8 +780,7 @@ namespace stream { * @param msg The message to pass. * @return 0 on success. */ - int - send_feedback_msg(session_t *session, platf::gamepad_feedback_msg_t &msg) { + int send_feedback_msg(session_t *session, platf::gamepad_feedback_msg_t &msg) { if (!session->control.peer) { BOOST_LOG(warning) << "Couldn't send gamepad feedback data, still waiting for PING from Moonlight"sv; // Still waiting for PING from Moonlight @@ -807,13 +801,11 @@ namespace stream { plaintext.highfreq = util::endian::little(data.highfreq); BOOST_LOG(verbose) << "Rumble: "sv << msg.id << " :: "sv << util::hex(data.lowfreq).to_string_view() << " :: "sv << util::hex(data.highfreq).to_string_view(); - std::array + std::array encrypted_payload; payload = encode_control(session, util::view(plaintext), encrypted_payload); - } - else if (msg.type == platf::gamepad_feedback_e::rumble_triggers) { + } else if (msg.type == platf::gamepad_feedback_e::rumble_triggers) { control_rumble_triggers_t plaintext; plaintext.header.type = packetTypes[IDX_RUMBLE_TRIGGER_DATA]; plaintext.header.payloadLength = sizeof(plaintext) - sizeof(control_header_v2); @@ -825,13 +817,11 @@ namespace stream { plaintext.right = util::endian::little(data.right_trigger); BOOST_LOG(verbose) << "Rumble triggers: "sv << msg.id << " :: "sv << util::hex(data.left_trigger).to_string_view() << " :: "sv << util::hex(data.right_trigger).to_string_view(); - std::array + std::array encrypted_payload; payload = encode_control(session, util::view(plaintext), encrypted_payload); - } - else if (msg.type == platf::gamepad_feedback_e::set_motion_event_state) { + } else if (msg.type == platf::gamepad_feedback_e::set_motion_event_state) { control_set_motion_event_t plaintext; plaintext.header.type = packetTypes[IDX_SET_MOTION_EVENT]; plaintext.header.payloadLength = sizeof(plaintext) - sizeof(control_header_v2); @@ -843,13 +833,11 @@ namespace stream { plaintext.type = data.motion_type; BOOST_LOG(verbose) << "Motion event state: "sv << msg.id << " :: "sv << util::hex(data.report_rate).to_string_view() << " :: "sv << util::hex(data.motion_type).to_string_view(); - std::array + std::array encrypted_payload; payload = encode_control(session, util::view(plaintext), encrypted_payload); - } - else if (msg.type == platf::gamepad_feedback_e::set_rgb_led) { + } else if (msg.type == platf::gamepad_feedback_e::set_rgb_led) { control_set_rgb_led_t plaintext; plaintext.header.type = packetTypes[IDX_SET_RGB_LED]; plaintext.header.payloadLength = sizeof(plaintext) - sizeof(control_header_v2); @@ -862,13 +850,27 @@ namespace stream { plaintext.b = data.b; BOOST_LOG(verbose) << "RGB: "sv << msg.id << " :: "sv << util::hex(data.r).to_string_view() << util::hex(data.g).to_string_view() << util::hex(data.b).to_string_view(); - std::array + std::array encrypted_payload; payload = encode_control(session, util::view(plaintext), encrypted_payload); - } - else { + } else if (msg.type == platf::gamepad_feedback_e::set_adaptive_triggers) { + control_adaptive_triggers_t plaintext; + plaintext.header.type = packetTypes[IDX_SET_ADAPTIVE_TRIGGERS]; + plaintext.header.payloadLength = sizeof(plaintext) - sizeof(control_header_v2); + + plaintext.id = util::endian::little(msg.id); + plaintext.event_flags = msg.data.adaptive_triggers.event_flags; + plaintext.type_left = msg.data.adaptive_triggers.type_left; + std::ranges::copy(msg.data.adaptive_triggers.left, plaintext.left); + plaintext.type_right = msg.data.adaptive_triggers.type_right; + std::ranges::copy(msg.data.adaptive_triggers.right, plaintext.right); + + std::array + encrypted_payload; + + payload = encode_control(session, util::view(plaintext), encrypted_payload); + } else { BOOST_LOG(error) << "Unknown gamepad feedback message type"sv; return -1; } @@ -883,8 +885,7 @@ namespace stream { return 0; } - int - send_hdr_mode(session_t *session, video::hdr_info_t hdr_info) { + int send_hdr_mode(session_t *session, video::hdr_info_t hdr_info) { if (!session->control.peer) { BOOST_LOG(warning) << "Couldn't send HDR mode, still waiting for PING from Moonlight"sv; // Still waiting for PING from Moonlight @@ -898,8 +899,7 @@ namespace stream { plaintext.enabled = hdr_info->enabled; plaintext.metadata = hdr_info->metadata; - std::array + std::array encrypted_payload; auto payload = encode_control(session, util::view(plaintext), encrypted_payload); @@ -914,8 +914,7 @@ namespace stream { return 0; } - void - controlBroadcastThread(control_server_t *server) { + void controlBroadcastThread(control_server_t *server) { server->map(packetTypes[IDX_PERIODIC_PING], [](session_t *session, const std::string_view &payload) { BOOST_LOG(verbose) << "type [IDX_PERIODIC_PING]"sv; }); @@ -931,7 +930,7 @@ namespace stream { server->map(packetTypes[IDX_LOSS_STATS], [&](session_t *session, const std::string_view &payload) { int32_t *stats = (int32_t *) payload.data(); auto count = stats[0]; - std::chrono::milliseconds t { stats[1] }; + std::chrono::milliseconds t {stats[1]}; auto lastGoodFrame = stats[3]; @@ -967,7 +966,7 @@ namespace stream { BOOST_LOG(debug) << "type [IDX_INPUT_DATA]"sv; auto tagged_cipher_length = util::endian::big(*(int32_t *) payload.data()); - std::string_view tagged_cipher { payload.data() + sizeof(tagged_cipher_length), (size_t) tagged_cipher_length }; + std::string_view tagged_cipher {payload.data() + sizeof(tagged_cipher_length), (size_t) tagged_cipher_length}; std::vector plaintext; @@ -1003,7 +1002,7 @@ namespace stream { } auto tagged_cipher_length = length - 4; - std::string_view tagged_cipher { (char *) header->payload(), (size_t) tagged_cipher_length }; + std::string_view tagged_cipher {(char *) header->payload(), (size_t) tagged_cipher_length}; auto &cipher = session->control.cipher; auto &iv = session->control.incoming_iv; @@ -1020,8 +1019,7 @@ namespace stream { std::copy_n((uint8_t *) &seq, sizeof(seq), std::begin(iv)); iv[10] = 'C'; // Client originated iv[11] = 'C'; // Control stream - } - else { + } else { // Nvidia's old style encryption uses a 16-byte IV iv.resize(16); @@ -1039,7 +1037,7 @@ namespace stream { } auto type = *(std::uint16_t *) plaintext.data(); - std::string_view next_payload { (char *) plaintext.data() + 4, plaintext.size() - 4 }; + std::string_view next_payload {(char *) plaintext.data() + 4, plaintext.size() - 4}; if (type == packetTypes[IDX_ENCRYPTED]) { BOOST_LOG(error) << "Bad packet type [IDX_ENCRYPTED] found"sv; @@ -1051,8 +1049,7 @@ namespace stream { if (type == packetTypes[IDX_INPUT_DATA]) { plaintext.erase(std::begin(plaintext), std::begin(plaintext) + 4); input::passthrough(session->input, std::move(plaintext)); - } - else { + } else { server->call(type, session, next_payload, true); } }); @@ -1108,8 +1105,7 @@ namespace stream { // the app terminates before they finish connecting. if (!session->control.peer) { has_session_awaiting_peer = true; - } - else { + } else { auto &feedback_queue = session->control.feedback_queue; while (feedback_queue->peek()) { auto feedback_msg = feedback_queue->pop(); @@ -1147,8 +1143,7 @@ namespace stream { plaintext.header.payloadLength = sizeof(plaintext.ec); plaintext.ec = util::endian::big(reason); - std::array + std::array encrypted_payload; auto lg = server->_sessions.lock(); @@ -1172,8 +1167,7 @@ namespace stream { server->flush(); } - void - recvThread(broadcast_ctx_t &ctx) { + void recvThread(broadcast_ctx_t &ctx) { std::map peer_to_video_session; std::map peer_to_audio_session; @@ -1183,7 +1177,7 @@ namespace stream { auto &message_queue_queue = ctx.message_queue_queue; auto broadcast_shutdown_event = mail::man->event(mail::broadcast_shutdown); - auto &io = ctx.io; + auto &io = ctx.io_context; udp::endpoint peer; @@ -1199,16 +1193,14 @@ namespace stream { case socket_e::video: if (message_queue) { peer_to_video_session.emplace(session_id, message_queue); - } - else { + } else { peer_to_video_session.erase(session_id); } break; case socket_e::audio: if (message_queue) { peer_to_audio_session.emplace(session_id, message_queue); - } - else { + } else { peer_to_audio_session.erase(session_id); } break; @@ -1242,17 +1234,16 @@ namespace stream { auto it = peer_to_session.find(peer.address()); if (it != std::end(peer_to_session)) { BOOST_LOG(debug) << "RAISE: "sv << peer.address().to_string() << ':' << peer.port() << " :: " << type_str; - it->second->raise(peer, std::string { buf[buf_elem].data(), bytes }); + it->second->raise(peer, std::string {buf[buf_elem].data(), bytes}); } - } - else if (bytes >= sizeof(SS_PING)) { + } else if (bytes >= sizeof(SS_PING)) { auto ping = (PSS_PING) buf[buf_elem].data(); // For new PING packets that include a client identifier, search by payload. - auto it = peer_to_session.find(std::string { ping->payload, sizeof(ping->payload) }); + auto it = peer_to_session.find(std::string {ping->payload, sizeof(ping->payload)}); if (it != std::end(peer_to_session)) { BOOST_LOG(debug) << "RAISE: "sv << peer.address().to_string() << ':' << peer.port() << " :: " << type_str; - it->second->raise(peer, std::string { buf[buf_elem].data(), bytes }); + it->second->raise(peer, std::string {buf[buf_elem].data(), bytes}); } } }; @@ -1269,8 +1260,7 @@ namespace stream { } } - void - videoBroadcastThread(udp::socket &sock) { + void videoBroadcastThread(udp::socket &sock) { auto shutdown_event = mail::man->event(mail::broadcast_shutdown); auto packets = mail::man->queue(mail::video_packets); auto timebase = boost::posix_time::microsec_clock::universal_time(); @@ -1304,7 +1294,7 @@ namespace stream { auto session = (session_t *) packet->channel_data; auto lowseq = session->video.lowseq; - std::string_view payload { (char *) packet->data(), packet->data_size() }; + std::string_view payload {(char *) packet->data(), packet->data_size()}; std::vector payload_with_replacements; // Apply replacements on the packet payload before performing any other operations. @@ -1317,7 +1307,7 @@ namespace stream { auto frame_new = replacement._new; payload_with_replacements = replace(payload, frame_old, frame_new); - payload = { (char *) payload_with_replacements.data(), payload_with_replacements.size() }; + payload = {(char *) payload_with_replacements.data(), payload_with_replacements.size()}; } } @@ -1340,8 +1330,7 @@ namespace stream { uint16_t latency = duration_to_latency(std::chrono::steady_clock::now() - *packet->frame_timestamp); frame_header.frame_processing_latency = latency; frame_processing_latency_logger.collect_and_log(latency / 10.); - } - else { + } else { frame_header.frame_processing_latency = 0; } @@ -1350,10 +1339,9 @@ namespace stream { // Insert space for packet headers auto blocksize = session->config.packetsize + MAX_RTP_HEADER_SIZE; auto payload_blocksize = blocksize - sizeof(video_packet_raw_t); - auto payload_new = concat_and_insert(sizeof(video_packet_raw_t), payload_blocksize, - std::string_view { (char *) &frame_header, sizeof(frame_header) }, payload); + auto payload_new = concat_and_insert(sizeof(video_packet_raw_t), payload_blocksize, std::string_view {(char *) &frame_header, sizeof(frame_header)}, payload); - payload = std::string_view { (char *) payload_new.data(), payload_new.size() }; + payload = std::string_view {(char *) payload_new.data(), payload_new.size()}; // There are 2 bits for FEC block count for a maximum of 4 FEC blocks constexpr auto MAX_FEC_BLOCKS = 4; @@ -1401,8 +1389,7 @@ namespace stream { if (x == fec_blocks_needed - 1) { // The last block must extend to the end of the payload fec_blocks[x] = payload.substr(x * aligned_size); - } - else { + } else { // Earlier blocks just extend to the next block offset fec_blocks[x] = payload.substr(x * aligned_size, aligned_size); } @@ -1453,8 +1440,7 @@ namespace stream { frame_fec_latency_logger.first_point_now(); // If video encryption is enabled, we allocate space for the encryption header before each shard - auto shards = fec::encode(current_payload, blocksize, fecPercentage, session->config.minRequiredFecPackets, - session->video.cipher ? sizeof(video_packet_enc_prefix_t) : 0); + auto shards = fec::encode(current_payload, blocksize, fecPercentage, session->config.minRequiredFecPackets, session->video.cipher ? sizeof(video_packet_enc_prefix_t) : 0); frame_fec_latency_logger.second_point_now_and_log(); auto peer_address = session->video.peer.address(); @@ -1483,8 +1469,8 @@ namespace stream { inspect->packet.fecInfo = (x << 12 | - shards.data_shards << 22 | - shards.percentage << 4); + shards.data_shards << 22 | + shards.percentage << 4); inspect->rtp.header = 0x80 | FLAG_EXTENSION; inspect->rtp.sequenceNumber = util::endian::big(lowseq + x); @@ -1511,8 +1497,7 @@ namespace stream { auto *prefix = (video_packet_enc_prefix_t *) shards.prefix(x); prefix->frameNumber = packet->frame_index(); std::copy(std::begin(iv), std::end(iv), prefix->iv); - session->video.cipher->encrypt(std::string_view { (char *) inspect, (size_t) blocksize }, - prefix->tag, (uint8_t *) inspect, &iv); + session->video.cipher->encrypt(std::string_view {(char *) inspect, (size_t) blocksize}, prefix->tag, (uint8_t *) inspect, &iv); } if (x - next_shard_to_send + 1 >= send_batch_size || @@ -1575,8 +1560,7 @@ namespace stream { if (packet->is_idr()) { BOOST_LOG(verbose) << "Key Frame ["sv << packet->frame_index() << "] :: send ["sv << shards.size() << "] shards..."sv; - } - else { + } else { BOOST_LOG(verbose) << "Frame ["sv << packet->frame_index() << "] :: send ["sv << shards.size() << "] shards..."sv << std::endl; } @@ -1585,8 +1569,7 @@ namespace stream { }); session->video.lowseq = lowseq; - } - catch (const std::exception &e) { + } catch (const std::exception &e) { BOOST_LOG(error) << "Broadcast video failed "sv << e.what(); std::this_thread::sleep_for(100ms); } @@ -1595,13 +1578,12 @@ namespace stream { shutdown_event->raise(true); } - void - audioBroadcastThread(udp::socket &sock) { + void audioBroadcastThread(udp::socket &sock) { auto shutdown_event = mail::man->event(mail::broadcast_shutdown); auto packets = mail::man->queue(mail::audio_packets); audio_packet_t audio_packet; - fec::rs_t rs { reed_solomon_new(RTPA_DATA_SHARDS, RTPA_FEC_SHARDS) }; + fec::rs_t rs {reed_solomon_new(RTPA_DATA_SHARDS, RTPA_FEC_SHARDS)}; crypto::aes_t iv(16); // For unknown reasons, the RS parity matrix computed by our RS implementation @@ -1609,7 +1591,7 @@ namespace stream { // but we can simply replace it with the matrix generated by OpenFEC which // works correctly. This is possible because the data and FEC shard count is // constant and known in advance. - const unsigned char parity[] = { 0x77, 0x40, 0x38, 0x0e, 0xc7, 0xa7, 0x0d, 0x6c }; + const unsigned char parity[] = {0x77, 0x40, 0x38, 0x0e, 0xc7, 0xa7, 0x0d, 0x6c}; memcpy(rs.get()->p, parity, sizeof(parity)); audio_packet.rtp.header = 0x80; @@ -1634,8 +1616,7 @@ namespace stream { auto &shards_p = session->audio.shards_p; - auto bytes = encode_audio(session->config.encryptionFlagsEnabled & SS_ENC_AUDIO, packet_data, - shards_p[sequenceNumber % RTPA_DATA_SHARDS], iv, session->audio.cipher); + auto bytes = encode_audio(session->config.encryptionFlagsEnabled & SS_ENC_AUDIO, packet_data, shards_p[sequenceNumber % RTPA_DATA_SHARDS], iv, session->audio.cipher); if (bytes < 0) { BOOST_LOG(error) << "Couldn't encode audio packet"sv; break; @@ -1691,8 +1672,7 @@ namespace stream { BOOST_LOG(verbose) << "Audio FEC ["sv << (sequenceNumber & ~(RTPA_DATA_SHARDS - 1)) << ' ' << x << "] :: send..."sv; } } - } - catch (const std::exception &e) { + } catch (const std::exception &e) { BOOST_LOG(error) << "Broadcast audio failed "sv << e.what(); std::this_thread::sleep_for(100ms); } @@ -1701,8 +1681,7 @@ namespace stream { shutdown_event->raise(true); } - int - start_broadcast(broadcast_ctx_t &ctx) { + int start_broadcast(broadcast_ctx_t &ctx) { auto address_family = net::af_from_enum_string(config::sunshine.address_family); auto protocol = address_family == net::IPV4 ? udp::v4() : udp::v6(); auto control_port = net::map_port(CONTROL_PORT); @@ -1726,8 +1705,7 @@ namespace stream { // Set video socket send buffer size (SO_SENDBUF) to 1MB try { ctx.video_sock.set_option(boost::asio::socket_base::send_buffer_size(1024 * 1024)); - } - catch (...) { + } catch (...) { BOOST_LOG(error) << "Failed to set video socket send buffer size (SO_SENDBUF)"; } @@ -1754,17 +1732,16 @@ namespace stream { ctx.message_queue_queue = std::make_shared(30); - ctx.video_thread = std::thread { videoBroadcastThread, std::ref(ctx.video_sock) }; - ctx.audio_thread = std::thread { audioBroadcastThread, std::ref(ctx.audio_sock) }; - ctx.control_thread = std::thread { controlBroadcastThread, &ctx.control_server }; + ctx.video_thread = std::thread {videoBroadcastThread, std::ref(ctx.video_sock)}; + ctx.audio_thread = std::thread {audioBroadcastThread, std::ref(ctx.audio_sock)}; + ctx.control_thread = std::thread {controlBroadcastThread, &ctx.control_server}; - ctx.recv_thread = std::thread { recvThread, std::ref(ctx) }; + ctx.recv_thread = std::thread {recvThread, std::ref(ctx)}; return 0; } - void - end_broadcast(broadcast_ctx_t &ctx) { + void end_broadcast(broadcast_ctx_t &ctx) { auto broadcast_shutdown_event = mail::man->event(mail::broadcast_shutdown); broadcast_shutdown_event->raise(true); @@ -1777,7 +1754,7 @@ namespace stream { audio_packets->stop(); ctx.message_queue_queue->stop(); - ctx.io.stop(); + ctx.io_context.stop(); ctx.video_sock.close(); ctx.audio_sock.close(); @@ -1798,10 +1775,9 @@ namespace stream { broadcast_shutdown_event->reset(); } - int - recv_ping(session_t *session, decltype(broadcast)::ptr_t ref, socket_e type, std::string_view expected_payload, udp::endpoint &peer, std::chrono::milliseconds timeout) { + int recv_ping(session_t *session, decltype(broadcast)::ptr_t ref, socket_e type, std::string_view expected_payload, udp::endpoint &peer, std::chrono::milliseconds timeout) { auto messages = std::make_shared(30); - av_session_id_t session_id = std::string { expected_payload }; + av_session_id_t session_id = std::string {expected_payload}; // Only allow matches on the peer address for legacy clients if (!(session->config.mlFeatureFlags & ML_FF_SESSION_ID_V1)) { @@ -1834,12 +1810,10 @@ namespace stream { if (msg.find(expected_payload) != std::string::npos) { // Match the new PING payload format BOOST_LOG(debug) << "Received ping [v2] from "sv << recv_peer.address() << ':' << recv_peer.port() << " ["sv << util::hex_vec(msg) << ']'; - } - else if (!(session->config.mlFeatureFlags & ML_FF_SESSION_ID_V1) && msg == "PING"sv) { + } else if (!(session->config.mlFeatureFlags & ML_FF_SESSION_ID_V1) && msg == "PING"sv) { // Match the legacy fixed PING payload only if the new type is not supported BOOST_LOG(debug) << "Received ping [v1] from "sv << recv_peer.address() << ':' << recv_peer.port() << " ["sv << util::hex_vec(msg) << ']'; - } - else { + } else { BOOST_LOG(debug) << "Received non-ping from "sv << recv_peer.address() << ':' << recv_peer.port() << " ["sv << util::hex_vec(msg) << ']'; current_time = std::chrono::steady_clock::now(); continue; @@ -1854,8 +1828,7 @@ namespace stream { return -1; } - void - videoThread(session_t *session) { + void videoThread(session_t *session) { auto fg = util::fail_guard([&]() { session::stop(*session); }); @@ -1870,15 +1843,13 @@ namespace stream { // Enable local prioritization and QoS tagging on video traffic if requested by the client auto address = session->video.peer.address(); - session->video.qos = platf::enable_socket_qos(ref->video_sock.native_handle(), address, - session->video.peer.port(), platf::qos_data_type_e::video, session->config.videoQosType != 0); + session->video.qos = platf::enable_socket_qos(ref->video_sock.native_handle(), address, session->video.peer.port(), platf::qos_data_type_e::video, session->config.videoQosType != 0); BOOST_LOG(debug) << "Start capturing Video"sv; video::capture(session->mail, session->config.monitor, session); } - void - audioThread(session_t *session) { + void audioThread(session_t *session) { auto fg = util::fail_guard([&]() { session::stop(*session); }); @@ -1893,8 +1864,7 @@ namespace stream { // Enable local prioritization and QoS tagging on audio traffic if requested by the client auto address = session->audio.peer.address(); - session->audio.qos = platf::enable_socket_qos(ref->audio_sock.native_handle(), address, - session->audio.peer.port(), platf::qos_data_type_e::audio, session->config.audioQosType != 0); + session->audio.qos = platf::enable_socket_qos(ref->audio_sock.native_handle(), address, session->audio.peer.port(), platf::qos_data_type_e::audio, session->config.audioQosType != 0); BOOST_LOG(debug) << "Start capturing Audio"sv; audio::capture(session->mail, session->config.audio, session); @@ -1903,13 +1873,11 @@ namespace stream { namespace session { std::atomic_uint running_sessions; - state_e - state(session_t &session) { + state_e state(session_t &session) { return session.state.load(std::memory_order_relaxed); } - void - stop(session_t &session) { + void stop(session_t &session) { while_starting_do_nothing(session.state); auto expected = state_e::RUNNING; auto already_stopping = !session.state.compare_exchange_strong(expected, state_e::STOPPING); @@ -1920,8 +1888,7 @@ namespace stream { session.shutdown_event->raise(true); } - void - join(session_t &session) { + void join(session_t &session) { // Current Nvidia drivers have a bug where NVENC can deadlock the encoder thread with hardware-accelerated // GPU scheduling enabled. If this happens, we will terminate ourselves and the service can restart. // The alternative is that Sunshine can never start another session until it's manually restarted. @@ -1948,19 +1915,27 @@ namespace stream { // If this is the last session, invoke the platform callbacks if (--running_sessions == 0) { -#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 + bool revert_display_config {config::video.dd.config_revert_on_disconnect}; if (proc::proc.running()) { +#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 system_tray::update_tray_pausing(proc::proc.get_last_run_app_name()); - } #endif + } else { + // We have no app running and also no clients anymore. + revert_display_config = true; + } + + if (revert_display_config) { + display_device::revert_configuration(); + } + platf::streaming_will_stop(); } BOOST_LOG(debug) << "Session ended"sv; } - int - start(session_t &session, const std::string &addr_string) { + int start(session_t &session, const std::string &addr_string) { session.input = input::alloc(session.mail); session.broadcast_ref = broadcast.ref(); @@ -1986,8 +1961,8 @@ namespace stream { session.pingTimeout = std::chrono::steady_clock::now() + config::stream.ping_timeout; - session.audioThread = std::thread { audioThread, &session }; - session.videoThread = std::thread { videoThread, &session }; + session.audioThread = std::thread {audioThread, &session}; + session.videoThread = std::thread {videoThread, &session}; session.state.store(state_e::RUNNING, std::memory_order_relaxed); @@ -2002,8 +1977,7 @@ namespace stream { return 0; } - std::shared_ptr - alloc(config_t &config, rtsp_stream::launch_session_t &launch_session) { + std::shared_ptr alloc(config_t &config, rtsp_stream::launch_session_t &launch_session) { auto session = std::make_shared(); auto mail = std::make_shared(); @@ -2018,7 +1992,8 @@ namespace stream { session->control.hdr_queue = mail->event(mail::hdr); session->control.legacy_input_enc_iv = launch_session.iv; session->control.cipher = crypto::cipher::gcm_t { - launch_session.gcm_key, false + launch_session.gcm_key, + false }; session->video.idr_events = mail->event(mail::idr); @@ -2028,15 +2003,16 @@ namespace stream { if (config.encryptionFlagsEnabled & SS_ENC_VIDEO) { BOOST_LOG(info) << "Video encryption enabled"sv; session->video.cipher = crypto::cipher::gcm_t { - launch_session.gcm_key, false + launch_session.gcm_key, + false }; session->video.gcm_iv_counter = 0; } constexpr auto max_block_size = crypto::cipher::round_to_pkcs7_padded(2048); - util::buffer_t shards { RTPA_TOTAL_SHARDS * max_block_size }; - util::buffer_t shards_p { RTPA_TOTAL_SHARDS }; + util::buffer_t shards {RTPA_TOTAL_SHARDS * max_block_size}; + util::buffer_t shards_p {RTPA_TOTAL_SHARDS}; for (auto x = 0; x < RTPA_TOTAL_SHARDS; ++x) { shards_p[x] = (uint8_t *) &shards[x * max_block_size]; @@ -2056,7 +2032,8 @@ namespace stream { session->audio.fec_packet.fecHeader.ssrc = 0; session->audio.cipher = crypto::cipher::cbc_t { - launch_session.gcm_key, true + launch_session.gcm_key, + true }; session->audio.ping_payload = launch_session.av_ping_payload; diff --git a/src/stream.h b/src/stream.h index 95d1e2d4e88..53afff4fabe 100644 --- a/src/stream.h +++ b/src/stream.h @@ -3,10 +3,14 @@ * @brief Declarations for the streaming protocols. */ #pragma once + +// standard includes #include +// lib includes #include +// local includes #include "audio.h" #include "crypto.h" #include "video.h" @@ -17,6 +21,7 @@ namespace stream { constexpr auto AUDIO_STREAM_PORT = 11; struct session_t; + struct config_t { audio::config_t audio; video::config_t monitor; @@ -41,15 +46,10 @@ namespace stream { RUNNING, ///< The session is running }; - std::shared_ptr - alloc(config_t &config, rtsp_stream::launch_session_t &launch_session); - int - start(session_t &session, const std::string &addr_string); - void - stop(session_t &session); - void - join(session_t &session); - state_e - state(session_t &session); + std::shared_ptr alloc(config_t &config, rtsp_stream::launch_session_t &launch_session); + int start(session_t &session, const std::string &addr_string); + void stop(session_t &session); + void join(session_t &session); + state_e state(session_t &session); } // namespace session } // namespace stream diff --git a/src/sync.h b/src/sync.h index 5dbeaeb97d0..a9472a72d2a 100644 --- a/src/sync.h +++ b/src/sync.h @@ -4,29 +4,29 @@ */ #pragma once +// standard includes #include #include #include namespace sync_util { - template + template class sync_t { public: using value_t = T; using mutex_t = M; - std::lock_guard - lock() { - return std::lock_guard { _lock }; + std::lock_guard lock() { + return std::lock_guard {_lock}; } - template + template sync_t(Args &&...args): - raw { std::forward(args)... } {} + raw {std::forward(args)...} { + } - sync_t & - operator=(sync_t &&other) noexcept { + sync_t &operator=(sync_t &&other) noexcept { std::lock(_lock, other._lock); raw = std::move(other.raw); @@ -37,8 +37,7 @@ namespace sync_util { return *this; } - sync_t & - operator=(sync_t &other) noexcept { + sync_t &operator=(sync_t &other) noexcept { std::lock(_lock, other._lock); raw = other.raw; @@ -49,9 +48,8 @@ namespace sync_util { return *this; } - template - sync_t & - operator=(V &&val) { + template + sync_t &operator=(V &&val) { auto lg = lock(); raw = val; @@ -59,8 +57,7 @@ namespace sync_util { return *this; } - sync_t & - operator=(const value_t &val) noexcept { + sync_t &operator=(const value_t &val) noexcept { auto lg = lock(); raw = val; @@ -68,8 +65,7 @@ namespace sync_util { return *this; } - sync_t & - operator=(value_t &&val) noexcept { + sync_t &operator=(value_t &&val) noexcept { auto lg = lock(); raw = std::move(val); @@ -77,18 +73,15 @@ namespace sync_util { return *this; } - value_t * - operator->() { + value_t *operator->() { return &raw; } - value_t & - operator*() { + value_t &operator*() { return raw; } - const value_t & - operator*() const { + const value_t &operator*() const { return raw; } diff --git a/src/system_tray.cpp b/src/system_tray.cpp index 95edb67da4b..621a809c0b8 100644 --- a/src/system_tray.cpp +++ b/src/system_tray.cpp @@ -14,10 +14,10 @@ #define TRAY_ICON_PAUSING WEB_DIR "images/sunshine-pausing.ico" #define TRAY_ICON_LOCKED WEB_DIR "images/sunshine-locked.ico" #elif defined(__linux__) || defined(linux) || defined(__linux) - #define TRAY_ICON "sunshine-tray" - #define TRAY_ICON_PLAYING "sunshine-playing" - #define TRAY_ICON_PAUSING "sunshine-pausing" - #define TRAY_ICON_LOCKED "sunshine-locked" + #define TRAY_ICON SUNSHINE_TRAY_PREFIX "-tray" + #define TRAY_ICON_PLAYING SUNSHINE_TRAY_PREFIX "-playing" + #define TRAY_ICON_PAUSING SUNSHINE_TRAY_PREFIX "-pausing" + #define TRAY_ICON_LOCKED SUNSHINE_TRAY_PREFIX "-locked" #elif defined(__APPLE__) || defined(__MACH__) #define TRAY_ICON WEB_DIR "images/logo-sunshine-16.png" #define TRAY_ICON_PLAYING WEB_DIR "images/sunshine-playing-16.png" @@ -31,12 +31,13 @@ #include // lib includes - #include "tray/src/tray.h" #include #include + #include // local includes #include "confighttp.h" + #include "display_device.h" #include "logging.h" #include "platform/common.h" #include "process.h" @@ -49,36 +50,36 @@ using namespace std::literals; namespace system_tray { static std::atomic tray_initialized = false; - void - tray_open_ui_cb(struct tray_menu *item) { + void tray_open_ui_cb(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(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(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(struct tray_menu *item) { platf::open_url("https://www.paypal.com/paypalme/ReenigneArcher"); } - void - tray_restart_cb(struct tray_menu *item) { + void tray_reset_display_device_config_cb(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) { BOOST_LOG(info) << "Restarting from system tray"sv; platf::restart(); } - void - tray_quit_cb(struct tray_menu *item) { + void tray_quit_cb(struct tray_menu *item) { BOOST_LOG(info) << "Quitting from system tray"sv; #ifdef _WIN32 @@ -100,25 +101,30 @@ namespace system_tray { .menu = (struct tray_menu[]) { // todo - use boost/locale to translate menu strings - { .text = "Open Sunshine", .cb = tray_open_ui_cb }, - { .text = "-" }, - { .text = "Donate", - .submenu = - (struct tray_menu[]) { - { .text = "GitHub Sponsors", .cb = tray_donate_github_cb }, - { .text = "Patreon", .cb = tray_donate_patreon_cb }, - { .text = "PayPal", .cb = tray_donate_paypal_cb }, - { .text = nullptr } } }, - { .text = "-" }, - { .text = "Restart", .cb = tray_restart_cb }, - { .text = "Quit", .cb = tray_quit_cb }, - { .text = nullptr } }, + {.text = "Open Sunshine", .cb = tray_open_ui_cb}, + {.text = "-"}, + {.text = "Donate", + .submenu = + (struct tray_menu[]) { + {.text = "GitHub Sponsors", .cb = tray_donate_github_cb}, + {.text = "Patreon", .cb = tray_donate_patreon_cb}, + {.text = "PayPal", .cb = tray_donate_paypal_cb}, + {.text = nullptr} + }}, + {.text = "-"}, + // Currently display device settings are only supported on Windows + #ifdef _WIN32 + {.text = "Reset Display Device Config", .cb = tray_reset_display_device_config_cb}, + #endif + {.text = "Restart", .cb = tray_restart_cb}, + {.text = "Quit", .cb = tray_quit_cb}, + {.text = nullptr} + }, .iconPathCount = 4, - .allIconPaths = { TRAY_ICON, TRAY_ICON_LOCKED, TRAY_ICON_PLAYING, TRAY_ICON_PAUSING }, + .allIconPaths = {TRAY_ICON, TRAY_ICON_LOCKED, TRAY_ICON_PLAYING, TRAY_ICON_PAUSING}, }; - int - system_tray() { + int system_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 @@ -127,14 +133,7 @@ namespace system_tray { { PACL old_dacl; PSECURITY_DESCRIPTOR sd; - auto error = GetSecurityInfo(GetCurrentThread(), - SE_KERNEL_OBJECT, - DACL_SECURITY_INFORMATION, - nullptr, - nullptr, - &old_dacl, - nullptr, - &sd); + auto error = GetSecurityInfo(GetCurrentThread(), SE_KERNEL_OBJECT, DACL_SECURITY_INFORMATION, nullptr, nullptr, &old_dacl, nullptr, &sd); if (error != ERROR_SUCCESS) { BOOST_LOG(warning) << "GetSecurityInfo() failed: "sv << error; return 1; @@ -174,13 +173,7 @@ namespace system_tray { LocalFree(new_dacl); }); - error = SetSecurityInfo(GetCurrentThread(), - SE_KERNEL_OBJECT, - DACL_SECURITY_INFORMATION, - nullptr, - nullptr, - new_dacl, - nullptr); + error = SetSecurityInfo(GetCurrentThread(), SE_KERNEL_OBJECT, DACL_SECURITY_INFORMATION, nullptr, nullptr, new_dacl, nullptr); if (error != ERROR_SUCCESS) { BOOST_LOG(warning) << "SetSecurityInfo() failed: "sv << error; return 1; @@ -197,8 +190,7 @@ namespace system_tray { if (tray_init(&tray) < 0) { BOOST_LOG(warning) << "Failed to create system tray"sv; return 1; - } - else { + } else { BOOST_LOG(info) << "System tray created"sv; } @@ -210,8 +202,7 @@ namespace system_tray { return 0; } - void - run_tray() { + void run_tray() { // create the system tray #if defined(__APPLE__) || defined(__MACH__) // macOS requires that UI elements be created on the main thread @@ -229,15 +220,13 @@ namespace system_tray { #endif } - int - end_tray() { + int end_tray() { tray_initialized = false; tray_exit(); return 0; } - void - update_tray_playing(std::string app_name) { + void update_tray_playing(std::string app_name) { if (!tray_initialized) { return; } @@ -258,8 +247,7 @@ namespace system_tray { tray_update(&tray); } - void - update_tray_pausing(std::string app_name) { + void update_tray_pausing(std::string app_name) { if (!tray_initialized) { return; } @@ -280,8 +268,7 @@ namespace system_tray { tray_update(&tray); } - void - update_tray_stopped(std::string app_name) { + void update_tray_stopped(std::string app_name) { if (!tray_initialized) { return; } @@ -302,8 +289,7 @@ namespace system_tray { tray_update(&tray); } - void - update_tray_require_pin() { + void update_tray_require_pin() { if (!tray_initialized) { return; } diff --git a/src/system_tray.h b/src/system_tray.h index d027fb45922..00a17f92cf4 100644 --- a/src/system_tray.h +++ b/src/system_tray.h @@ -12,90 +12,83 @@ 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(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(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(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(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); /** * @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(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(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. */ - int - system_tray(); + int system_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. */ - int - run_tray(); + int run_tray(); /** * @brief Exit the system tray. * @return 0 after exiting the system tray. */ - int - end_tray(); + int end_tray(); /** * @brief Sets the tray icon in playing mode and spawns the appropriate notification * @param app_name The started application name */ - void - update_tray_playing(std::string app_name); + void update_tray_playing(std::string app_name); /** * @brief Sets the tray icon in pausing mode (stream stopped but app running) and spawns the appropriate notification * @param app_name The paused application name */ - void - update_tray_pausing(std::string app_name); + void update_tray_pausing(std::string app_name); /** * @brief Sets the tray icon in stopped mode (app and stream stopped) and spawns the appropriate notification * @param app_name The started application name */ - void - update_tray_stopped(std::string app_name); + void update_tray_stopped(std::string app_name); /** * @brief Spawns a notification for PIN Pairing. Clicking it opens the PIN Web UI Page */ - void - update_tray_require_pin(); + void update_tray_require_pin(); } // namespace system_tray diff --git a/src/task_pool.h b/src/task_pool.h index 3d6b1b5d38a..081be24d069 100644 --- a/src/task_pool.h +++ b/src/task_pool.h @@ -4,6 +4,7 @@ */ #pragma once +// standard includes #include #include #include @@ -14,8 +15,10 @@ #include #include +// local includes #include "move_by_copy.h" #include "utility.h" + namespace task_pool_util { class _ImplBase { @@ -24,20 +27,19 @@ namespace task_pool_util { inline virtual ~_ImplBase() = default; - virtual void - run() = 0; + virtual void run() = 0; }; - template + template class _Impl: public _ImplBase { Function _func; public: _Impl(Function &&f): - _func(std::forward(f)) {} + _func(std::forward(f)) { + } - void - run() override { + void run() override { _func(); } }; @@ -49,14 +51,16 @@ namespace task_pool_util { typedef std::chrono::steady_clock::time_point __time_point; - template + template class timer_task_t { public: task_id_t task_id; std::future future; timer_task_t(task_id_t task_id, std::future &future): - task_id { task_id }, future { std::move(future) } {} + task_id {task_id}, + future {std::move(future)} { + } }; protected: @@ -66,20 +70,21 @@ namespace task_pool_util { public: TaskPool() = default; + TaskPool(TaskPool &&other) noexcept: - _tasks { std::move(other._tasks) }, _timer_tasks { std::move(other._timer_tasks) } {} + _tasks {std::move(other._tasks)}, + _timer_tasks {std::move(other._timer_tasks)} { + } - TaskPool & - operator=(TaskPool &&other) noexcept { + TaskPool &operator=(TaskPool &&other) noexcept { std::swap(_tasks, other._tasks); std::swap(_timer_tasks, other._timer_tasks); return *this; } - template - auto - push(Function &&newTask, Args &&...args) { + template + auto push(Function &&newTask, Args &&...args) { static_assert(std::is_invocable_v, "arguments don't match the function"); using __return = std::invoke_result_t; @@ -99,8 +104,7 @@ namespace task_pool_util { return future; } - void - pushDelayed(std::pair<__time_point, __task> &&task) { + void pushDelayed(std::pair<__time_point, __task> &&task) { std::lock_guard lg(_task_mutex); auto it = _timer_tasks.cbegin(); @@ -116,9 +120,8 @@ namespace task_pool_util { /** * @return An id to potentially delay the task. */ - template - auto - pushDelayed(Function &&newTask, std::chrono::duration duration, Args &&...args) { + template + auto pushDelayed(Function &&newTask, std::chrono::duration duration, Args &&...args) { static_assert(std::is_invocable_v, "arguments don't match the function"); using __return = std::invoke_result_t; @@ -127,8 +130,7 @@ namespace task_pool_util { __time_point time_point; if constexpr (std::is_floating_point_v) { time_point = std::chrono::steady_clock::now() + std::chrono::duration_cast(duration); - } - else { + } else { time_point = std::chrono::steady_clock::now() + duration; } @@ -143,18 +145,17 @@ namespace task_pool_util { task_id_t task_id = &*runnable; - pushDelayed(std::pair { time_point, std::move(runnable) }); + pushDelayed(std::pair {time_point, std::move(runnable)}); - return timer_task_t<__return> { task_id, future }; + return timer_task_t<__return> {task_id, future}; } /** * @param task_id The id of the task to delay. * @param duration The delay before executing the task. */ - template - void - delay(task_id_t task_id, std::chrono::duration duration) { + template + void delay(task_id_t task_id, std::chrono::duration duration) { std::lock_guard lg(_task_mutex); auto it = _timer_tasks.begin(); @@ -184,8 +185,7 @@ namespace task_pool_util { } } - bool - cancel(task_id_t task_id) { + bool cancel(task_id_t task_id) { std::lock_guard lg(_task_mutex); auto it = _timer_tasks.begin(); @@ -202,11 +202,12 @@ namespace task_pool_util { return false; } - std::optional> - pop(task_id_t task_id) { + std::optional> pop(task_id_t task_id) { std::lock_guard lg(_task_mutex); - auto pos = std::find_if(std::begin(_timer_tasks), std::end(_timer_tasks), [&task_id](const auto &t) { return t.second.get() == task_id; }); + auto pos = std::find_if(std::begin(_timer_tasks), std::end(_timer_tasks), [&task_id](const auto &t) { + return t.second.get() == task_id; + }); if (pos == std::end(_timer_tasks)) { return std::nullopt; @@ -215,8 +216,7 @@ namespace task_pool_util { return std::move(*pos); } - std::optional<__task> - pop() { + std::optional<__task> pop() { std::lock_guard lg(_task_mutex); if (!_tasks.empty()) { @@ -234,15 +234,13 @@ namespace task_pool_util { return std::nullopt; } - bool - ready() { + bool ready() { std::lock_guard lg(_task_mutex); return !_tasks.empty() || (!_timer_tasks.empty() && std::get<0>(_timer_tasks.back()) <= std::chrono::steady_clock::now()); } - std::optional<__time_point> - next() { + std::optional<__time_point> next() { std::lock_guard lg(_task_mutex); if (_timer_tasks.empty()) { @@ -253,9 +251,8 @@ namespace task_pool_util { } private: - template - std::unique_ptr<_ImplBase> - toRunnable(Function &&f) { + template + std::unique_ptr<_ImplBase> toRunnable(Function &&f) { return std::make_unique<_Impl>(std::forward(f)); } }; diff --git a/src/thread_pool.h b/src/thread_pool.h index 25b3d267835..e6377b2a018 100644 --- a/src/thread_pool.h +++ b/src/thread_pool.h @@ -4,9 +4,12 @@ */ #pragma once -#include "task_pool.h" +// standard includes #include +// local includes +#include "task_pool.h" + namespace thread_pool_util { /** * Allow threads to execute unhindered while keeping full control over the threads. @@ -25,25 +28,28 @@ namespace thread_pool_util { public: ThreadPool(): - _continue { false } {} + _continue {false} { + } explicit ThreadPool(int threads): - _thread(threads), _continue { true } { + _thread(threads), + _continue {true} { for (auto &t : _thread) { t = std::thread(&ThreadPool::_main, this); } } ~ThreadPool() noexcept { - if (!_continue) return; + if (!_continue) { + return; + } stop(); join(); } - template - auto - push(Function &&newTask, Args &&...args) { + template + auto push(Function &&newTask, Args &&...args) { std::lock_guard lg(_lock); auto future = TaskPool::push(std::forward(newTask), std::forward(args)...); @@ -51,16 +57,14 @@ namespace thread_pool_util { return future; } - void - pushDelayed(std::pair<__time_point, __task> &&task) { + void pushDelayed(std::pair<__time_point, __task> &&task) { std::lock_guard lg(_lock); TaskPool::pushDelayed(std::move(task)); } - template - auto - pushDelayed(Function &&newTask, std::chrono::duration duration, Args &&...args) { + template + auto pushDelayed(Function &&newTask, std::chrono::duration duration, Args &&...args) { std::lock_guard lg(_lock); auto future = TaskPool::pushDelayed(std::forward(newTask), duration, std::forward(args)...); @@ -69,8 +73,7 @@ namespace thread_pool_util { return future; } - void - start(int threads) { + void start(int threads) { _continue = true; _thread.resize(threads); @@ -80,29 +83,25 @@ namespace thread_pool_util { } } - void - stop() { + void stop() { std::lock_guard lg(_lock); _continue = false; _cv.notify_all(); } - void - join() { + void join() { for (auto &t : _thread) { t.join(); } } public: - void - _main() { + void _main() { while (_continue) { if (auto task = this->pop()) { (*task)->run(); - } - else { + } else { std::unique_lock uniq_lock(_lock); if (ready()) { @@ -115,8 +114,7 @@ namespace thread_pool_util { if (auto tp = next()) { _cv.wait_until(uniq_lock, *tp); - } - else { + } else { _cv.wait(uniq_lock); } } diff --git a/src/thread_safe.h b/src/thread_safe.h index 9d8ce8a25be..9657de06371 100644 --- a/src/thread_safe.h +++ b/src/thread_safe.h @@ -4,6 +4,7 @@ */ #pragma once +// standard includes #include #include #include @@ -12,36 +13,34 @@ #include #include +// local includes #include "utility.h" namespace safe { - template + template class event_t { public: using status_t = util::optional_t; - template - void - raise(Args &&...args) { - std::lock_guard lg { _lock }; + template + void raise(Args &&...args) { + std::lock_guard lg {_lock}; if (!_continue) { return; } if constexpr (std::is_same_v, status_t>) { _status = std::make_optional(std::forward(args)...); - } - else { - _status = status_t { std::forward(args)... }; + } else { + _status = status_t {std::forward(args)...}; } _cv.notify_all(); } // pop and view should not be used interchangeably - status_t - pop() { - std::unique_lock ul { _lock }; + status_t pop() { + std::unique_lock ul {_lock}; if (!_continue) { return util::false_v; @@ -61,10 +60,9 @@ namespace safe { } // pop and view should not be used interchangeably - template - status_t - pop(std::chrono::duration delay) { - std::unique_lock ul { _lock }; + template + status_t pop(std::chrono::duration delay) { + std::unique_lock ul {_lock}; if (!_continue) { return util::false_v; @@ -82,9 +80,8 @@ namespace safe { } // pop and view should not be used interchangeably - status_t - view() { - std::unique_lock ul { _lock }; + status_t view() { + std::unique_lock ul {_lock}; if (!_continue) { return util::false_v; @@ -102,10 +99,9 @@ namespace safe { } // pop and view should not be used interchangeably - template - status_t - view(std::chrono::duration delay) { - std::unique_lock ul { _lock }; + template + status_t view(std::chrono::duration delay) { + std::unique_lock ul {_lock}; if (!_continue) { return util::false_v; @@ -120,49 +116,44 @@ namespace safe { return _status; } - bool - peek() { + bool peek() { return _continue && (bool) _status; } - void - stop() { - std::lock_guard lg { _lock }; + void stop() { + std::lock_guard lg {_lock}; _continue = false; _cv.notify_all(); } - void - reset() { - std::lock_guard lg { _lock }; + void reset() { + std::lock_guard lg {_lock}; _continue = true; _status = util::false_v; } - [[nodiscard]] bool - running() const { + [[nodiscard]] bool running() const { return _continue; } private: - bool _continue { true }; - status_t _status { util::false_v }; + bool _continue {true}; + status_t _status {util::false_v}; std::condition_variable _cv; std::mutex _lock; }; - template + template class alarm_raw_t { public: using status_t = util::optional_t; - void - ring(const status_t &status) { + void ring(const status_t &status) { std::lock_guard lg(_lock); _status = status; @@ -170,8 +161,7 @@ namespace safe { _cv.notify_one(); } - void - ring(status_t &&status) { + void ring(status_t &&status) { std::lock_guard lg(_lock); _status = std::move(status); @@ -179,63 +169,66 @@ namespace safe { _cv.notify_one(); } - template - auto - wait_for(const std::chrono::duration &rel_time) { + template + auto wait_for(const std::chrono::duration &rel_time) { std::unique_lock ul(_lock); - return _cv.wait_for(ul, rel_time, [this]() { return _rang; }); + return _cv.wait_for(ul, rel_time, [this]() { + return _rang; + }); } - template - auto - wait_for(const std::chrono::duration &rel_time, Pred &&pred) { + template + auto wait_for(const std::chrono::duration &rel_time, Pred &&pred) { std::unique_lock ul(_lock); - return _cv.wait_for(ul, rel_time, [this, &pred]() { return _rang || pred(); }); + return _cv.wait_for(ul, rel_time, [this, &pred]() { + return _rang || pred(); + }); } - template - auto - wait_until(const std::chrono::duration &rel_time) { + template + auto wait_until(const std::chrono::duration &rel_time) { std::unique_lock ul(_lock); - return _cv.wait_until(ul, rel_time, [this]() { return _rang; }); + return _cv.wait_until(ul, rel_time, [this]() { + return _rang; + }); } - template - auto - wait_until(const std::chrono::duration &rel_time, Pred &&pred) { + template + auto wait_until(const std::chrono::duration &rel_time, Pred &&pred) { std::unique_lock ul(_lock); - return _cv.wait_until(ul, rel_time, [this, &pred]() { return _rang || pred(); }); + return _cv.wait_until(ul, rel_time, [this, &pred]() { + return _rang || pred(); + }); } - auto - wait() { + auto wait() { std::unique_lock ul(_lock); - _cv.wait(ul, [this]() { return _rang; }); + _cv.wait(ul, [this]() { + return _rang; + }); } - template - auto - wait(Pred &&pred) { + template + auto wait(Pred &&pred) { std::unique_lock ul(_lock); - _cv.wait(ul, [this, &pred]() { return _rang || pred(); }); + _cv.wait(ul, [this, &pred]() { + return _rang || pred(); + }); } - const status_t & - status() const { + const status_t &status() const { return _status; } - status_t & - status() { + status_t &status() { return _status; } - void - reset() { + void reset() { _status = status_t {}; _rang = false; } @@ -244,31 +237,30 @@ namespace safe { std::mutex _lock; std::condition_variable _cv; - status_t _status { util::false_v }; - bool _rang { false }; + status_t _status {util::false_v}; + bool _rang {false}; }; - template + template using alarm_t = std::shared_ptr>; - template - alarm_t - make_alarm() { + template + alarm_t make_alarm() { return std::make_shared>(); } - template + template class queue_t { public: using status_t = util::optional_t; queue_t(std::uint32_t max_elements = 32): - _max_elements { max_elements } {} + _max_elements {max_elements} { + } - template - void - raise(Args &&...args) { - std::lock_guard ul { _lock }; + template + void raise(Args &&...args) { + std::lock_guard ul {_lock}; if (!_continue) { return; @@ -283,15 +275,13 @@ namespace safe { _cv.notify_all(); } - bool - peek() { + bool peek() { return _continue && !_queue.empty(); } - template - status_t - pop(std::chrono::duration delay) { - std::unique_lock ul { _lock }; + template + status_t pop(std::chrono::duration delay) { + std::unique_lock ul {_lock}; if (!_continue) { return util::false_v; @@ -309,9 +299,8 @@ namespace safe { return val; } - status_t - pop() { - std::unique_lock ul { _lock }; + status_t pop() { + std::unique_lock ul {_lock}; if (!_continue) { return util::false_v; @@ -331,27 +320,24 @@ namespace safe { return val; } - std::vector & - unsafe() { + std::vector &unsafe() { return _queue; } - void - stop() { - std::lock_guard lg { _lock }; + void stop() { + std::lock_guard lg {_lock}; _continue = false; _cv.notify_all(); } - [[nodiscard]] bool - running() const { + [[nodiscard]] bool running() const { return _continue; } private: - bool _continue { true }; + bool _continue {true}; std::uint32_t _max_elements; std::mutex _lock; @@ -360,7 +346,7 @@ namespace safe { std::vector _queue; }; - template + template class shared_t { public: using element_type = T; @@ -372,17 +358,20 @@ namespace safe { shared_t *owner; ptr_t(): - owner { nullptr } {} + owner {nullptr} { + } + explicit ptr_t(shared_t *owner): - owner { owner } {} + owner {owner} { + } ptr_t(ptr_t &&ptr) noexcept: - owner { ptr.owner } { + owner {ptr.owner} { ptr.owner = nullptr; } ptr_t(const ptr_t &ptr) noexcept: - owner { ptr.owner } { + owner {ptr.owner} { if (!owner) { return; } @@ -391,8 +380,7 @@ namespace safe { tmp.owner = nullptr; } - ptr_t & - operator=(const ptr_t &ptr) noexcept { + ptr_t &operator=(const ptr_t &ptr) noexcept { if (!ptr.owner) { release(); @@ -402,8 +390,7 @@ namespace safe { return *this = std::move(*ptr.owner->ref()); } - ptr_t & - operator=(ptr_t &&ptr) noexcept { + ptr_t &operator=(ptr_t &&ptr) noexcept { if (owner) { release(); } @@ -423,9 +410,8 @@ namespace safe { return owner != nullptr; } - void - release() { - std::lock_guard lg { owner->_lock }; + void release() { + std::lock_guard lg {owner->_lock}; if (!--owner->_count) { owner->_destruct(*get()); @@ -435,34 +421,34 @@ namespace safe { owner = nullptr; } - element_type * - get() const { + element_type *get() const { return reinterpret_cast(owner->_object_buf.data()); } - element_type * - operator->() { + element_type *operator->() { return reinterpret_cast(owner->_object_buf.data()); } }; - template + template shared_t(FC &&fc, FD &&fd): - _construct { std::forward(fc) }, _destruct { std::forward(fd) } {} - [[nodiscard]] ptr_t - ref() { - std::lock_guard lg { _lock }; + _construct {std::forward(fc)}, + _destruct {std::forward(fd)} { + } + + [[nodiscard]] ptr_t ref() { + std::lock_guard lg {_lock}; if (!_count) { new (_object_buf.data()) element_type; if (_construct(*reinterpret_cast(_object_buf.data()))) { - return ptr_t { nullptr }; + return ptr_t {nullptr}; } } ++_count; - return ptr_t { this }; + return ptr_t {this}; } private: @@ -475,11 +461,11 @@ namespace safe { std::mutex _lock; }; - template - auto - make_shared(F_Construct &&fc, F_Destruct &&fd) { + template + auto make_shared(F_Construct &&fc, F_Destruct &&fd) { return shared_t { - std::forward(fc), std::forward(fd) + std::forward(fc), + std::forward(fd) }; } @@ -488,14 +474,16 @@ namespace safe { class mail_raw_t; using mail_t = std::shared_ptr; - void - cleanup(mail_raw_t *); - template + void cleanup(mail_raw_t *); + + template class post_t: public T { public: - template + template post_t(mail_t mail, Args &&...args): - T(std::forward(args)...), mail { std::move(mail) } {} + T(std::forward(args)...), + mail {std::move(mail)} { + } mail_t mail; @@ -504,24 +492,22 @@ namespace safe { } }; - template - inline auto - lock(const std::weak_ptr &wp) { + template + inline auto lock(const std::weak_ptr &wp) { return std::reinterpret_pointer_cast(wp.lock()); } class mail_raw_t: public std::enable_shared_from_this { public: - template + template using event_t = std::shared_ptr>>; - template + template using queue_t = std::shared_ptr>>; - template - event_t - event(const std::string_view &id) { - std::lock_guard lg { mutex }; + template + event_t event(const std::string_view &id) { + std::lock_guard lg {mutex}; auto it = id_to_post.find(id); if (it != std::end(id_to_post)) { @@ -529,15 +515,14 @@ namespace safe { } auto post = std::make_shared::element_type>(shared_from_this()); - id_to_post.emplace(std::pair> { std::string { id }, post }); + id_to_post.emplace(std::pair> {std::string {id}, post}); return post; } - template - queue_t - queue(const std::string_view &id) { - std::lock_guard lg { mutex }; + template + queue_t queue(const std::string_view &id) { + std::lock_guard lg {mutex}; auto it = id_to_post.find(id); if (it != std::end(id_to_post)) { @@ -545,14 +530,13 @@ namespace safe { } auto post = std::make_shared::element_type>(shared_from_this(), 32); - id_to_post.emplace(std::pair> { std::string { id }, post }); + id_to_post.emplace(std::pair> {std::string {id}, post}); return post; } - void - cleanup() { - std::lock_guard lg { mutex }; + void cleanup() { + std::lock_guard lg {mutex}; for (auto it = std::begin(id_to_post); it != std::end(id_to_post); ++it) { auto &weak = it->second; @@ -570,8 +554,7 @@ namespace safe { std::map, std::less<>> id_to_post; }; - inline void - cleanup(mail_raw_t *mail) { + inline void cleanup(mail_raw_t *mail) { mail->cleanup(); } } // namespace safe diff --git a/src/upnp.cpp b/src/upnp.cpp index 747e604a332..14103ad52c6 100644 --- a/src/upnp.cpp +++ b/src/upnp.cpp @@ -2,9 +2,11 @@ * @file src/upnp.cpp * @brief Definitions for UPnP port mapping. */ +// lib includes #include #include +// local includes #include "config.h" #include "confighttp.h" #include "globals.h" @@ -30,8 +32,7 @@ namespace upnp { std::string description; }; - static std::string_view - status_string(int status) { + static std::string_view status_string(int status) { switch (status) { case 0: return "No IGD device found"sv; @@ -50,8 +51,7 @@ namespace upnp { * This function is a wrapper around UPNP_GetValidIGD() that returns the status code. There is a pre-processor * check to determine which version of the function to call based on the version of the MiniUPnPc library. */ - int - UPNP_GetValidIGDStatus(device_t &device, urls_t *urls, IGDdatas *data, std::array &lan_addr) { + int UPNP_GetValidIGDStatus(device_t &device, urls_t *urls, IGDdatas *data, std::array &lan_addr) { #if (MINIUPNPC_API_VERSION >= 18) return UPNP_GetValidIGD(device.get(), &urls->el, data, lan_addr.data(), lan_addr.size(), nullptr, 0); #else @@ -71,21 +71,21 @@ namespace upnp { auto wm_http = std::to_string(net::map_port(confighttp::PORT_HTTPS)); mappings.assign({ - { { rtsp, rtsp, "TCP"s }, "Sunshine - RTSP"s }, - { { video, video, "UDP"s }, "Sunshine - Video"s }, - { { audio, audio, "UDP"s }, "Sunshine - Audio"s }, - { { control, control, "UDP"s }, "Sunshine - Control"s }, - { { gs_http, gs_http, "TCP"s }, "Sunshine - Client HTTP"s }, - { { gs_https, gs_https, "TCP"s }, "Sunshine - Client HTTPS"s }, + {{rtsp, rtsp, "TCP"s}, "Sunshine - RTSP"s}, + {{video, video, "UDP"s}, "Sunshine - Video"s}, + {{audio, audio, "UDP"s}, "Sunshine - Audio"s}, + {{control, control, "UDP"s}, "Sunshine - Control"s}, + {{gs_http, gs_http, "TCP"s}, "Sunshine - Client HTTP"s}, + {{gs_https, gs_https, "TCP"s}, "Sunshine - Client HTTPS"s}, }); // Only map port for the Web Manager if it is configured to accept connection from WAN if (net::from_enum_string(config::nvhttp.origin_web_ui_allowed) > net::LAN) { - mappings.emplace_back(mapping_t { { wm_http, wm_http, "TCP"s }, "Sunshine - Web UI"s }); + mappings.emplace_back(mapping_t {{wm_http, wm_http, "TCP"s}, "Sunshine - Web UI"s}); } // Start the mapping thread - upnp_thread = std::thread { &deinit_t::upnp_thread_proc, this }; + upnp_thread = std::thread {&deinit_t::upnp_thread_proc, this}; } ~deinit_t() { @@ -97,10 +97,9 @@ namespace upnp { * @details Not many IGDs support this feature, so we perform error logging with debug level. * @return `true` if the pinholes were opened successfully. */ - bool - create_ipv6_pinholes() { + bool create_ipv6_pinholes() { int err; - device_t device { upnpDiscover(2000, nullptr, nullptr, 0, IPv6, 2, &err) }; + device_t device {upnpDiscover(2000, nullptr, nullptr, 0, IPv6, 2, &err)}; if (!device || err) { BOOST_LOG(debug) << "Couldn't discover any IPv6 UPNP devices"sv; return false; @@ -136,35 +135,24 @@ namespace upnp { char uniqueId[8]; // Open a pinhole for the LAN port, since there will be no WAN->LAN port mapping on IPv6 - err = UPNP_AddPinhole(urls->controlURL_6FC, - data.IPv6FC.servicetype, - "", "0", - lan_addr.data(), - mapping.port.lan.c_str(), - mapping.port.proto.c_str(), - mapping_period.c_str(), - uniqueId); + err = UPNP_AddPinhole(urls->controlURL_6FC, data.IPv6FC.servicetype, "", "0", lan_addr.data(), mapping.port.lan.c_str(), mapping.port.proto.c_str(), mapping_period.c_str(), uniqueId); if (err == UPNPCOMMAND_SUCCESS) { BOOST_LOG(debug) << "Successfully created pinhole for "sv << mapping.port.proto << ' ' << mapping.port.lan; - } - else { + } else { BOOST_LOG(debug) << "Failed to create pinhole for "sv << mapping.port.proto << ' ' << mapping.port.lan << ": "sv << err; } } return err == 0; - } - else { + } else { BOOST_LOG(debug) << "IPv6 pinholes are not allowed by the IGD"sv; return false; } - } - else { + } else { BOOST_LOG(debug) << "Failed to get IPv6 firewall status: "sv << err; return false; } - } - else { + } else { BOOST_LOG(debug) << "IPv6 Firewall Control is not supported by the IGD"sv; return false; } @@ -178,8 +166,7 @@ namespace upnp { * @param mapping Information about port to map * @return `true` on success. */ - bool - map_upnp_port(const IGDdatas &data, const urls_t &urls, const std::string &lan_addr, const mapping_t &mapping) { + bool map_upnp_port(const IGDdatas &data, const urls_t &urls, const std::string &lan_addr, const mapping_t &mapping) { char intClient[16]; char intPort[6]; char desc[80]; @@ -197,11 +184,15 @@ namespace upnp { mapping.port.proto.c_str(), nullptr, // Out params - intClient, intPort, desc, enabled, leaseDuration); + intClient, + intPort, + desc, + enabled, + leaseDuration + ); if (err == 714) { // NoSuchEntryInArray BOOST_LOG(debug) << "Mapping entry not found for "sv << mapping.port.wan; - } - else if (err == UPNPCOMMAND_SUCCESS) { + } else if (err == UPNPCOMMAND_SUCCESS) { // Some routers change the description, so we can't check that here if (!std::strcmp(intClient, lan_addr.c_str())) { if (std::atoi(leaseDuration) == 0) { @@ -209,12 +200,10 @@ namespace upnp { // It's a static mapping, so we're done here return true; - } - else { + } else { BOOST_LOG(debug) << "Mapping entry found for "sv << mapping.port.wan << " ("sv << leaseDuration << " seconds remaining)"sv; } - } - else { + } else { BOOST_LOG(warning) << "UPnP conflict detected with: "sv << intClient; // Some UPnP IGDs won't let unauthenticated clients delete other conflicting port mappings @@ -224,14 +213,14 @@ namespace upnp { data.first.servicetype, mapping.port.wan.c_str(), mapping.port.proto.c_str(), - nullptr); + nullptr + ); if (err) { BOOST_LOG(error) << "Unable to delete conflicting UPnP port mapping: "sv << err; return false; } } - } - else { + } else { BOOST_LOG(error) << "UPNP_GetSpecificPortMappingEntry() failed: "sv << err; // If we get a strange error from the router, we'll assume it's some old broken IGDv1 @@ -252,7 +241,8 @@ namespace upnp { mapping.description.c_str(), mapping.port.proto.c_str(), nullptr, - mapping_period.c_str()); + mapping_period.c_str() + ); if (err != UPNPCOMMAND_SUCCESS && !indefinite) { // This may be an old/broken IGD that doesn't like non-static mappings. @@ -266,7 +256,8 @@ namespace upnp { mapping.description.c_str(), mapping.port.proto.c_str(), nullptr, - "0"); + "0" + ); } if (err) { @@ -283,20 +274,19 @@ namespace upnp { * @param urls urls_t from UPNP_GetValidIGD() * @param data IGDdatas from UPNP_GetValidIGD() */ - void - unmap_all_upnp_ports(const urls_t &urls, const IGDdatas &data) { + void unmap_all_upnp_ports(const urls_t &urls, const IGDdatas &data) { for (auto it = std::begin(mappings); it != std::end(mappings); ++it) { auto status = UPNP_DeletePortMapping( urls->controlURL, data.first.servicetype, it->port.wan.c_str(), it->port.proto.c_str(), - nullptr); + nullptr + ); if (status && status != 714) { // NoSuchEntryInArray BOOST_LOG(warning) << "Failed to unmap "sv << it->port.proto << ' ' << it->port.lan << ": "sv << status; - } - else { + } else { BOOST_LOG(debug) << "Successfully unmapped "sv << it->port.proto << ' ' << it->port.lan; } } @@ -305,8 +295,7 @@ namespace upnp { /** * @brief Maintains UPnP port forwarding rules */ - void - upnp_thread_proc() { + void upnp_thread_proc() { auto shutdown_event = mail::man->event(mail::shutdown); bool mapped = false; IGDdatas data; @@ -317,7 +306,7 @@ namespace upnp { // WAN IP address changes, or various other conditions. do { int err = 0; - device_t device { upnpDiscover(2000, nullptr, nullptr, 0, IPv4, 2, &err) }; + device_t device {upnpDiscover(2000, nullptr, nullptr, 0, IPv4, 2, &err)}; if (!device || err) { BOOST_LOG(warning) << "Couldn't discover any IPv4 UPNP devices"sv; mapped = false; @@ -338,7 +327,7 @@ namespace upnp { continue; } - std::string lan_addr_str { lan_addr.data() }; + std::string lan_addr_str {lan_addr.data()}; BOOST_LOG(debug) << "Found valid IGD device: "sv << urls->rootdescURL; @@ -373,8 +362,7 @@ namespace upnp { std::thread upnp_thread; }; - std::unique_ptr - start() { + std::unique_ptr start() { if (!config::sunshine.flags[config::flag::UPNP]) { return nullptr; } diff --git a/src/upnp.h b/src/upnp.h index 6f32f2e9d1e..cc7eb32a412 100644 --- a/src/upnp.h +++ b/src/upnp.h @@ -4,8 +4,10 @@ */ #pragma once +// lib includes #include +// local includes #include "platform/common.h" /** @@ -36,9 +38,7 @@ namespace upnp { * @retval 2 A valid IGD has been found but it reported as not connected. * @retval 3 An UPnP device has been found but was not recognized as an IGD. */ - int - UPNP_GetValidIGDStatus(device_t &device, urls_t *urls, IGDdatas *data, std::array &lan_addr); + int UPNP_GetValidIGDStatus(device_t &device, urls_t *urls, IGDdatas *data, std::array &lan_addr); - [[nodiscard]] std::unique_ptr - start(); + [[nodiscard]] std::unique_ptr start(); } // namespace upnp diff --git a/src/utility.h b/src/utility.h index e9adefd8930..db5d7f9d24d 100644 --- a/src/utility.h +++ b/src/utility.h @@ -4,6 +4,7 @@ */ #pragma once +// standard includes #include #include #include @@ -17,100 +18,104 @@ #include #define KITTY_WHILE_LOOP(x, y, z) \ - { \ - x; \ - while (y) z \ + { \ + x; \ + while (y) z \ } -template +template struct argument_type; -template +template struct argument_type { typedef U type; }; -#define KITTY_USING_MOVE_T(move_t, t, init_val, z) \ - class move_t { \ - public: \ - using element_type = typename argument_type::type; \ - \ - move_t(): el { init_val } {} \ - template \ - move_t(Args &&...args): el { std::forward(args)... } {} \ - move_t(const move_t &) = delete; \ - \ - move_t(move_t &&other) noexcept: el { std::move(other.el) } { \ - other.el = element_type { init_val }; \ - } \ - \ - move_t & \ - operator=(const move_t &) = delete; \ - \ - move_t & \ - operator=(move_t &&other) { \ - std::swap(el, other.el); \ - return *this; \ - } \ - element_type * \ - operator->() { return ⪙ } \ - const element_type * \ - operator->() const { return ⪙ } \ - \ - inline element_type \ - release() { \ - element_type val = std::move(el); \ - el = element_type { init_val }; \ - return val; \ - } \ - \ - ~move_t() z \ - \ - element_type el; \ +#define KITTY_USING_MOVE_T(move_t, t, init_val, z) \ + class move_t { \ + public: \ + using element_type = typename argument_type::type; \ +\ + move_t(): \ + el {init_val} { \ + } \ + template \ + move_t(Args &&...args): \ + el {std::forward(args)...} { \ + } \ + move_t(const move_t &) = delete; \ +\ + move_t(move_t &&other) noexcept: \ + el {std::move(other.el)} { \ + other.el = element_type {init_val}; \ + } \ +\ + move_t &operator=(const move_t &) = delete; \ +\ + move_t &operator=(move_t &&other) { \ + std::swap(el, other.el); \ + return *this; \ + } \ + element_type *operator->() { \ + return ⪙ \ + } \ + const element_type *operator->() const { \ + return ⪙ \ + } \ +\ + inline element_type release() { \ + element_type val = std::move(el); \ + el = element_type {init_val}; \ + return val; \ + } \ +\ + ~move_t() z \ +\ + element_type el; \ } -#define KITTY_DECL_CONSTR(x) \ - x(x &&) noexcept = default; \ +#define KITTY_DECL_CONSTR(x) \ + x(x &&) noexcept = default; \ x &operator=(x &&) noexcept = default; \ x(); #define KITTY_DEFAULT_CONSTR_MOVE(x) \ - x(x &&) noexcept = default; \ + x(x &&) noexcept = default; \ x &operator=(x &&) noexcept = default; #define KITTY_DEFAULT_CONSTR_MOVE_THROW(x) \ - x(x &&) = default; \ - x &operator=(x &&) = default; \ + x(x &&) = default; \ + x &operator=(x &&) = default; \ x() = default; -#define KITTY_DEFAULT_CONSTR(x) \ - KITTY_DEFAULT_CONSTR_MOVE(x) \ +#define KITTY_DEFAULT_CONSTR(x) \ + KITTY_DEFAULT_CONSTR_MOVE(x) \ x(const x &) noexcept = default; \ x &operator=(const x &) = default; -#define TUPLE_2D(a, b, expr) \ - decltype(expr) a##_##b = expr; \ +#define TUPLE_2D(a, b, expr) \ + decltype(expr) a##_##b = expr; \ auto &a = std::get<0>(a##_##b); \ auto &b = std::get<1>(a##_##b) -#define TUPLE_2D_REF(a, b, expr) \ - auto &a##_##b = expr; \ +#define TUPLE_2D_REF(a, b, expr) \ + auto &a##_##b = expr; \ auto &a = std::get<0>(a##_##b); \ auto &b = std::get<1>(a##_##b) -#define TUPLE_3D(a, b, c, expr) \ - decltype(expr) a##_##b##_##c = expr; \ +#define TUPLE_3D(a, b, c, expr) \ + decltype(expr) a##_##b##_##c = expr; \ auto &a = std::get<0>(a##_##b##_##c); \ auto &b = std::get<1>(a##_##b##_##c); \ auto &c = std::get<2>(a##_##b##_##c) -#define TUPLE_3D_REF(a, b, c, expr) \ - auto &a##_##b##_##c = expr; \ +#define TUPLE_3D_REF(a, b, c, expr) \ + auto &a##_##b##_##c = expr; \ auto &a = std::get<0>(a##_##b##_##c); \ auto &b = std::get<1>(a##_##b##_##c); \ auto &c = std::get<2>(a##_##b##_##c) -#define TUPLE_EL(a, b, expr) \ +#define TUPLE_EL(a, b, expr) \ decltype(expr) a##_ = expr; \ auto &a = std::get(a##_) @@ -119,46 +124,49 @@ struct argument_type { namespace util { - template