From bf0517e1fd00cb70471c0b5eb5d4c6e999a97dde Mon Sep 17 00:00:00 2001 From: qiin Date: Mon, 22 Jul 2024 12:26:01 +0800 Subject: [PATCH] Merge remote-tracking branch 'origin/nightly' --- .github/workflows/CI.yml | 159 +++- .gitmodules | 4 + cmake/compile_definitions/common.cmake | 9 + cmake/compile_definitions/linux.cmake | 3 + cmake/compile_definitions/macos.cmake | 1 + cmake/compile_definitions/windows.cmake | 9 + cmake/dependencies/common.cmake | 2 +- cmake/packaging/common.cmake | 4 + cmake/packaging/windows.cmake | 13 +- cmake/prep/build_version.cmake | 4 +- cmake/prep/options.cmake | 1 + cmake/targets/windows.cmake | 6 + docs/source/about/advanced_usage.rst | 34 +- .../src/display_device/display_device.rst | 4 + .../src/display_device/parsed_config.rst | 4 + .../src/display_device/session.rst | 4 + .../src/display_device/settings.rst | 4 + .../src/display_device/to_string.rst | 4 + .../windows/display_device/settings_data.rst | 4 + .../display_device/settings_topology.rst | 4 + .../windows/display_device/windows_utils.rst | 4 + package.json | 3 +- .../flatpak/deps/org.flatpak.Builder.BaseApp | 1 + src/audio.cpp | 20 +- src/audio.h | 29 + src/config.cpp | 54 +- src/config.h | 22 +- src/confighttp.cpp | 37 + src/display_device/display_device.h | 307 +++++++ src/display_device/parsed_config.cpp | 561 +++++++++++++ src/display_device/parsed_config.h | 140 ++++ src/display_device/session.cpp | 225 ++++++ src/display_device/session.h | 190 +++++ src/display_device/settings.cpp | 39 + src/display_device/settings.h | 200 +++++ src/display_device/to_string.cpp | 142 ++++ src/display_device/to_string.h | 138 ++++ src/main.cpp | 15 +- src/nvhttp.cpp | 146 +++- src/platform/linux/display_device.cpp | 117 +++ src/platform/macos/display_device.cpp | 117 +++ src/platform/windows/display_base.cpp | 4 +- .../display_device/device_hdr_states.cpp | 108 +++ .../windows/display_device/device_modes.cpp | 344 ++++++++ .../display_device/device_topology.cpp | 482 +++++++++++ .../display_device/general_functions.cpp | 138 ++++ .../windows/display_device/settings.cpp | 756 ++++++++++++++++++ .../display_device/settings_topology.cpp | 277 +++++++ .../display_device/settings_topology.h | 88 ++ .../windows/display_device/windows_utils.cpp | 654 +++++++++++++++ .../windows/display_device/windows_utils.h | 491 ++++++++++++ src/platform/windows/misc.cpp | 2 +- src/process.cpp | 65 +- src/process.h | 11 + src/rtsp.cpp | 3 + src/rtsp.h | 4 + src/stream.cpp | 105 ++- src/system_tray.cpp | 19 +- src/video.cpp | 12 +- src_assets/common/assets/web/Locale.vue | 56 ++ src_assets/common/assets/web/Navbar.vue | 17 +- src_assets/common/assets/web/ResourceCard.vue | 100 ++- src_assets/common/assets/web/apps.html | 75 +- src_assets/common/assets/web/config.html | 26 +- .../assets/web/configs/tabs/AudioVideo.vue | 9 +- .../tabs/audiovideo/DisplayDeviceOptions.vue | 219 ++++- .../audiovideo/NewDisplayOutputSelector.vue | 95 ++- src_assets/common/assets/web/index.html | 21 +- src_assets/common/assets/web/locale/de.json | 79 ++ .../common/assets/web/locale/de/sunshine.json | 13 + .../common/assets/web/locale/en-GB.json | 79 ++ .../assets/web/locale/en-GB/sunshine.json | 13 + .../common/assets/web/locale/en-US.json | 79 ++ .../assets/web/locale/en-US/sunshine.json | 13 + src_assets/common/assets/web/locale/en.json | 79 ++ .../common/assets/web/locale/en/sunshine.json | 13 + src_assets/common/assets/web/locale/es.json | 79 ++ .../common/assets/web/locale/es/sunshine.json | 13 + src_assets/common/assets/web/locale/fr.json | 79 ++ .../common/assets/web/locale/fr/sunshine.json | 13 + src_assets/common/assets/web/locale/it.json | 79 ++ .../common/assets/web/locale/it/sunshine.json | 13 + src_assets/common/assets/web/locale/ru.json | 79 ++ .../common/assets/web/locale/ru/sunshine.json | 13 + .../common/assets/web/locale/sunshine.json | 13 + src_assets/common/assets/web/locale/sv.json | 79 ++ .../common/assets/web/locale/sv/sunshine.json | 13 + src_assets/common/assets/web/locale/zh.json | 79 ++ .../common/assets/web/locale/zh/sunshine.json | 13 + src_assets/common/assets/web/pin.html | 223 +++++- .../assets/web/public/assets/locale/en.json | 41 +- .../web/public/assets/locale/en_GB.json | 2 +- .../web/public/assets/locale/en_US.json | 2 +- .../assets/web/public/assets/locale/zh.json | 35 +- .../common/assets/web/troubleshooting.html | 149 ++-- src_assets/common/sunshine-control-panel | 1 + .../windows/misc/gamepad/install-gamepad.bat | 2 +- src_assets/windows/misc/service/sleep.bat | 1 + .../misc/vdd/driver/IddSampleDriver.dll | Bin 0 -> 107152 bytes .../misc/vdd/driver/IddSampleDriver.inf | Bin 0 -> 4402 bytes .../vdd/driver/Virtual_Display_Driver.cer | Bin 0 -> 961 bytes .../misc/vdd/driver/iddsampledriver.cat | Bin 0 -> 2784 bytes .../windows/misc/vdd/driver/installCert.bat | 7 + .../windows/misc/vdd/driver/nefconw.exe | Bin 0 -> 588200 bytes src_assets/windows/misc/vdd/driver/option.txt | 48 ++ src_assets/windows/misc/vdd/install-vdd.bat | 64 ++ src_assets/windows/misc/vdd/uninstall-vdd.bat | 9 + 107 files changed, 8199 insertions(+), 283 deletions(-) create mode 100644 docs/source/source_code/src/display_device/display_device.rst create mode 100644 docs/source/source_code/src/display_device/parsed_config.rst create mode 100644 docs/source/source_code/src/display_device/session.rst create mode 100644 docs/source/source_code/src/display_device/settings.rst create mode 100644 docs/source/source_code/src/display_device/to_string.rst create mode 100644 docs/source/source_code/src/platform/windows/display_device/settings_data.rst create mode 100644 docs/source/source_code/src/platform/windows/display_device/settings_topology.rst create mode 100644 docs/source/source_code/src/platform/windows/display_device/windows_utils.rst create mode 160000 packaging/linux/flatpak/deps/org.flatpak.Builder.BaseApp create mode 100644 src/display_device/display_device.h create mode 100644 src/display_device/parsed_config.cpp create mode 100644 src/display_device/parsed_config.h create mode 100644 src/display_device/session.cpp create mode 100644 src/display_device/session.h create mode 100644 src/display_device/settings.cpp create mode 100644 src/display_device/settings.h create mode 100644 src/display_device/to_string.cpp create mode 100644 src/display_device/to_string.h create mode 100644 src/platform/linux/display_device.cpp create mode 100644 src/platform/macos/display_device.cpp create mode 100644 src/platform/windows/display_device/device_hdr_states.cpp create mode 100644 src/platform/windows/display_device/device_modes.cpp create mode 100644 src/platform/windows/display_device/device_topology.cpp create mode 100644 src/platform/windows/display_device/general_functions.cpp create mode 100644 src/platform/windows/display_device/settings.cpp create mode 100644 src/platform/windows/display_device/settings_topology.cpp create mode 100644 src/platform/windows/display_device/settings_topology.h create mode 100644 src/platform/windows/display_device/windows_utils.cpp create mode 100644 src/platform/windows/display_device/windows_utils.h create mode 100644 src_assets/common/assets/web/Locale.vue create mode 100644 src_assets/common/assets/web/locale/de.json create mode 100644 src_assets/common/assets/web/locale/de/sunshine.json create mode 100644 src_assets/common/assets/web/locale/en-GB.json create mode 100644 src_assets/common/assets/web/locale/en-GB/sunshine.json create mode 100644 src_assets/common/assets/web/locale/en-US.json create mode 100644 src_assets/common/assets/web/locale/en-US/sunshine.json create mode 100644 src_assets/common/assets/web/locale/en.json create mode 100644 src_assets/common/assets/web/locale/en/sunshine.json create mode 100644 src_assets/common/assets/web/locale/es.json create mode 100644 src_assets/common/assets/web/locale/es/sunshine.json create mode 100644 src_assets/common/assets/web/locale/fr.json create mode 100644 src_assets/common/assets/web/locale/fr/sunshine.json create mode 100644 src_assets/common/assets/web/locale/it.json create mode 100644 src_assets/common/assets/web/locale/it/sunshine.json create mode 100644 src_assets/common/assets/web/locale/ru.json create mode 100644 src_assets/common/assets/web/locale/ru/sunshine.json create mode 100644 src_assets/common/assets/web/locale/sunshine.json create mode 100644 src_assets/common/assets/web/locale/sv.json create mode 100644 src_assets/common/assets/web/locale/sv/sunshine.json create mode 100644 src_assets/common/assets/web/locale/zh.json create mode 100644 src_assets/common/assets/web/locale/zh/sunshine.json create mode 160000 src_assets/common/sunshine-control-panel create mode 100644 src_assets/windows/misc/service/sleep.bat create mode 100644 src_assets/windows/misc/vdd/driver/IddSampleDriver.dll create mode 100644 src_assets/windows/misc/vdd/driver/IddSampleDriver.inf create mode 100644 src_assets/windows/misc/vdd/driver/Virtual_Display_Driver.cer create mode 100644 src_assets/windows/misc/vdd/driver/iddsampledriver.cat create mode 100644 src_assets/windows/misc/vdd/driver/installCert.bat create mode 100644 src_assets/windows/misc/vdd/driver/nefconw.exe create mode 100644 src_assets/windows/misc/vdd/driver/option.txt create mode 100644 src_assets/windows/misc/vdd/install-vdd.bat create mode 100644 src_assets/windows/misc/vdd/uninstall-vdd.bat diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 408f4710212..4ea7904381c 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -504,8 +504,9 @@ jobs: tag="${{ needs.setup_release.outputs.release_tag }}" version="${{ needs.setup_release.outputs.release_version }}" else - tag="${{ github.ref_name }}" - version="0.0.${{ github.run_number }}" + matrix=$(( + echo '{ "arch" : ["x86_64"] }' + ) | jq -c .) fi else echo "This is a PR event" @@ -872,21 +873,147 @@ jobs: [string]$Uri, [string]$OutFile ) - - $maxRetries = 5 - $retryCount = 0 - $success = $false - - while (-not $success -and $retryCount -lt $maxRetries) { - $retryCount++ - Write-Host "Downloading $Uri to $OutFile, attempt $retryCount of $maxRetries" - try { - Invoke-WebRequest -Uri $Uri -OutFile $OutFile - $success = $true - } catch { - Write-Host "Attempt $retryCount of $maxRetries failed with error: $($_.Exception.Message). Retrying..." - Start-Sleep -Seconds 5 + if [[ "${{ 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 port install \ + doxygen \ + graphviz + 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} \ + --exclude-noncode-lines \ + --exclude-throw-branches \ + --exclude-unreachable-branches \ + --exclude '.*${dir}/tests/.*' \ + --exclude '.*${dir}/third-party/.*' \ + --gcov-object-directory $(pwd) \ + --verbose \ + --xml-pretty \ + -o ${{ github.workspace }}/build/coverage.xml + + - name: Upload coverage + # any except canceled or skipped + if: >- + always() && + (steps.test_report.outcome == 'success') && + startsWith(github.repository, 'LizardByte/') + uses: codecov/codecov-action@v4 + 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.614.221009 + with: + allowUpdates: true + body: ${{ needs.setup_release.outputs.release_body }} + discussionCategory: announcements + 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 + runs-on: windows-2019 + needs: [setup_release] + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Prepare tests + id: prepare-tests + if: false # todo: DirectX11 is not available, so even software encoder fails + run: | + # function to download and extract a zip file + function DownloadAndExtract { + param ( + [string]$Uri, + [string]$OutFile + ) + + $maxRetries = 5 + $retryCount = 0 + $success = $false + + while (-not $success -and $retryCount -lt $maxRetries) { + $retryCount++ + Write-Host "Downloading $Uri to $OutFile, attempt $retryCount of $maxRetries" + try { + Invoke-WebRequest -Uri $Uri -OutFile $OutFile + $success = $true + } catch { + Write-Host "Attempt $retryCount of $maxRetries failed with error: $($_.Exception.Message). Retrying..." + Start-Sleep -Seconds 5 + } + } + + if (-not $success) { + Write-Host "Failed to download the file after $maxRetries attempts." + exit 1 } + + # use .NET to get the base name of the file + $baseName = (Get-Item $OutFile).BaseName + + # Extract the zip file + Expand-Archive -Path $OutFile -DestinationPath $baseName } if (-not $success) { diff --git a/.gitmodules b/.gitmodules index 4aa76d5206f..867c05a8d98 100644 --- a/.gitmodules +++ b/.gitmodules @@ -58,3 +58,7 @@ path = third-party/wlr-protocols url = https://gitlab.freedesktop.org/wlroots/wlr-protocols.git branch = master +[submodule "src_assets/common/sunshine-control-panel"] + path = src_assets/common/sunshine-control-panel + url = https://github.com/qiin2333/sunshine-control-panel.git + branch = master diff --git a/cmake/compile_definitions/common.cmake b/cmake/compile_definitions/common.cmake index a2260595c9a..656f7658818 100644 --- a/cmake/compile_definitions/common.cmake +++ b/cmake/compile_definitions/common.cmake @@ -61,6 +61,15 @@ set(SUNSHINE_TARGET_FILES "${CMAKE_SOURCE_DIR}/third-party/moonlight-common-c/src/RtspParser.c" "${CMAKE_SOURCE_DIR}/third-party/moonlight-common-c/src/Video.h" "${CMAKE_SOURCE_DIR}/third-party/tray/src/tray.h" + "${CMAKE_SOURCE_DIR}/src/display_device/display_device.h" + "${CMAKE_SOURCE_DIR}/src/display_device/parsed_config.cpp" + "${CMAKE_SOURCE_DIR}/src/display_device/parsed_config.h" + "${CMAKE_SOURCE_DIR}/src/display_device/session.cpp" + "${CMAKE_SOURCE_DIR}/src/display_device/session.h" + "${CMAKE_SOURCE_DIR}/src/display_device/settings.cpp" + "${CMAKE_SOURCE_DIR}/src/display_device/settings.h" + "${CMAKE_SOURCE_DIR}/src/display_device/to_string.cpp" + "${CMAKE_SOURCE_DIR}/src/display_device/to_string.h" "${CMAKE_SOURCE_DIR}/src/upnp.cpp" "${CMAKE_SOURCE_DIR}/src/upnp.h" "${CMAKE_SOURCE_DIR}/src/cbs.cpp" diff --git a/cmake/compile_definitions/linux.cmake b/cmake/compile_definitions/linux.cmake index e07c2a55d8f..3adc68a1d13 100644 --- a/cmake/compile_definitions/linux.cmake +++ b/cmake/compile_definitions/linux.cmake @@ -249,6 +249,9 @@ list(APPEND PLATFORM_TARGET_FILES "${CMAKE_SOURCE_DIR}/src/platform/linux/misc.h" "${CMAKE_SOURCE_DIR}/src/platform/linux/misc.cpp" "${CMAKE_SOURCE_DIR}/src/platform/linux/audio.cpp" + "${CMAKE_SOURCE_DIR}/src/platform/linux/display_device.cpp" + "${CMAKE_SOURCE_DIR}/src/platform/linux/input.cpp" + "${CMAKE_SOURCE_DIR}/src/platform/linux/display_device.cpp" "${CMAKE_SOURCE_DIR}/third-party/glad/src/egl.c" "${CMAKE_SOURCE_DIR}/third-party/glad/src/gl.c" "${CMAKE_SOURCE_DIR}/third-party/glad/include/EGL/eglplatform.h" diff --git a/cmake/compile_definitions/macos.cmake b/cmake/compile_definitions/macos.cmake index fb33d3bf235..25529a981d8 100644 --- a/cmake/compile_definitions/macos.cmake +++ b/cmake/compile_definitions/macos.cmake @@ -38,6 +38,7 @@ set(PLATFORM_TARGET_FILES "${CMAKE_SOURCE_DIR}/src/platform/macos/av_video.h" "${CMAKE_SOURCE_DIR}/src/platform/macos/av_video.m" "${CMAKE_SOURCE_DIR}/src/platform/macos/display.mm" + "${CMAKE_SOURCE_DIR}/src/platform/macos/display_device.cpp" "${CMAKE_SOURCE_DIR}/src/platform/macos/input.cpp" "${CMAKE_SOURCE_DIR}/src/platform/macos/microphone.mm" "${CMAKE_SOURCE_DIR}/src/platform/macos/misc.mm" diff --git a/cmake/compile_definitions/windows.cmake b/cmake/compile_definitions/windows.cmake index 7643d1d9efc..5ee0f9142ae 100644 --- a/cmake/compile_definitions/windows.cmake +++ b/cmake/compile_definitions/windows.cmake @@ -52,6 +52,15 @@ set(PLATFORM_TARGET_FILES "${CMAKE_SOURCE_DIR}/src/platform/windows/display_ram.cpp" "${CMAKE_SOURCE_DIR}/src/platform/windows/display_wgc.cpp" "${CMAKE_SOURCE_DIR}/src/platform/windows/audio.cpp" + "${CMAKE_SOURCE_DIR}/src/platform/windows/display_device/device_hdr_states.cpp" + "${CMAKE_SOURCE_DIR}/src/platform/windows/display_device/device_modes.cpp" + "${CMAKE_SOURCE_DIR}/src/platform/windows/display_device/device_topology.cpp" + "${CMAKE_SOURCE_DIR}/src/platform/windows/display_device/general_functions.cpp" + "${CMAKE_SOURCE_DIR}/src/platform/windows/display_device/settings_topology.cpp" + "${CMAKE_SOURCE_DIR}/src/platform/windows/display_device/settings_topology.h" + "${CMAKE_SOURCE_DIR}/src/platform/windows/display_device/settings.cpp" + "${CMAKE_SOURCE_DIR}/src/platform/windows/display_device/windows_utils.h" + "${CMAKE_SOURCE_DIR}/src/platform/windows/display_device/windows_utils.cpp" "${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/src/ViGEmClient.cpp" "${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/include/ViGEm/Client.h" "${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/include/ViGEm/Common.h" diff --git a/cmake/dependencies/common.cmake b/cmake/dependencies/common.cmake index 01065b387e0..fc22b86e24f 100644 --- a/cmake/dependencies/common.cmake +++ b/cmake/dependencies/common.cmake @@ -78,4 +78,4 @@ elseif(UNIX) else() include("${CMAKE_MODULE_PATH}/dependencies/linux.cmake") endif() -endif() +endif() \ No newline at end of file diff --git a/cmake/packaging/common.cmake b/cmake/packaging/common.cmake index c7c5b3a5cc9..c46439f4ad3 100644 --- a/cmake/packaging/common.cmake +++ b/cmake/packaging/common.cmake @@ -29,6 +29,10 @@ endforeach() install(DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/assets/web" DESTINATION "${SUNSHINE_ASSETS_DIR}") +# install sunshine control panel +install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/common/sunshine-control-panel/dist/win-unpacked/" + DESTINATION "${SUNSHINE_ASSETS_DIR}/gui") + # platform specific packaging if(WIN32) include(${CMAKE_MODULE_PATH}/packaging/windows.cmake) diff --git a/cmake/packaging/windows.cmake b/cmake/packaging/windows.cmake index bbd497ee3a0..1f4785cff4d 100644 --- a/cmake/packaging/windows.cmake +++ b/cmake/packaging/windows.cmake @@ -34,6 +34,9 @@ install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/windows/misc/firewall/" install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/windows/misc/gamepad/" DESTINATION "scripts" COMPONENT gamepad) +install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/windows/misc/vdd/" + DESTINATION "scripts" + COMPONENT vdd) # Sunshine assets install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/windows/assets/" @@ -56,10 +59,11 @@ 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.qq.com/aio/DSGdQc3htbFJjSFdO' nsExec::ExecToLog 'icacls \\\"$INSTDIR\\\" /reset' nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\migrate-config.bat\\\"' nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\add-firewall-rule.bat\\\"' + nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\install-vdd.bat\\\"' nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\install-gamepad.bat\\\"' nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\install-service.bat\\\"' nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\autostart-service.bat\\\"' @@ -72,6 +76,7 @@ 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\\\\scripts\\\\uninstall-vdd.bat\\\"' nsExec::ExecToLog '\\\"$INSTDIR\\\\sunshine.exe\\\" --restore-nvprefs-undo' MessageBox MB_YESNO|MB_ICONQUESTION \ 'Do you want to remove Virtual Gamepad)?' \ @@ -115,6 +120,7 @@ set(CPACK_NSIS_MANIFEST_DPI_AWARE true) # Setting components groups and dependencies set(CPACK_COMPONENT_GROUP_CORE_EXPANDED true) +set(CPACK_COMPONENT_GROUP_SCRIPTS_EXPANDED true) # sunshine binary set(CPACK_COMPONENT_APPLICATION_DISPLAY_NAME "${CMAKE_PROJECT_NAME}") @@ -153,3 +159,8 @@ set(CPACK_COMPONENT_FIREWALL_GROUP "Scripts") set(CPACK_COMPONENT_GAMEPAD_DISPLAY_NAME "Virtual Gamepad") set(CPACK_COMPONENT_GAMEPAD_DESCRIPTION "Scripts to install and uninstall Virtual Gamepad.") set(CPACK_COMPONENT_GAMEPAD_GROUP "Scripts") + +# Virtual Display Driver +set(CPACK_COMPONENT_VDD_DISPLAY_NAME "IddSampleDriver") +set(CPACK_COMPONENT_VDD_DESCRIPTION "支持HDR的虚拟显示器驱动安装") +set(CPACK_COMPONENT_VDD_GROUP "Scripts") diff --git a/cmake/prep/build_version.cmake b/cmake/prep/build_version.cmake index 5457fed1b01..b2c0bb6fc35 100644 --- a/cmake/prep/build_version.cmake +++ b/cmake/prep/build_version.cmake @@ -14,6 +14,8 @@ if((DEFINED ENV{BRANCH}) AND (DEFINED ENV{BUILD_VERSION}) AND (DEFINED ENV{COMMI # https://github.com/nocnokneo/cmake-git-versioning-example/blob/master/LICENSE else() find_package(Git) + string(TIMESTAMP COMPILE_TIME "%Y.%m%d.%H%M%S") + set(PROJECT_VERSION ${COMPILE_TIME}) if(GIT_EXECUTABLE) MESSAGE("${CMAKE_SOURCE_DIR}") get_filename_component(SRC_DIR "${CMAKE_SOURCE_DIR}" DIRECTORY) @@ -44,7 +46,7 @@ else() MESSAGE("Sunshine Version: ${GIT_DESCRIBE_VERSION}") endif() if(GIT_IS_DIRTY) - set(PROJECT_VERSION ${PROJECT_VERSION}.dirty) + set(PROJECT_VERSION ${PROJECT_VERSION}.杂鱼) MESSAGE("Git tree is dirty!") endif() else() diff --git a/cmake/prep/options.cmake b/cmake/prep/options.cmake index f358f7273fe..9a4c5da34cf 100644 --- a/cmake/prep/options.cmake +++ b/cmake/prep/options.cmake @@ -14,6 +14,7 @@ option(SUNSHINE_CONFIGURE_ONLY "Configure special files only, then exit." OFF) option(SUNSHINE_ENABLE_TRAY "Enable system tray icon. This option will be ignored on macOS." ON) option(SUNSHINE_REQUIRE_TRAY "Require system tray icon. Fail the build if tray requirements are not met." ON) +option(SUNSHINE_SYSTEM_NLOHMANN_JSON "Use system installation of nlohmann_json rather than the submodule." OFF) option(SUNSHINE_SYSTEM_WAYLAND_PROTOCOLS "Use system installation of wayland-protocols rather than the submodule." OFF) if(APPLE) diff --git a/cmake/targets/windows.cmake b/cmake/targets/windows.cmake index b7f8fbcfe7a..35086b2373e 100644 --- a/cmake/targets/windows.cmake +++ b/cmake/targets/windows.cmake @@ -5,3 +5,9 @@ find_library(ZLIB ZLIB1) list(APPEND SUNSHINE_EXTERNAL_LIBRARIES Windowsapp.lib Wtsapi32.lib) + +#GUI build +add_custom_target(sunshine-control-panel ALL + WORKING_DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/common/sunshine-control-panel" + COMMENT "Installing NPM Dependencies and Building the gui" + COMMAND bash -c \"npm install && npm run build:win\") # cmake-lint: disable=C0301 diff --git a/docs/source/about/advanced_usage.rst b/docs/source/about/advanced_usage.rst index 95025b91358..80154616808 100644 --- a/docs/source/about/advanced_usage.rst +++ b/docs/source/about/advanced_usage.rst @@ -586,7 +586,7 @@ keybindings ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Description** - Select the display number you want to stream. + Select the display you want to stream. .. tip:: To find the name of the appropriate values follow these instructions. @@ -616,9 +616,35 @@ keybindings You need to use the id value inside the parenthesis, e.g. ``3``. **Windows** - .. code-block:: batch + During Sunshine startup, you should see the list of detected display devices: - tools\dxgi-info.exe + .. code-block:: text + + DEVICE ID: {de9bb7e2-186e-505b-9e93-f48793333810} + DISPLAY NAME: \\.\DISPLAY1 + FRIENDLY NAME: ROG PG279Q + DEVICE STATE: PRIMARY + HDR STATE: UNKNOWN + ----------------------- + DEVICE ID: {3bd008cd-0465-547c-8da5-c28749c041e6} + DISPLAY NAME: NOT AVAILABLE + FRIENDLY NAME: IDD HDR + DEVICE STATE: INACTIVE + HDR STATE: UNKNOWN + ----------------------- + DEVICE ID: {77f67f3e-754f-5d31-af64-ee037e18100a} + DISPLAY NAME: NOT AVAILABLE + FRIENDLY NAME: SunshineHDR + DEVICE STATE: INACTIVE + HDR STATE: UNKNOWN + ----------------------- + DEVICE ID: {bc172e6d-86eb-5851-aeca-56525ed716e9} + DISPLAY NAME: NOT AVAILABLE + FRIENDLY NAME: ROG PG279Q + DEVICE STATE: INACTIVE + HDR STATE: UNKNOWN + + You need to use the ``DEVICE ID`` value. **Default** Sunshine will select the default display. @@ -637,7 +663,7 @@ keybindings **Windows** .. code-block:: text - output_name = \\.\DISPLAY1 + output_name = {de9bb7e2-186e-505b-9e93-f48793333810} `resolutions `__ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/source/source_code/src/display_device/display_device.rst b/docs/source/source_code/src/display_device/display_device.rst new file mode 100644 index 00000000000..147db4ef91c --- /dev/null +++ b/docs/source/source_code/src/display_device/display_device.rst @@ -0,0 +1,4 @@ +display_device +============== + +.. todo:: Add display_device.h diff --git a/docs/source/source_code/src/display_device/parsed_config.rst b/docs/source/source_code/src/display_device/parsed_config.rst new file mode 100644 index 00000000000..24d23f1e807 --- /dev/null +++ b/docs/source/source_code/src/display_device/parsed_config.rst @@ -0,0 +1,4 @@ +parsed_config +============= + +.. todo:: Add parsed_config.h diff --git a/docs/source/source_code/src/display_device/session.rst b/docs/source/source_code/src/display_device/session.rst new file mode 100644 index 00000000000..47a4d49ebce --- /dev/null +++ b/docs/source/source_code/src/display_device/session.rst @@ -0,0 +1,4 @@ +session +======= + +.. todo:: Add session.h diff --git a/docs/source/source_code/src/display_device/settings.rst b/docs/source/source_code/src/display_device/settings.rst new file mode 100644 index 00000000000..f0f9dd82f2d --- /dev/null +++ b/docs/source/source_code/src/display_device/settings.rst @@ -0,0 +1,4 @@ +settings +======== + +.. todo:: Add settings.h diff --git a/docs/source/source_code/src/display_device/to_string.rst b/docs/source/source_code/src/display_device/to_string.rst new file mode 100644 index 00000000000..d0211b9423c --- /dev/null +++ b/docs/source/source_code/src/display_device/to_string.rst @@ -0,0 +1,4 @@ +to_string +========= + +.. todo:: Add to_string.h diff --git a/docs/source/source_code/src/platform/windows/display_device/settings_data.rst b/docs/source/source_code/src/platform/windows/display_device/settings_data.rst new file mode 100644 index 00000000000..893209c7ab0 --- /dev/null +++ b/docs/source/source_code/src/platform/windows/display_device/settings_data.rst @@ -0,0 +1,4 @@ +settings_data +============= + +.. todo:: Add settings_data.h diff --git a/docs/source/source_code/src/platform/windows/display_device/settings_topology.rst b/docs/source/source_code/src/platform/windows/display_device/settings_topology.rst new file mode 100644 index 00000000000..b0d242dc6b1 --- /dev/null +++ b/docs/source/source_code/src/platform/windows/display_device/settings_topology.rst @@ -0,0 +1,4 @@ +settings_topology +================= + +.. todo:: Add settings_topology.h diff --git a/docs/source/source_code/src/platform/windows/display_device/windows_utils.rst b/docs/source/source_code/src/platform/windows/display_device/windows_utils.rst new file mode 100644 index 00000000000..d1af6fde5fb --- /dev/null +++ b/docs/source/source_code/src/platform/windows/display_device/windows_utils.rst @@ -0,0 +1,4 @@ +windows_utils +============= + +.. todo:: Add windows_utils.h diff --git a/package.json b/package.json index 5b1c31f9ced..27b46f95d35 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "vite": "4.5.2", "vite-plugin-ejs": "1.6.4", "vue": "3.4.32", - "vue-i18n": "9.13.1" + "vue-i18n": "9.13.1", + "nanoid": "^5.0.7" } } diff --git a/packaging/linux/flatpak/deps/org.flatpak.Builder.BaseApp b/packaging/linux/flatpak/deps/org.flatpak.Builder.BaseApp new file mode 160000 index 00000000000..17d551d4979 --- /dev/null +++ b/packaging/linux/flatpak/deps/org.flatpak.Builder.BaseApp @@ -0,0 +1 @@ +Subproject commit 17d551d49798b30e4e2846a53377ce2afe13d7a3 diff --git a/src/audio.cpp b/src/audio.cpp index b24ae61350f..7415f63fca7 100644 --- a/src/audio.cpp +++ b/src/audio.cpp @@ -20,16 +20,6 @@ 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 @@ -95,8 +85,6 @@ 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) { auto packets = mail::man->queue(mail::audio_packets); @@ -149,7 +137,7 @@ namespace audio { apply_surround_params(stream, config.customStreamParams); } - auto ref = control_shared.ref(); + auto ref = get_audio_ctx_ref(); if (!ref) { return; } @@ -255,6 +243,12 @@ namespace audio { } } + 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(); + } + int map_stream(int channels, bool quality) { int shift = quality ? 1 : 0; diff --git a/src/audio.h b/src/audio.h index 6d04d242b4c..5a4027fd946 100644 --- a/src/audio.h +++ b/src/audio.h @@ -4,8 +4,11 @@ */ #pragma once +// local includes +#include "platform/common.h" #include "thread_safe.h" #include "utility.h" + namespace audio { enum stream_config_e : int { STEREO, ///< Stereo @@ -52,8 +55,34 @@ 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; + 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: + * ```cpp + * audio_ctx_ref_t audio = get_audio_ctx_ref() + * ``` + */ + audio_ctx_ref_t + get_audio_ctx_ref(); } // namespace audio diff --git a/src/config.cpp b/src/config.cpp index 7f5888633ae..d42b93c3002 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -24,6 +24,7 @@ #include "rtsp.h" #include "utility.h" +#include "display_device/parsed_config.h" #include "platform/common.h" #ifdef _WIN32 @@ -377,7 +378,15 @@ namespace config { {}, // capture {}, // encoder {}, // adapter_name + {}, // output_name + (int) display_device::parsed_config_t::device_prep_e::no_operation, // display_device_prep + (int) display_device::parsed_config_t::resolution_change_e::automatic, // resolution_change + {}, // manual_resolution + (int) display_device::parsed_config_t::refresh_rate_change_e::automatic, // refresh_rate_change + {}, // manual_refresh_rate + (int) display_device::parsed_config_t::hdr_prep_e::automatic, // hdr_prep + {} // display_mode_remapping }; audio_t audio { @@ -829,6 +838,40 @@ namespace config { } } + void + list_display_mode_remapping_f(std::unordered_map &vars, const std::string &name, std::vector &input) { + std::string string; + string_f(vars, name, string); + + std::stringstream jsonStream; + + // check if string is empty, i.e. when the value doesn't exist in the config file + if (string.empty()) { + return; + } + + // We need to add a wrapping object to make it valid JSON, otherwise ptree cannot parse it. + jsonStream << "{\"display_mode_remapping\":" << string << "}"; + + boost::property_tree::ptree jsonTree; + boost::property_tree::read_json(jsonStream, jsonTree); + + for (auto &[_, entry] : jsonTree.get_child("display_mode_remapping"s)) { + auto type = entry.get_optional("type"s); + auto received_resolution = entry.get_optional("received_resolution"s); + auto received_fps = entry.get_optional("received_fps"s); + auto final_resolution = entry.get_optional("final_resolution"s); + auto final_refresh_rate = entry.get_optional("final_refresh_rate"s); + + input.push_back(video_t::display_mode_remapping_t { + type.value_or(""), + received_resolution.value_or(""), + received_fps.value_or(""), + final_resolution.value_or(""), + final_refresh_rate.value_or("") }); + } + } + void list_prep_cmd_f(std::unordered_map &vars, const std::string &name, std::vector &input) { std::string string; @@ -851,8 +894,9 @@ namespace config { auto do_cmd = prep_cmd.get_optional("do"s); auto undo_cmd = prep_cmd.get_optional("undo"s); auto elevated = prep_cmd.get_optional("elevated"s); + auto on_session = prep_cmd.get_optional("on-session"s); - input.emplace_back(do_cmd.value_or(""), undo_cmd.value_or(""), elevated.value_or(false)); + input.emplace_back(do_cmd.value_or(""), undo_cmd.value_or(""), elevated.value_or(false), on_session.value_or(false)); } } @@ -1030,7 +1074,15 @@ namespace config { string_f(vars, "capture", video.capture); string_f(vars, "encoder", video.encoder); string_f(vars, "adapter_name", video.adapter_name); + string_f(vars, "output_name", video.output_name); + int_f(vars, "display_device_prep", video.display_device_prep, display_device::parsed_config_t::device_prep_from_view); + int_f(vars, "resolution_change", video.resolution_change, display_device::parsed_config_t::resolution_change_from_view); + string_f(vars, "manual_resolution", video.manual_resolution); + list_display_mode_remapping_f(vars, "display_mode_remapping", video.display_mode_remapping); + int_f(vars, "refresh_rate_change", video.refresh_rate_change, display_device::parsed_config_t::refresh_rate_change_from_view); + string_f(vars, "manual_refresh_rate", video.manual_refresh_rate); + int_f(vars, "hdr_prep", video.hdr_prep, display_device::parsed_config_t::hdr_prep_from_view); int_between_f(vars, "min_fps_factor", video.min_fps_factor, { 1, 3 }); path_f(vars, "pkey", nvhttp.pkey); diff --git a/src/config.h b/src/config.h index d2c4783ce93..6f8b7bc1376 100644 --- a/src/config.h +++ b/src/config.h @@ -74,7 +74,23 @@ namespace config { std::string capture; std::string encoder; std::string adapter_name; + + struct display_mode_remapping_t { + std::string type; + std::string received_resolution; + std::string received_fps; + std::string final_resolution; + std::string final_refresh_rate; + }; + std::string output_name; + int display_device_prep; + int resolution_change; + std::string manual_resolution; + int refresh_rate_change; + std::string manual_refresh_rate; + int hdr_prep; + std::vector display_mode_remapping; }; struct audio_t { @@ -153,13 +169,17 @@ namespace config { } struct prep_cmd_t { - prep_cmd_t(std::string &&do_cmd, std::string &&undo_cmd, bool &&elevated): + prep_cmd_t(std::string &&do_cmd, std::string &&undo_cmd, bool &&elevated, bool &&on_session): + do_cmd(std::move(do_cmd)), undo_cmd(std::move(undo_cmd)), elevated(std::move(elevated)), + on_session(std::move(on_session)) {} + explicit 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)) {} explicit prep_cmd_t(std::string &&do_cmd, bool &&elevated): do_cmd(std::move(do_cmd)), elevated(std::move(elevated)) {} std::string do_cmd; std::string undo_cmd; bool elevated; + bool on_session; }; struct sunshine_t { std::string locale; diff --git a/src/confighttp.cpp b/src/confighttp.cpp index 6d80220612c..c1f2555d61e 100644 --- a/src/confighttp.cpp +++ b/src/confighttp.cpp @@ -28,6 +28,9 @@ #include "config.h" #include "confighttp.h" #include "crypto.h" +#include "src/display_device/display_device.h" +#include "src/display_device/to_string.h" +#include "display_device/session.h" #include "file_handler.h" #include "globals.h" #include "httpcommon.h" @@ -106,6 +109,10 @@ namespace confighttp { return false; } + if (ip_type == net::LAN || ip_type == net::PC) { + return true; + } + // If credentials are shown, redirect the user to a /welcome page if (config::sunshine.username.empty()) { send_redirect(response, request, "/welcome"); @@ -542,6 +549,17 @@ namespace confighttp { response->write(data.str()); }); + auto devices { display_device::enum_available_devices() }; + + pt::ptree devices_nodes; + for (const auto &[device_id, data] : devices) { + pt::ptree devices_node; + devices_node.put("device_id"s, device_id); + devices_node.put("data"s, to_string(data)); + devices_nodes.push_back(std::make_pair(""s, devices_node)); + } + + outputTree.add_child("display_devices", devices_nodes); outputTree.put("status", "true"); outputTree.put("platform", SUNSHINE_PLATFORM); outputTree.put("version", PROJECT_VER); @@ -617,6 +635,23 @@ namespace confighttp { platf::restart(); } + void + resetDisplayDevicePersistence(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()); + }); + + display_device::session_t::get().reset_persistence(); + outputTree.put("status", true); + } + void savePassword(resp_https_t response, req_https_t request) { if (!config::sunshine.username.empty() && !authenticate(response, request)) return; @@ -821,10 +856,12 @@ 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$"]["POST"] = saveConfig; server.resource["^/api/clients/unpair$"]["POST"] = unpair; server.resource["^/api/apps/close$"]["POST"] = closeApp; server.resource["^/api/covers/upload$"]["POST"] = uploadCover; diff --git a/src/display_device/display_device.h b/src/display_device/display_device.h new file mode 100644 index 00000000000..b6c1c80cd38 --- /dev/null +++ b/src/display_device/display_device.h @@ -0,0 +1,307 @@ +#pragma once + +// standard includes +#include +#include +#include +#include + +// lib includes +#include +#include + +namespace display_device { + + /** + * @brief The device state in the operating system. + * @note On Windows you can have have multiple primary displays when they are duplicated. + */ + enum class device_state_e { + inactive, + active, + primary /**< Primary state is also implicitly active. */ + }; + + /** + * @brief The device's HDR state in the operating system. + */ + enum class hdr_state_e { + unknown, /**< HDR state could not be retrieved from the OS (even if the display supports it). */ + disabled, + enabled + }; + + // For JSON serialization for hdr_state_e + NLOHMANN_JSON_SERIALIZE_ENUM(hdr_state_e, { { hdr_state_e::unknown, "unknown" }, + { hdr_state_e::disabled, "disabled" }, + { hdr_state_e::enabled, "enabled" } }) + + /** + * @brief Ordered map of [DEVICE_ID -> hdr_state_e]. + */ + using hdr_state_map_t = std::map; + + /** + * @brief The device's HDR state in the operating system. + */ + struct device_info_t { + std::string display_name; /**< A name representing the OS display (source) the device is connected to. */ + std::string friendly_name; /**< A human-readable name for the device. */ + device_state_e device_state; /**< Device's state. @see device_state_e */ + hdr_state_e hdr_state; /**< Device's HDR state. @see hdr_state_e */ + }; + + /** + * @brief Ordered map of [DEVICE_ID -> device_info_t]. + * @see device_info_t + */ + using device_info_map_t = std::map; + + /** + * @brief Display's resolution. + */ + struct resolution_t { + unsigned int width; + unsigned int height; + + // For JSON serialization + NLOHMANN_DEFINE_TYPE_INTRUSIVE(resolution_t, width, height) + }; + + /** + * @brief Display's refresh rate. + * @note Floating point is stored in a "numerator/denominator" form. + */ + struct refresh_rate_t { + unsigned int numerator; + unsigned int denominator; + + // For JSON serialization + NLOHMANN_DEFINE_TYPE_INTRUSIVE(refresh_rate_t, numerator, denominator) + }; + + /** + * @brief Display's mode (resolution + refresh rate). + * @see resolution_t + * @see refresh_rate_t + */ + struct display_mode_t { + resolution_t resolution; + refresh_rate_t refresh_rate; + + // For JSON serialization + NLOHMANN_DEFINE_TYPE_INTRUSIVE(display_mode_t, resolution, refresh_rate) + }; + + /** + * @brief Ordered map of [DEVICE_ID -> display_mode_t]. + * @see display_mode_t + */ + using device_display_mode_map_t = std::map; + + /** + * @brief A LIST[LIST[DEVICE_ID]] structure which represents an active topology. + * + * Single display: + * [[DISPLAY_1]] + * 2 extended displays: + * [[DISPLAY_1], [DISPLAY_2]] + * 2 duplicated displays: + * [[DISPLAY_1, DISPLAY_2]] + * Mixed displays: + * [[EXTENDED_DISPLAY_1], [DUPLICATED_DISPLAY_1, DUPLICATED_DISPLAY_2], [EXTENDED_DISPLAY_2]] + * + * @note On Windows the order does not matter of both device ids or the inner lists. + */ + using active_topology_t = std::vector>; + + /** + * @brief Enumerate the available (active and inactive) devices. + * @returns A map of available devices. + * Empty map can also be returned if an error has occurred. + * + * EXAMPLES: + * ```cpp + * const auto devices { enum_available_devices() }; + * ``` + */ + device_info_map_t + enum_available_devices(); + + /** + * @brief Get display name associated with the device. + * @param device_id A device to get display name for. + * @returns A display name for the device, or an empty string if the device is inactive or not found. + * Empty string can also be returned if an error has occurred. + * @see device_info_t + * + * EXAMPLES: + * ```cpp + * const std::string device_name { "MY_DEVICE_ID" }; + * const std::string display_name = get_display_name(device_id); + * ``` + */ + std::string + get_display_name(const std::string &device_id); + + /** + * @brief Get current display modes for the devices. + * @param device_ids A list of devices to get the modes for. + * @returns A map of device modes per a device or an empty map if a mode could not be found (e.g. device is inactive). + * Empty map can also be returned if an error has occurred. + * + * EXAMPLES: + * ```cpp + * const std::unordered_set device_ids { "DEVICE_ID_1", "DEVICE_ID_2" }; + * const auto current_modes = get_current_display_modes(device_ids); + * ``` + */ + device_display_mode_map_t + get_current_display_modes(const std::unordered_set &device_ids); + + /** + * @brief Set new display modes for the devices. + * @param modes A map of modes to set. + * @returns True if modes were set, false otherwise. + * @warning if any of the specified devices are duplicated, modes modes be provided + * for duplicates too! + * + * EXAMPLES: + * ```cpp + * const std::string display_a { "MY_ID_1" }; + * const std::string display_b { "MY_ID_2" }; + * const auto success = set_display_modes({ { display_a, { { 1920, 1080 }, { 60, 1 } } }, + * { display_b, { { 1920, 1080 }, { 120, 1 } } } }); + * ``` + */ + bool + set_display_modes(const device_display_mode_map_t &modes); + + /** + * @brief Check whether the specified device is primary. + * @param device_id A device to perform the check for. + * @returns True if the device is primary, false otherwise. + * @see device_state_e + * + * EXAMPLES: + * ```cpp + * const std::string device_id { "MY_DEVICE_ID" }; + * const bool is_primary = is_primary_device(device_id); + * ``` + */ + bool + is_primary_device(const std::string &device_id); + + /** + * @brief Set the device as a primary display. + * @param device_id A device to set as primary. + * @returns True if the device is or was set as primary, false otherwise. + * @note On Windows if the device is duplicated, the other duplicated device(-s) will also become a primary device. + * + * EXAMPLES: + * ```cpp + * const std::string device_id { "MY_DEVICE_ID" }; + * const bool success = set_as_primary_device(device_id); + * `` + */ + bool + set_as_primary_device(const std::string &device_id); + + /** + * @brief Get HDR state for the devices. + * @param device_ids A list of devices to get the HDR states for. + * @returns A map of HDR states per a device or an empty map if an error has occurred. + * @note On Windows the state cannot be retrieved until the device is active even if it supports it. + * + * EXAMPLES: + * ```cpp + * const std::unordered_set device_ids { "DEVICE_ID_1", "DEVICE_ID_2" }; + * const auto current_hdr_states = get_current_hdr_states(device_ids); + * ``` + */ + hdr_state_map_t + get_current_hdr_states(const std::unordered_set &device_ids); + + /** + * @brief Set HDR states for the devices. + * @param modes A map of HDR states to set. + * @returns True if HDR states were set, false otherwise. + * @note If `unknown` states are provided, they will be silently ignored + * and current state will not be changed. + * + * EXAMPLES: + * ```cpp + * const std::string display_a { "MY_ID_1" }; + * const std::string display_b { "MY_ID_2" }; + * const auto success = set_hdr_states({ { display_a, hdr_state_e::enabled }, + * { display_b, hdr_state_e::disabled } }); + * ``` + */ + bool + set_hdr_states(const hdr_state_map_t &states); + + /** + * @brief Get the active (current) topology. + * @returns A list representing the current topology. + * Empty list can also be returned if an error has occurred. + * + * EXAMPLES: + * ```cpp + * const auto current_topology { get_current_topology() }; + * ``` + */ + active_topology_t + get_current_topology(); + + /** + * @brief Verify if the active topology is valid. + * + * This is mostly meant as a sanity check or to verify that it is still valid + * after a manual modification to an existing topology. + * + * @param topology Topology to validated. + * @returns True if it is valid, false otherwise. + * + * EXAMPLES: + * ```cpp + * auto current_topology { get_current_topology() }; + * // Modify the current_topology + * const bool is_valid = is_topology_valid(current_topology); + * ``` + */ + bool + is_topology_valid(const active_topology_t &topology); + + /** + * @brief Check if the topologies are close enough to be considered the same by the OS. + * @param topology_a First topology to compare. + * @param topology_b Second topology to compare. + * @returns True if topologies are close enough, false otherwise. + * + * EXAMPLES: + * ```cpp + * auto current_topology { get_current_topology() }; + * auto new_topology { current_topology }; + * // Modify the new_topology + * const bool is_the_same = is_topology_the_same(current_topology, new_topology); + * ``` + */ + bool + is_topology_the_same(const active_topology_t &topology_a, const active_topology_t &topology_b); + + /** + * @brief Set the a new active topology for the OS. + * @param new_topology New device topology to set. + * @returns True if the new topology has been set, false otherwise. + * + * EXAMPLES: + * ```cpp + * auto current_topology { get_current_topology() }; + * // Modify the current_topology + * const bool success = set_topology(current_topology); + * ``` + */ + bool + set_topology(const active_topology_t &new_topology); + +} // namespace display_device diff --git a/src/display_device/parsed_config.cpp b/src/display_device/parsed_config.cpp new file mode 100644 index 00000000000..915be8873c5 --- /dev/null +++ b/src/display_device/parsed_config.cpp @@ -0,0 +1,561 @@ +// lib includes +#include +#include +#include + +// local includes +#include "parsed_config.h" +#include "src/config.h" +#include "src/logging.h" +#include "src/rtsp.h" +#include "to_string.h" + +namespace display_device { + + namespace { + /** + * @brief Parse resolution value from the string. + * @param input String to be parsed. + * @param output Reference to output variable. + * @returns True on successful parsing (empty string allowed), false otherwise. + * + * EXAMPLES: + * ```cpp + * boost::optional resolution; + * if (parse_resolution_string("1920x1080", resolution)) { + * if (resolution) { + * // Value was specified + * } + * else { + * // Value was empty + * } + * } + * ``` + */ + bool + parse_resolution_string(const std::string &input, boost::optional &output) { + const std::string trimmed_input { boost::algorithm::trim_copy(input) }; + const boost::regex resolution_regex { R"(^(\d+)x(\d+)$)" }; // std::regex hangs in CTOR for some reason when called in a thread. Problem with MSYS2 packages (UCRT64), maybe? + + boost::smatch match; + if (boost::regex_match(trimmed_input, match, resolution_regex)) { + try { + output = resolution_t { + static_cast(std::stol(match[1])), + static_cast(std::stol(match[2])) + }; + } + catch (const std::invalid_argument &err) { + BOOST_LOG(error) << "Failed to parse resolution string " << trimmed_input << " (invalid argument):\n" + << err.what(); + return false; + } + catch (const std::out_of_range &err) { + BOOST_LOG(error) << "Failed to parse resolution string " << trimmed_input << " (number out of range):\n" + << err.what(); + return false; + } + catch (const std::exception &err) { + BOOST_LOG(error) << "Failed to parse resolution string " << trimmed_input << ":\n" + << err.what(); + return false; + } + } + else { + output = boost::none; + + if (!trimmed_input.empty()) { + BOOST_LOG(error) << "Failed to parse resolution string " << trimmed_input << ". It must match a \"1920x1080\" pattern!"; + return false; + } + } + + return true; + } + + /** + * @brief Parse refresh rate value from the string. + * @param input String to be parsed. + * @param output Reference to output variable. + * @param allow_decimal_point Specify whether the decimal point is allowed in the string. + * @returns True on successful parsing (empty string allowed), false otherwise. + * + * EXAMPLES: + * ```cpp + * boost::optional refresh_rate; + * if (parse_refresh_rate_string("59.95", refresh_rate)) { + * if (refresh_rate) { + * // Value was specified + * } + * else { + * // Value was empty + * } + * } + * ``` + */ + bool + parse_refresh_rate_string(const std::string &input, boost::optional &output, bool allow_decimal_point = true) { + const std::string trimmed_input { boost::algorithm::trim_copy(input) }; + // std::regex hangs in CTOR for some reason when called in a thread. Problem with MSYS2 packages (UCRT64), maybe? + const boost::regex refresh_rate_regex { allow_decimal_point ? R"(^(\d+)(?:\.(\d+))?$)" : R"(^(\d+)$)" }; + + boost::smatch match; + if (boost::regex_match(trimmed_input, match, refresh_rate_regex)) { + try { + if (allow_decimal_point && match[2].matched) { + // 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 { match[1].str() + match[2].str() }; + const auto numerator { static_cast(std::stol(numerator_str)) }; + + // Here we are counting decimal places and calculating denominator: 10^decimal_places + const auto denominator { static_cast(std::pow(10, std::distance(match[2].first, match[2].second))) }; + + output = refresh_rate_t { numerator, denominator }; + } + else { + // We do not have a decimal point, just a valid number. + // For example: + // 60: + // numerator = 60 + // denominator = 1 + output = refresh_rate_t { static_cast(std::stol(match[1])), 1 }; + } + } + catch (const std::invalid_argument &err) { + BOOST_LOG(error) << "Failed to parse refresh rate or FPS string " << trimmed_input << " (invalid argument):\n" + << err.what(); + return false; + } + catch (const std::out_of_range &err) { + BOOST_LOG(error) << "Failed to parse refresh rate or FPS string " << trimmed_input << " (number out of range):\n" + << err.what(); + return false; + } + catch (const std::exception &err) { + BOOST_LOG(error) << "Failed to parse refresh rate or FPS string " << trimmed_input << ":\n" + << err.what(); + return false; + } + } + else { + output = boost::none; + + if (!trimmed_input.empty()) { + BOOST_LOG(error) << "Failed to parse refresh rate or FPS string " << trimmed_input << ". Must have a pattern of " << (allow_decimal_point ? "\"123\" or \"123.456\"" : "\"123\"") << "!"; + return false; + } + } + + return true; + } + + /** + * @brief Parse resolution option from the user configuration and the session information. + * @param config User's video related configuration. + * @param session Session information. + * @param parsed_config A reference to a config object that will be modified on success. + * @returns True on successful parsing, false otherwise. + * + * EXAMPLES: + * ```cpp + * const std::shared_ptr launch_session; // Assuming ptr is properly initialized + * const config::video_t &video_config { config::video }; + * + * parsed_config_t parsed_config; + * const bool success = parse_resolution_option(video_config, *launch_session, parsed_config); + * ``` + */ + bool + parse_resolution_option(const config::video_t &config, const rtsp_stream::launch_session_t &session, parsed_config_t &parsed_config) { + const auto resolution_option { static_cast(config.resolution_change) }; + switch (resolution_option) { + case parsed_config_t::resolution_change_e::automatic: { + if (!session.enable_sops) { + BOOST_LOG(warning) << "Sunshine is configured to change resolution automatically, but the \"Optimize game settings\" is not set in the client! Resolution will not be changed."; + parsed_config.resolution = boost::none; + } + else if (session.width >= 0 && session.height >= 0) { + parsed_config.resolution = resolution_t { + 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 parsed_config_t::resolution_change_e::manual: { + if (!session.enable_sops) { + BOOST_LOG(warning) << "Sunshine is configured to change resolution manually, but the \"Optimize game settings\" is not set in the client! Resolution will not be changed."; + parsed_config.resolution = boost::none; + } + else { + if (!parse_resolution_string(config.manual_resolution, parsed_config.resolution)) { + BOOST_LOG(error) << "Failed to parse manual resolution string!"; + return false; + } + + if (!parsed_config.resolution) { + BOOST_LOG(error) << "Manual resolution must be specified!"; + return false; + } + } + break; + } + case parsed_config_t::resolution_change_e::no_operation: + default: + break; + } + + return true; + } + + /** + * @brief Parse refresh rate option from the user configuration and the session information. + * @param config User's video related configuration. + * @param session Session information. + * @param parsed_config A reference to a config object that will be modified on success. + * @returns True on successful parsing, false otherwise. + * + * EXAMPLES: + * ```cpp + * const std::shared_ptr launch_session; // Assuming ptr is properly initialized + * const config::video_t &video_config { config::video }; + * + * parsed_config_t parsed_config; + * const bool success = parse_refresh_rate_option(video_config, *launch_session, parsed_config); + * ``` + */ + bool + parse_refresh_rate_option(const config::video_t &config, const rtsp_stream::launch_session_t &session, parsed_config_t &parsed_config) { + const auto refresh_rate_option { static_cast(config.refresh_rate_change) }; + switch (refresh_rate_option) { + case parsed_config_t::refresh_rate_change_e::automatic: { + if (session.fps >= 0) { + parsed_config.refresh_rate = refresh_rate_t { static_cast(session.fps), 1 }; + } + else { + BOOST_LOG(error) << "FPS value provided by client session config is invalid: " << session.fps; + return false; + } + break; + } + case parsed_config_t::refresh_rate_change_e::manual: { + if (!parse_refresh_rate_string(config.manual_refresh_rate, parsed_config.refresh_rate)) { + BOOST_LOG(error) << "Failed to parse manual refresh rate string!"; + return false; + } + + if (!parsed_config.refresh_rate) { + BOOST_LOG(error) << "Manual refresh rate must be specified!"; + return false; + } + break; + } + case parsed_config_t::refresh_rate_change_e::no_operation: + default: + break; + } + + return true; + } + + /** + * @brief Remap the already parsed display mode based on the user configuration. + * @param config User's video related configuration. + * @param parsed_config A reference to a config object that will be modified on success. + * @returns True is display mode was remapped or no remapping was needed, false otherwise. + * + * EXAMPLES: + * ```cpp + * const std::shared_ptr launch_session; // Assuming ptr is properly initialized + * const config::video_t &video_config { config::video }; + * + * parsed_config_t parsed_config; + * const bool success = remap_display_modes_if_needed(video_config, *launch_session, parsed_config); + * ``` + */ + bool + remap_display_modes_if_needed(const config::video_t &config, const rtsp_stream::launch_session_t &session, parsed_config_t &parsed_config) { + constexpr auto mixed_remapping { "" }; + constexpr auto resolution_only_remapping { "resolution_only" }; + constexpr auto refresh_rate_only_remapping { "refresh_rate_only" }; + + const auto resolution_option { static_cast(config.resolution_change) }; + const auto refresh_rate_option { static_cast(config.refresh_rate_change) }; + + // Copy only the remapping values that we can actually use with our configuration options + std::vector remapping_values; + std::copy_if(std::begin(config.display_mode_remapping), std::end(config.display_mode_remapping), std::back_inserter(remapping_values), [&](const auto &value) { + if (resolution_option == parsed_config_t::resolution_change_e::automatic && refresh_rate_option == parsed_config_t::refresh_rate_change_e::automatic) { + return value.type == mixed_remapping; // Comparison instead of empty check to be explicit + } + else if (resolution_option == parsed_config_t::resolution_change_e::automatic) { + return value.type == resolution_only_remapping; + } + else if (refresh_rate_option == parsed_config_t::refresh_rate_change_e::automatic) { + return value.type == refresh_rate_only_remapping; + } + + return false; + }); + + if (remapping_values.empty()) { + BOOST_LOG(debug) << "No values are available for display mode remapping."; + return true; + } + BOOST_LOG(debug) << "Trying to remap display modes..."; + + struct parsed_remapping_values_t { + boost::optional received_resolution; + boost::optional received_fps; + boost::optional final_resolution; + boost::optional final_refresh_rate; + }; + + std::vector parsed_values; + for (const auto &entry : remapping_values) { + boost::optional received_resolution; + boost::optional received_fps; + boost::optional final_resolution; + boost::optional final_refresh_rate; + + if (entry.type == resolution_only_remapping) { + if (!parse_resolution_string(entry.received_resolution, received_resolution) || + !parse_resolution_string(entry.final_resolution, final_resolution)) { + BOOST_LOG(error) << "Failed to parse entry value: " << entry.received_resolution << " -> " << entry.final_resolution; + return false; + } + + if (!received_resolution || !final_resolution) { + BOOST_LOG(error) << "Both values must be set for remapping resolution! Current entry value: " << entry.received_resolution << " -> " << entry.final_resolution; + return false; + } + + if (!session.enable_sops) { + BOOST_LOG(warning) << "Skipping remapping resolution, because the \"Optimize game settings\" is not set in the client!"; + return true; + } + } + else if (entry.type == refresh_rate_only_remapping) { + if (!parse_refresh_rate_string(entry.received_fps, received_fps, false) || + !parse_refresh_rate_string(entry.final_refresh_rate, final_refresh_rate)) { + BOOST_LOG(error) << "Failed to parse entry value: " << entry.received_fps << " -> " << entry.final_refresh_rate; + return false; + } + + if (!received_fps || !final_refresh_rate) { + BOOST_LOG(error) << "Both values must be set for remapping refresh rate! Current entry value: " << entry.received_fps << " -> " << entry.final_refresh_rate; + return false; + } + } + else { + if (!parse_resolution_string(entry.received_resolution, received_resolution) || + !parse_refresh_rate_string(entry.received_fps, received_fps, false) || + !parse_resolution_string(entry.final_resolution, final_resolution) || + !parse_refresh_rate_string(entry.final_refresh_rate, final_refresh_rate)) { + BOOST_LOG(error) << "Failed to parse entry value: " + << "[" << entry.received_resolution << "|" << entry.received_fps << "] -> [" << entry.final_resolution << "|" << entry.final_refresh_rate << "]"; + return false; + } + + if ((!received_resolution && !received_fps) || (!final_resolution && !final_refresh_rate)) { + BOOST_LOG(error) << "At least one received and final value must be set for remapping display modes! Entry: " + << "[" << entry.received_resolution << "|" << entry.received_fps << "] -> [" << entry.final_resolution << "|" << entry.final_refresh_rate << "]"; + return false; + } + + if (!session.enable_sops && (received_resolution || final_resolution)) { + BOOST_LOG(warning) << "Skipping remapping entry, because the \"Optimize game settings\" is not set in the client! Entry: " + << "[" << entry.received_resolution << "|" << entry.received_fps << "] -> [" << entry.final_resolution << "|" << entry.final_refresh_rate << "]"; + continue; + } + } + + parsed_values.push_back({ received_resolution, received_fps, final_resolution, final_refresh_rate }); + } + + const auto compare_resolution { [](const resolution_t &a, const resolution_t &b) { + return a.width == b.width && a.height == b.height; + } }; + const auto compare_refresh_rate { [](const refresh_rate_t &a, const refresh_rate_t &b) { + return a.numerator == b.numerator && a.denominator == b.denominator; + } }; + + for (const auto &entry : parsed_values) { + bool do_remap { false }; + if (entry.received_resolution && entry.received_fps) { + if (parsed_config.resolution && parsed_config.refresh_rate) { + do_remap = compare_resolution(*entry.received_resolution, *parsed_config.resolution) && compare_refresh_rate(*entry.received_fps, *parsed_config.refresh_rate); + } + else { + // Sanity check + BOOST_LOG(error) << "Cannot remap: (parsed_config.resolution && parsed_config.refresh_rate) == false!"; + return false; + } + } + else if (entry.received_resolution) { + if (parsed_config.resolution) { + do_remap = compare_resolution(*entry.received_resolution, *parsed_config.resolution); + } + else { + // Sanity check + BOOST_LOG(error) << "Cannot remap: parsed_config.resolution == false!"; + return false; + } + } + else if (entry.received_fps) { + if (parsed_config.refresh_rate) { + do_remap = compare_refresh_rate(*entry.received_fps, *parsed_config.refresh_rate); + } + else { + // Sanity check + BOOST_LOG(error) << "Cannot remap: parsed_config.refresh_rate == false!"; + return false; + } + } + else { + // Sanity check + BOOST_LOG(error) << "Cannot remap: (entry.received_resolution || entry.received_fps) == false!"; + return false; + } + + if (do_remap) { + if (!entry.final_resolution && !entry.final_refresh_rate) { + // Sanity check + BOOST_LOG(error) << "Cannot remap: (!entry.final_resolution && !entry.final_refresh_rate) == true!"; + return false; + } + + if (entry.final_resolution) { + BOOST_LOG(debug) << "Remapping resolution to: " << to_string(*entry.final_resolution); + parsed_config.resolution = entry.final_resolution; + } + if (entry.final_refresh_rate) { + BOOST_LOG(debug) << "Remapping refresh rate to: " << to_string(*entry.final_refresh_rate); + parsed_config.refresh_rate = entry.final_refresh_rate; + } + + break; + } + } + + return true; + } + + /** + * @brief Parse HDR option from the user configuration and the session information. + * @param config User's video related configuration. + * @param session Session information. + * @returns Parsed HDR state value we need to switch to (true == ON, false == OFF). + * Empty optional if no action is required. + * + * EXAMPLES: + * ```cpp + * const std::shared_ptr launch_session; // Assuming ptr is properly initialized + * const config::video_t &video_config { config::video }; + * const auto hdr_option = parse_hdr_option(video_config, *launch_session); + * ``` + */ + boost::optional + parse_hdr_option(const config::video_t &config, const rtsp_stream::launch_session_t &session) { + const auto hdr_prep_option { static_cast(config.hdr_prep) }; + switch (hdr_prep_option) { + case parsed_config_t::hdr_prep_e::automatic: + return session.enable_hdr; + case parsed_config_t::hdr_prep_e::no_operation: + default: + return boost::none; + } + } + } // namespace + + int + parsed_config_t::device_prep_from_view(std::string_view value) { + using namespace std::string_view_literals; +#define _CONVERT_(x) \ + if (value == #x##sv) return static_cast(parsed_config_t::device_prep_e::x); + _CONVERT_(no_operation); + _CONVERT_(ensure_active); + _CONVERT_(ensure_primary); + _CONVERT_(ensure_only_display); +#undef _CONVERT_ + return static_cast(parsed_config_t::device_prep_e::no_operation); + } + + int + parsed_config_t::resolution_change_from_view(std::string_view value) { + using namespace std::string_view_literals; +#define _CONVERT_(x) \ + if (value == #x##sv) return static_cast(parsed_config_t::resolution_change_e::x); + _CONVERT_(no_operation); + _CONVERT_(automatic); + _CONVERT_(manual); +#undef _CONVERT_ + return static_cast(parsed_config_t::resolution_change_e::no_operation); + } + + int + parsed_config_t::refresh_rate_change_from_view(std::string_view value) { + using namespace std::string_view_literals; +#define _CONVERT_(x) \ + if (value == #x##sv) return static_cast(parsed_config_t::refresh_rate_change_e::x); + _CONVERT_(no_operation); + _CONVERT_(automatic); + _CONVERT_(manual); +#undef _CONVERT_ + return static_cast(parsed_config_t::refresh_rate_change_e::no_operation); + } + + int + parsed_config_t::hdr_prep_from_view(std::string_view value) { + using namespace std::string_view_literals; +#define _CONVERT_(x) \ + if (value == #x##sv) return static_cast(parsed_config_t::hdr_prep_e::x); + _CONVERT_(no_operation); + _CONVERT_(automatic); +#undef _CONVERT_ + return static_cast(parsed_config_t::hdr_prep_e::no_operation); + } + + boost::optional + make_parsed_config(const config::video_t &config, const rtsp_stream::launch_session_t &session) { + parsed_config_t parsed_config; + parsed_config.device_id = config.output_name; + parsed_config.device_prep = static_cast(config.display_device_prep); + parsed_config.change_hdr_state = parse_hdr_option(config, session); + + if (!parse_resolution_option(config, session, parsed_config)) { + // Error already logged + return boost::none; + } + + if (!parse_refresh_rate_option(config, session, parsed_config)) { + // Error already logged + return boost::none; + } + + if (!remap_display_modes_if_needed(config, session, parsed_config)) { + // Error already logged + return boost::none; + } + + BOOST_LOG(debug) << "Parsed display device config:\n" + << "device_id: " << parsed_config.device_id << "\n" + << "device_prep: " << static_cast(parsed_config.device_prep) << "\n" + << "change_hdr_state: " << (parsed_config.change_hdr_state ? *parsed_config.change_hdr_state ? "true" : "false" : "none") << "\n" + << "resolution: " << (parsed_config.resolution ? to_string(*parsed_config.resolution) : "none") << "\n" + << "refresh_rate: " << (parsed_config.refresh_rate ? to_string(*parsed_config.refresh_rate) : "none") << "\n"; + + return parsed_config; + } + +} // namespace display_device diff --git a/src/display_device/parsed_config.h b/src/display_device/parsed_config.h new file mode 100644 index 00000000000..b7b840c369d --- /dev/null +++ b/src/display_device/parsed_config.h @@ -0,0 +1,140 @@ +#pragma once + +// local includes +#include "display_device.h" + +// forward declarations +namespace config { + struct video_t; +} +namespace rtsp_stream { + struct launch_session_t; +} + +namespace display_device { + + /** + * @brief Configuration containing parsed information from the user config (video related) + * and the current session. + */ + struct parsed_config_t { + /** + * @brief Enum detailing how to prepare the display device. + */ + enum class device_prep_e : int { + no_operation, /**< User has to make sure the display device is active, we will only verify. */ + ensure_active, /**< Activate the device if needed. */ + ensure_primary, /**< Activate the device if needed and make it a primary display. */ + ensure_only_display /**< Deactivate other displays and turn on the specified one only. */ + }; + + /** + * @brief Convert the string to the matching value of device_prep_e. + * @param value String value to map to device_prep_e. + * @returns A device_prep_e value (converted to int) that matches the string + * or the default value if string does not match anything. + * @see device_prep_e + * + * EXAMPLES: + * ```cpp + * const int device_prep = device_prep_from_view("ensure_only_display"); + * ``` + */ + static int + device_prep_from_view(std::string_view value); + + /** + * @brief Enum detailing how to change the display's resolution. + */ + enum class resolution_change_e : int { + no_operation, /**< Keep the current resolution. */ + automatic, /**< Set the resolution to the one received from the client if the "Optimize game settings" option is also enabled in the client. */ + manual /**< User has to specify the resolution ("Optimize game settings" option must be enabled in the client). */ + }; + + /** + * @brief Convert the string to the matching value of resolution_change_e. + * @param value String value to map to resolution_change_e. + * @returns A resolution_change_e value (converted to int) that matches the string + * or the default value if string does not match anything. + * @see resolution_change_e + * + * EXAMPLES: + * ```cpp + * const int resolution_change = resolution_change_from_view("manual"); + * ``` + */ + static int + resolution_change_from_view(std::string_view value); + + /** + * @brief Enum detailing how to change the display's refresh rate. + */ + enum class refresh_rate_change_e : int { + no_operation, /**< Keep the current refresh rate. */ + automatic, /**< Set the refresh rate to the FPS value received from the client. */ + manual /**< User has to specify the refresh rate. */ + }; + + /** + * @brief Convert the string to the matching value of refresh_rate_change_e. + * @param value String value to map to refresh_rate_change_e. + * @returns A refresh_rate_change_e value (converted to int) that matches the string + * or the default value if string does not match anything. + * @see refresh_rate_change_e + * + * EXAMPLES: + * ```cpp + * const int refresh_rate_change = refresh_rate_change_from_view("manual"); + * ``` + */ + static int + refresh_rate_change_from_view(std::string_view value); + + /** + * @brief Enum detailing how to change the display's HDR state. + */ + enum class hdr_prep_e : int { + no_operation, /**< User has to switch the HDR state manually */ + automatic /**< Switch HDR state based on the session settings and if display supports it. */ + }; + + /** + * @brief Convert the string to the matching value of hdr_prep_e. + * @param value String value to map to hdr_prep_e. + * @returns A hdr_prep_e value (converted to int) that matches the string + * or the default value if string does not match anything. + * @see hdr_prep_e + * + * EXAMPLES: + * ```cpp + * const int hdr_prep = hdr_prep_from_view("automatic"); + * ``` + */ + static int + hdr_prep_from_view(std::string_view value); + + std::string device_id; /**< Device id manually provided by the user via config. */ + device_prep_e device_prep; /**< The device_prep_e value taken from config. */ + boost::optional resolution; /**< Parsed resolution value we need to switch to. Empty optional if no action is required. */ + boost::optional refresh_rate; /**< Parsed refresh rate value we need to switch to. Empty optional if no action is required. */ + boost::optional change_hdr_state; /**< Parsed HDR state value we need to switch to (true == ON, false == OFF). Empty optional if no action is required. */ + }; + + /** + * @brief Parse the user configuration and the session information. + * @param config User's video related configuration. + * @param session Session information. + * @returns Parsed configuration or empty optional if parsing has failed. + * + * EXAMPLES: + * ```cpp + * const std::shared_ptr launch_session; // Assuming ptr is properly initialized + * const config::video_t &video_config { config::video }; + * const auto parsed_config = make_parsed_config(video_config, *launch_session); + * ``` + */ + boost::optional + make_parsed_config(const config::video_t &config, const rtsp_stream::launch_session_t &session); + +} // namespace display_device diff --git a/src/display_device/session.cpp b/src/display_device/session.cpp new file mode 100644 index 00000000000..c1cf38dba1b --- /dev/null +++ b/src/display_device/session.cpp @@ -0,0 +1,225 @@ +// standard includes +#include + +// local includes +#include "session.h" +#include "src/platform/common.h" +#include "to_string.h" + +namespace display_device { + + class session_t::StateRetryTimer { + public: + /** + * @brief A constructor for the timer. + * @param mutex A shared mutex for synchronization. + * @warning Because we are keeping references to shared parameters, we MUST ensure they outlive this object! + */ + StateRetryTimer(std::mutex &mutex): + mutex { mutex }, timer_thread { + std::thread { [this]() { + std::unique_lock lock { this->mutex }; + while (keep_alive) { + can_wake_up = false; + if (next_wake_up_time) { + // We're going to sleep forever until manually woken up or the time elapses + sleep_cv.wait_until(lock, *next_wake_up_time, [this]() { return can_wake_up; }); + } + else { + // We're going to sleep forever until manually woken up + sleep_cv.wait(lock, [this]() { return can_wake_up; }); + } + + if (next_wake_up_time) { + // Timer has just been started, or we have waited for the required amount of time. + // We can check which case it is by comparing time points. + + const auto now { std::chrono::steady_clock::now() }; + if (now < *next_wake_up_time) { + // Thread has been woken up manually to synchronize the time points. + // We do nothing and just go back to waiting with a new time point. + } + else { + next_wake_up_time = boost::none; + + const auto result { !this->retry_function || this->retry_function() }; + if (!result) { + next_wake_up_time = now + this->timeout_duration; + } + } + } + else { + // Timer has been stopped. + // We do nothing and just go back to waiting until notified (unless we are killing the thread). + } + } + } } + } { + } + + /** + * @brief A destructor for the timer that gracefully shuts down the thread. + */ + ~StateRetryTimer() { + { + std::lock_guard lock { mutex }; + keep_alive = false; + next_wake_up_time = boost::none; + wake_up_thread(); + } + + timer_thread.join(); + } + + /** + * @brief Start or stop the timer thread. + * @param retry_function Function to be executed every X seconds. + * If the function returns true, the loop is stopped. + * If the function is of type nullptr_t, the loop is stopped. + * @warning This method does NOT acquire the mutex! It is intended to be used from places + * where the mutex has already been locked. + */ + void + setup_timer(std::function retry_function) { + this->retry_function = std::move(retry_function); + + if (this->retry_function) { + next_wake_up_time = std::chrono::steady_clock::now() + timeout_duration; + } + else { + if (!next_wake_up_time) { + return; + } + + next_wake_up_time = boost::none; + } + + wake_up_thread(); + } + + private: + /** + * @brief Manually wake up the thread. + */ + void + wake_up_thread() { + can_wake_up = true; + sleep_cv.notify_one(); + } + + std::mutex &mutex; /**< A reference to a shared mutex. */ + std::chrono::seconds timeout_duration { 5 }; /**< A retry time for the timer. */ + std::function retry_function; /**< Function to be executed until it succeeds. */ + + std::thread timer_thread; /**< A timer thread. */ + std::condition_variable sleep_cv; /**< Condition variable for waking up thread. */ + + bool can_wake_up { false }; /**< Safeguard for the condition variable to prevent sporadic thread wake ups. */ + bool keep_alive { true }; /**< A kill switch for the thread when it has been woken up. */ + boost::optional next_wake_up_time; /**< Next time point for thread to wake up. */ + }; + + session_t::deinit_t::~deinit_t() { + session_t::get().restore_state(); + } + + session_t & + session_t::get() { + static session_t session; + return session; + } + + std::unique_ptr + session_t::init() { + const auto devices { enum_available_devices() }; + if (!devices.empty()) { + BOOST_LOG(info) << "Available display devices: " << to_string(devices); + } + + session_t::get().settings.set_filepath(platf::appdata() / "original_display_settings.json"); + session_t::get().restore_state(); + return std::make_unique(); + } + + void + session_t::configure_display(const config::video_t &config, const rtsp_stream::launch_session_t &session) { + std::lock_guard lock { mutex }; + + const auto parsed_config { make_parsed_config(config, session) }; + if (!parsed_config) { + BOOST_LOG(error) << "Failed to parse configuration for the the display device settings!"; + return; + } + + if (settings.is_changing_settings_going_to_fail()) { + timer->setup_timer([this, config_copy = *parsed_config]() { + if (settings.is_changing_settings_going_to_fail()) { + BOOST_LOG(warning) << "Applying display settings will fail - retrying later..."; + return false; + } + + const auto result { settings.apply_config(config_copy) }; + if (!result) { + BOOST_LOG(warning) << "Failed to apply display settings - will stop trying, but will allow stream to continue."; + + // WARNING! After call to the method below, this lambda function is no be longer valid! + // DO NOT access anything from the capture list! + restore_state_impl(); + } + return true; + }); + + BOOST_LOG(warning) << "It is already known that display settings cannot be changed. Allowing stream to start without changing the settings, but will retry changing settings later..."; + return; + } + + const auto result { settings.apply_config(*parsed_config) }; + if (result) { + timer->setup_timer(nullptr); + } + else { + restore_state_impl(); + } + } + + void + session_t::restore_state() { + std::lock_guard lock { mutex }; + restore_state_impl(); + } + + void + session_t::reset_persistence() { + std::lock_guard lock { mutex }; + + settings.reset_persistence(); + timer->setup_timer(nullptr); + } + + void + session_t::restore_state_impl() { + const auto result { !settings.is_changing_settings_going_to_fail() && settings.revert_settings() }; + if (result) { + timer->setup_timer(nullptr); + } + else { + if (settings.is_changing_settings_going_to_fail()) { + BOOST_LOG(warning) << "Reverting display settings will fail - retrying later..."; + } + + timer->setup_timer([this]() { + if (settings.is_changing_settings_going_to_fail()) { + BOOST_LOG(warning) << "Reverting display settings will still fail - retrying later..."; + return false; + } + + return settings.revert_settings(); + }); + } + } + + session_t::session_t(): + timer { std::make_unique(mutex) } { + } + +} // namespace display_device diff --git a/src/display_device/session.h b/src/display_device/session.h new file mode 100644 index 00000000000..68fc935d53a --- /dev/null +++ b/src/display_device/session.h @@ -0,0 +1,190 @@ +#pragma once + +// standard includes +#include + +// local includes +#include "settings.h" + +namespace display_device { + + /** + * @brief A singleton class for managing the display device configuration for the whole Sunshine session. + * + * This class is meant to be an entry point for applying the configuration and reverting it later + * from within the various places in the Sunshine's source code. + * + * It is similar to settings_t and is more or less a wrapper around it. + * However, this class ensures thread-safe usage for the methods and additionally + * performs automatic cleanups. + * + * @note A lazy-evaluated, correctly-destroyed, thread-safe singleton pattern is used here (https://stackoverflow.com/a/1008289). + */ + class session_t { + public: + /** + * @brief A class that uses RAII to perform cleanup when it's destroyed. + * @note The deinit_t usage pattern is used here instead of the session_t destructor + * to expedite the cleanup process in case of Sunshine termination. + * @see session_t::init() + */ + class deinit_t { + public: + /** + * @brief A destructor that restores (or tries to) the initial state. + */ + virtual ~deinit_t(); + }; + + /** + * @brief Get the singleton instance. + * @returns Singleton instance for the class. + * + * EXAMPLES: + * ```cpp + * session_t& session { session_t::get() }; + * ``` + */ + static session_t & + get(); + + /** + * @brief Initialize the singleton and perform the initial state recovery (if needed). + * @returns A deinit_t instance that performs cleanup when destroyed. + * @see deinit_t + * + * EXAMPLES: + * ```cpp + * const auto session_guard { session_t::init() }; + * ``` + */ + static std::unique_ptr + init(); + + /** + * @brief Configure the display device based on the user configuration and the session information. + * + * Upon failing to completely apply configuration, the applied settings will be reverted. + * Or, in some cases, we will keep retrying even when the stream has already started as there + * is no possibility to apply settings before the stream start. + * + * @param config User's video related configuration. + * @param session Session information. + * @note There is no return value as we still want to continue with the stream, so that + * users can do something about it once they are connected. Otherwise, we might + * prevent users from logging in at all... + * + * EXAMPLES: + * ```cpp + * const std::shared_ptr launch_session; // Assuming ptr is properly initialized + * const config::video_t &video_config { config::video }; + * + * session_t::get().configure_display(video_config, *launch_session); + * ``` + */ + void + configure_display(const config::video_t &config, const rtsp_stream::launch_session_t &session); + + /** + * @brief Revert the display configuration and restore the previous state. + * @note This method automatically loads the persistence (if any) from the previous Sunshine session. + * @note In case the state could not be restored, it will be retried again in X seconds + * (repeating indefinitely until success or until persistence is reset). + * + * EXAMPLES: + * ```cpp + * const std::shared_ptr launch_session; // Assuming ptr is properly initialized + * const config::video_t &video_config { config::video }; + * + * const auto result = session_t::get().configure_display(video_config, *launch_session); + * if (result) { + * // Wait for some time + * session_t::get().restore_state(); + * } + * ``` + */ + void + restore_state(); + + /** + * @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 and refuses start the stream in most cases. + * + * 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. + * + * @note This also stops the retry timer. + * + * EXAMPLES: + * ```cpp + * const std::shared_ptr launch_session; // Assuming ptr is properly initialized + * const config::video_t &video_config { config::video }; + * + * const auto result = session_t::get().configure_display(video_config, *launch_session); + * if (!result) { + * // Wait for user to decide what to do + * const bool user_wants_reset { true }; + * if (user_wants_reset) { + * session_t::get().reset_persistence(); + * } + * } + * ``` + */ + void + reset_persistence(); + + /** + * @brief A deleted copy constructor for singleton pattern. + * @note Public to ensure better error message. + */ + session_t(session_t const &) = delete; + + /** + * @brief A deleted assignment operator for singleton pattern. + * @note Public to ensure better error message. + */ + void + operator=(session_t const &) = delete; + + private: + /** + * @brief A class for retrying to set/reset state. + * + * This timer class spins a thread which is mostly sleeping all the time, but can be + * configured to wake up every X seconds. + * + * It is tightly synchronized with the session_t class via a shared mutex to ensure + * that stupid race conditions do not happen where we successfully apply settings + * for them to be reset by the timer thread immediately. + */ + class StateRetryTimer; + + /** + * @brief A private constructor to ensure the singleton pattern. + * @note Cannot be defaulted in declaration because of forward declared StateRetryTimer. + */ + explicit session_t(); + + /** + * @brief An implementation of `restore_state` without a mutex lock. + * @see restore_state for the description. + */ + void + restore_state_impl(); + + settings_t settings; /**< A class for managing display device settings. */ + std::mutex mutex; /**< A mutex for ensuring thread-safety. */ + + /** + * @brief An instance of StateRetryTimer. + * @warning MUST BE declared after the settings and mutex members to ensure proper destruction order!. + */ + std::unique_ptr timer; + }; + +} // namespace display_device diff --git a/src/display_device/settings.cpp b/src/display_device/settings.cpp new file mode 100644 index 00000000000..dab8ee89103 --- /dev/null +++ b/src/display_device/settings.cpp @@ -0,0 +1,39 @@ +// local includes +#include "settings.h" +#include "src/logging.h" + +namespace display_device { + + settings_t::apply_result_t::operator bool() const { + return result == result_e::success; + } + + std::string + settings_t::apply_result_t::get_error_message() const { + switch (result) { + case result_e::success: + return "Success"; + case result_e::topology_fail: + return "Failed to change or validate the display topology"; + case result_e::primary_display_fail: + return "Failed to change primary display"; + case result_e::modes_fail: + return "Failed to set new display modes (resolution + refresh rate)"; + case result_e::hdr_states_fail: + return "Failed to set new HDR states"; + case result_e::file_save_fail: + return "Failed to save the original settings to persistent file"; + case result_e::revert_fail: + return "Failed to revert back to the original display settings"; + default: + BOOST_LOG(fatal) << "result_e conversion not implemented!"; + return "FATAL"; + } + } + + void + settings_t::set_filepath(std::filesystem::path filepath) { + this->filepath = std::move(filepath); + } + +} // namespace display_device diff --git a/src/display_device/settings.h b/src/display_device/settings.h new file mode 100644 index 00000000000..6f4b0839603 --- /dev/null +++ b/src/display_device/settings.h @@ -0,0 +1,200 @@ +#pragma once + +// standard includes +#include +#include + +// local includes +#include "parsed_config.h" + +namespace display_device { + + /** + * @brief A platform specific class that can apply configuration to the display device and later revert it. + * + * Main goals of this class: + * - Apply the configuration to the display device. + * - Revert the applied configuration to get back to the initial state. + * - Save and load the previous state to/from a file. + */ + class settings_t { + public: + /** + * @brief Platform specific persistent data. + */ + struct persistent_data_t; + + /** + * @brief Platform specific non-persistent audio data in case we need to manipulate + * audio session and keep some temporary data around. + */ + struct audio_data_t; + + /** + * @brief The result value of the apply_config with additional metadata. + * @note Metadata is used when generating an XML status report to the client. + * @see apply_config + */ + struct apply_result_t { + /** + * @brief Possible result values/reasons from apply_config. + * @note There is no deeper meaning behind the values. They simply represent + * the stage where the method has failed to give some hints to the user. + * @note The value of 700 has no special meaning and is just arbitrary. + * @see apply_config + */ + enum class result_e : int { + success, + topology_fail, + primary_display_fail, + modes_fail, + hdr_states_fail, + file_save_fail, + revert_fail + }; + + /** + * @brief Convert the result to boolean equivalent. + * @returns True if result means success, false otherwise. + * + * EXAMPLES: + * ```cpp + * const apply_result_t result { result_e::topology_fail }; + * if (result) { + * // Handle good result + * } + * else { + * // Handle bad result + * } + * ``` + */ + explicit + operator bool() const; + + /** + * @brief Get a string message with better explanation for the result. + * @returns String message for the result. + * + * EXAMPLES: + * ```cpp + * const apply_result_t result { result_e::topology_fail }; + * if (!result) { + * const int error_message = result.get_error_message(); + * } + * ``` + */ + [[nodiscard]] std::string + get_error_message() const; + + result_e result; /**< The result value. */ + }; + + /** + * @brief A platform specific default constructor. + * @note Needed due to forwarding declarations used by the class. + */ + explicit settings_t(); + + /** + * @brief A platform specific destructor. + * @note Needed due to forwarding declarations used by the class. + */ + virtual ~settings_t(); + + /** + * @brief Check whether it is already known that changing settings will fail due to various reasons. + * @returns True if it's definitely known that changing settings will fail, false otherwise. + * + * EXAMPLES: + * ```cpp + * settings_t settings; + * const bool will_fail { settings.is_changing_settings_going_to_fail() }; + * ``` + */ + bool + is_changing_settings_going_to_fail() const; + + /** + * @brief Set the file path for persistent data. + * + * EXAMPLES: + * ```cpp + * settings_t settings; + * settings.set_filepath("/foo/bar.json"); + * ``` + */ + void + set_filepath(std::filesystem::path filepath); + + /** + * @brief Apply the parsed configuration. + * @param config A parsed and validated configuration. + * @returns The apply result value. + * @see apply_result_t + * @see parsed_config_t + * + * EXAMPLES: + * ```cpp + * const parsed_config_t config; + * + * settings_t settings; + * const auto result = settings.apply_config(config); + * ``` + */ + apply_result_t + apply_config(const parsed_config_t &config); + + /** + * @brief Revert the applied configuration and restore the previous settings. + * @note It automatically loads the settings from persistence file if cached settings do not exist. + * @returns True if settings were reverted or there was nothing to revert, false otherwise. + * + * EXAMPLES: + * ```cpp + * const std::shared_ptr launch_session; // Assuming ptr is properly initialized + * const config::video_t &video_config { config::video }; + * + * settings_t settings; + * const auto result = settings.apply_config(video_config, *launch_session); + * if (result) { + * // Wait for some time + * settings.revert_settings(); + * } + * ``` + */ + bool + revert_settings(); + + /** + * @brief Reset the persistence and currently held initial display state. + * @see session_t::reset_persistence for more details. + * + * EXAMPLES: + * ```cpp + * const std::shared_ptr launch_session; // Assuming ptr is properly initialized + * const config::video_t &video_config { config::video }; + * + * settings_t settings; + * const auto result = settings.apply_config(video_config, *launch_session); + * if (result) { + * // Wait for some time + * if (settings.revert_settings()) { + * // Wait for user input + * const bool user_wants_reset { true }; + * if (user_wants_reset) { + * settings.reset_persistence(); + * } + * } + * } + * ``` + */ + void + reset_persistence(); + + private: + std::unique_ptr persistent_data; /**< Platform specific persistent data. */ + std::unique_ptr audio_data; /**< Platform specific temporary audio data. */ + std::filesystem::path filepath; /**< Filepath for persistent file. */ + }; + +} // namespace display_device diff --git a/src/display_device/to_string.cpp b/src/display_device/to_string.cpp new file mode 100644 index 00000000000..82848b74e3b --- /dev/null +++ b/src/display_device/to_string.cpp @@ -0,0 +1,142 @@ +// local includes +#include "to_string.h" +#include "src/logging.h" + +namespace display_device { + + std::string + to_string(device_state_e value) { + switch (value) { + case device_state_e::inactive: + return "INACTIVE"; + case device_state_e::active: + return "ACTIVE"; + case device_state_e::primary: + return "PRIMARY"; + default: + BOOST_LOG(fatal) << "device_state_e conversion not implemented!"; + return {}; + } + } + + std::string + to_string(hdr_state_e value) { + switch (value) { + case hdr_state_e::unknown: + return "UNKNOWN"; + case hdr_state_e::disabled: + return "DISABLED"; + case hdr_state_e::enabled: + return "ENABLED"; + default: + BOOST_LOG(fatal) << "hdr_state_e conversion not implemented!"; + return {}; + } + } + + std::string + to_string(const hdr_state_map_t &value) { + std::stringstream output; + for (const auto &item : value) { + output << std::endl + << item.first << " -> " << to_string(item.second); + } + return output.str(); + } + + std::string + to_string(const device_info_t &value) { + std::stringstream output; + output << "DISPLAY NAME: " << (value.display_name.empty() ? "NOT AVAILABLE" : value.display_name) << std::endl; + output << "FRIENDLY NAME: " << (value.friendly_name.empty() ? "NOT AVAILABLE" : value.friendly_name) << std::endl; + output << "DEVICE STATE: " << to_string(value.device_state) << std::endl; + output << "HDR STATE: " << to_string(value.hdr_state); + return output.str(); + } + + std::string + to_string(const device_info_map_t &value) { + std::stringstream output; + bool output_is_empty { true }; + for (const auto &item : value) { + output << std::endl; + if (!output_is_empty) { + output << "-----------------------" << std::endl; + } + + output << "DEVICE ID: " << item.first << std::endl; + output << to_string(item.second); + output_is_empty = false; + } + return output.str(); + } + + std::string + to_string(const resolution_t &value) { + std::stringstream output; + output << value.width << "x" << value.height; + return output.str(); + } + + std::string + to_string(const refresh_rate_t &value) { + std::stringstream output; + if (value.denominator > 0) { + output << (static_cast(value.numerator) / value.denominator); + } + else { + output << "INF"; + } + return output.str(); + } + + std::string + to_string(const display_mode_t &value) { + std::stringstream output; + output << to_string(value.resolution) << "x" << to_string(value.refresh_rate); + return output.str(); + } + + std::string + to_string(const device_display_mode_map_t &value) { + std::stringstream output; + for (const auto &item : value) { + output << std::endl + << item.first << " -> " << to_string(item.second); + } + return output.str(); + } + + std::string + to_string(const active_topology_t &value) { + std::stringstream output; + bool first_group { true }; + + output << std::endl + << "[" << std::endl; + for (const auto &group : value) { + if (!first_group) { + output << "," << std::endl; + } + first_group = false; + + output << " [" << std::endl; + bool first_group_item { true }; + for (const auto &group_item : group) { + if (!first_group_item) { + output << "," << std::endl; + } + first_group_item = false; + + output << " " << group_item; + } + output << std::endl + << " ]"; + } + output << std::endl + << "]"; + + return output.str(); + } + +} // namespace display_device diff --git a/src/display_device/to_string.h b/src/display_device/to_string.h new file mode 100644 index 00000000000..c24bdd4565a --- /dev/null +++ b/src/display_device/to_string.h @@ -0,0 +1,138 @@ +#pragma once + +// local includes +#include "display_device.h" + +namespace display_device { + + /** + * @brief Stringify a device_state_e value. + * @param value Value to be stringified. + * @return A string representation of device_state_e value. + * + * EXAMPLES: + * ```cpp + * const std::string string_value = to_string(device_state_e { }); + * ``` + */ + std::string + to_string(device_state_e value); + + /** + * @brief Stringify a hdr_state_e value. + * @param value Value to be stringified. + * @return A string representation of hdr_state_e value. + * + * EXAMPLES: + * ```cpp + * const std::string string_value = to_string(hdr_state_e { }); + * ``` + */ + std::string + to_string(hdr_state_e value); + + /** + * @brief Stringify a hdr_state_map_t value. + * @param value Value to be stringified. + * @return A string representation of hdr_state_map_t value. + * + * EXAMPLES: + * ```cpp + * const std::string string_value = to_string(hdr_state_map_t { }); + * ``` + */ + std::string + to_string(const hdr_state_map_t &value); + + /** + * @brief Stringify a device_info_t value. + * @param value Value to be stringified. + * @return A string representation of device_info_t value. + * + * EXAMPLES: + * ```cpp + * const std::string string_value = to_string(device_info_t { }); + * ``` + */ + std::string + to_string(const device_info_t &value); + + /** + * @brief Stringify a device_info_map_t value. + * @param value Value to be stringified. + * @return A string representation of device_info_map_t value. + * + * EXAMPLES: + * ```cpp + * const std::string string_value = to_string(device_info_map_t { }); + * ``` + */ + std::string + to_string(const device_info_map_t &value); + + /** + * @brief Stringify a resolution_t value. + * @param value Value to be stringified. + * @return A string representation of resolution_t value. + * + * EXAMPLES: + * ```cpp + * const std::string string_value = to_string(resolution_t { }); + * ``` + */ + std::string + to_string(const resolution_t &value); + + /** + * @brief Stringify a refresh_rate_t value. + * @param value Value to be stringified. + * @return A string representation of refresh_rate_t value. + * + * EXAMPLES: + * ```cpp + * const std::string string_value = to_string(refresh_rate_t { }); + * ``` + */ + std::string + to_string(const refresh_rate_t &value); + + /** + * @brief Stringify a display_mode_t value. + * @param value Value to be stringified. + * @return A string representation of display_mode_t value. + * + * EXAMPLES: + * ```cpp + * const std::string string_value = to_string(display_mode_t { }); + * ``` + */ + std::string + to_string(const display_mode_t &value); + + /** + * @brief Stringify a device_display_mode_map_t value. + * @param value Value to be stringified. + * @return A string representation of device_display_mode_map_t value. + * + * EXAMPLES: + * ```cpp + * const std::string string_value = to_string(device_display_mode_map_t { }); + * ``` + */ + std::string + to_string(const device_display_mode_map_t &value); + + /** + * @brief Stringify a active_topology_t value. + * @param value Value to be stringified. + * @return A string representation of active_topology_t value. + * + * EXAMPLES: + * ```cpp + * const std::string string_value = to_string(active_topology_t { }); + * ``` + */ + std::string + to_string(const active_topology_t &value); + +} // namespace display_device diff --git a/src/main.cpp b/src/main.cpp index 1a1d5ef24ef..ef8336fa9ee 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -10,6 +10,7 @@ // local includes #include "confighttp.h" +#include "display_device/session.h" #include "entry_handler.h" #include "globals.h" #include "httpcommon.h" @@ -130,6 +131,14 @@ main(int argc, char *argv[]) { return fn->second(argv[0], config::sunshine.cmd.argc, config::sunshine.cmd.argv); } + // Adding this guard here first as it also performs recovery after crash, + // otherwise people could theoretically end up without display output. + // It also should be run be destroyed before forced shutdown. + auto display_device_deinit_guard = display_device::session_t::init(); + 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()) { @@ -227,7 +236,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 = []() { @@ -238,9 +247,10 @@ main(int argc, char *argv[]) { force_shutdown = task_pool.pushDelayed(task, 10s).task_id; shutdown_event->raise(true); + display_device_deinit_guard.reset(); }); - 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 = []() { @@ -251,6 +261,7 @@ main(int argc, char *argv[]) { force_shutdown = task_pool.pushDelayed(task, 10s).task_id; shutdown_event->raise(true); + display_device_deinit_guard.reset(); }); #ifdef _WIN32 diff --git a/src/nvhttp.cpp b/src/nvhttp.cpp index af32391bbf6..4d67421ca24 100644 --- a/src/nvhttp.cpp +++ b/src/nvhttp.cpp @@ -17,11 +17,13 @@ #include #include #include +#include #include // local includes #include "config.h" #include "crypto.h" +#include "display_device/session.h" #include "file_handler.h" #include "globals.h" #include "httpcommon.h" @@ -36,6 +38,8 @@ #include "uuid.h" #include "video.h" +using json = nlohmann::json; + using namespace std::literals; namespace nvhttp { @@ -353,6 +357,29 @@ namespace nvhttp { uint32_t prepend_iv = util::endian::big(util::from_view(get_arg(args, "rikeyid"))); auto prepend_iv_p = (uint8_t *) &prepend_iv; std::copy(prepend_iv_p, prepend_iv_p + sizeof(prepend_iv), std::begin(launch_session->iv)); + + launch_session->env["SUNSHINE_CLIENT_ID"] = std::to_string(launch_session->id); + launch_session->env["SUNSHINE_CLIENT_UNIQUE_ID"] = launch_session->unique_id; + launch_session->env["SUNSHINE_CLIENT_WIDTH"] = std::to_string(launch_session->width); + launch_session->env["SUNSHINE_CLIENT_HEIGHT"] = std::to_string(launch_session->height); + launch_session->env["SUNSHINE_CLIENT_FPS"] = std::to_string(launch_session->fps); + launch_session->env["SUNSHINE_CLIENT_HDR"] = launch_session->enable_hdr ? "true" : "false"; + launch_session->env["SUNSHINE_CLIENT_GCMAP"] = std::to_string(launch_session->gcmap); + launch_session->env["SUNSHINE_CLIENT_HOST_AUDIO"] = launch_session->host_audio ? "true" : "false"; + launch_session->env["SUNSHINE_CLIENT_ENABLE_SOPS"] = launch_session->enable_sops ? "true" : "false"; + int channelCount = launch_session->surround_info & (65535); + switch (channelCount) { + case 2: + launch_session->env["SUNSHINE_CLIENT_AUDIO_CONFIGURATION"] = "2.0"; + break; + case 6: + launch_session->env["SUNSHINE_CLIENT_AUDIO_CONFIGURATION"] = "5.1"; + break; + case 8: + launch_session->env["SUNSHINE_CLIENT_AUDIO_CONFIGURATION"] = "7.1"; + break; + } + return launch_session; } @@ -810,6 +837,20 @@ namespace nvhttp { app.put("AppTitle"s, proc.name); app.put("ID", proc.id); + json json_cmds; + + for (auto &cmd : proc.menu_cmds) { + json json_cmd; + json_cmd["id"] = cmd.id; + json_cmd["name"] = cmd.name; + json_cmd["do_cmd"] = cmd.do_cmd; + json_cmd["elevated"] = cmd.elevated; + + json_cmds.push_back(json_cmd); + } + + app.put("SuperCmds"s, json_cmds.dump(4)); + apps.push_back(std::make_pair("App", std::move(app))); } } @@ -819,12 +860,17 @@ namespace nvhttp { print_req(request); pt::ptree tree; + bool need_to_restore_display_state { 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 (need_to_restore_display_state) { + display_device::session_t::get().restore_state(); + } }); if (rtsp_stream::session_count() == config::stream.channels) { @@ -859,11 +905,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")); + const auto launch_session = make_launch_session(host_audio, args); + if (rtsp_stream::session_count() == 0) { + // We want to prepare display only if there are no active sessions at + // the moment. This should to be done before probing encoders as it could + // change display device's state. + display_device::session_t::get().configure_display(config::video, *launch_session); + + // The display should be restored by the fail guard in case something happens. + need_to_restore_display_state = true; + + // 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?"); @@ -873,9 +930,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; @@ -905,6 +959,9 @@ namespace nvhttp { tree.put("root.gamesession", 1); rtsp_stream::launch_session_raise(launch_session); + + // Stream was started successfully, we will restore the state when the app or session terminates + need_to_restore_display_state = false; } void @@ -950,7 +1007,20 @@ 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 (rtsp_stream::session_count() == 0 && 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 (rtsp_stream::session_count() == 0) { + // We want to prepare display only if there are no active sessions at + // the moment. This should to be done before probing encoders as it could + // change display device's state. + display_device::session_t::get().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, @@ -962,17 +1032,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; @@ -1022,6 +1083,57 @@ namespace nvhttp { if (proc::proc.running() > 0) { proc::proc.terminate(); } + + // The state needs to be restored regardless of whether "proc::proc.terminate()" was called or not. + display_device::session_t::get().restore_state(); + } + + void + sleep(resp_https_t response, req_https_t request) { + print_req(request); + + boost::process::environment _env = boost::this_process::environment(); + auto working_dir = boost::filesystem::path(); + std::error_code ec; + std::string cmd = "rundll32.exe powrprof.dll,SetSuspendState 0,1,0"; + + auto child = platf::run_command(false, true, cmd, working_dir, _env, nullptr, ec, nullptr); + if (ec) { + BOOST_LOG(warning) << "Couldn't run cmd ["sv << cmd << "]: System: "sv << ec.message(); + } + else { + BOOST_LOG(info) << "Executing sleep cmd ["sv << cmd << "]"sv; + child.detach(); + } + + pt::ptree tree; + tree.put("root.pcsleep", 1); + tree.put("root..status_code", 200); + + std::ostringstream data; + + pt::write_xml(data, tree); + response->write(data.str()); + response->close_connection_after_response = true; + } + + void + execSuperCmd(resp_https_t response, req_https_t request) { + print_req(request); + + auto args = request->parse_query_string(); + auto cmdId = get_arg(args, "cmdId", ""); + proc::proc.run_menu_cmd(cmdId); + + pt::ptree tree; + tree.put("root.supercmd", 1); + tree.put("root..status_code", 200); + + std::ostringstream data; + + pt::write_xml(data, tree); + response->write(data.str()); + response->close_connection_after_response = true; } void @@ -1127,6 +1239,8 @@ namespace nvhttp { 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.resource["^/pcsleep$"]["GET"] = sleep; + https_server.resource["^/supercmd$"]["GET"] = execSuperCmd; https_server.config.reuse_address = true; https_server.config.address = net::af_to_any_address_string(address_family); diff --git a/src/platform/linux/display_device.cpp b/src/platform/linux/display_device.cpp new file mode 100644 index 00000000000..23e7aa923ba --- /dev/null +++ b/src/platform/linux/display_device.cpp @@ -0,0 +1,117 @@ +// local includes +#include "src/display_device/settings.h" + +namespace display_device { + + device_info_map_t + enum_available_devices() { + // Not implemented + return {}; + } + + std::string + get_display_name(const std::string &value) { + // Not implemented, but just passthrough the value + return value; + } + + device_display_mode_map_t + get_current_display_modes(const std::unordered_set &) { + // Not implemented + return {}; + } + + bool + set_display_modes(const device_display_mode_map_t &) { + // Not implemented + return false; + } + + bool + is_primary_device(const std::string &) { + // Not implemented + return false; + } + + bool + set_as_primary_device(const std::string &) { + // Not implemented + return false; + } + + hdr_state_map_t + get_current_hdr_states(const std::unordered_set &) { + // Not implemented + return {}; + } + + bool + set_hdr_states(const hdr_state_map_t &) { + // Not implemented + return false; + } + + active_topology_t + get_current_topology() { + // Not implemented + return {}; + } + + bool + is_topology_valid(const active_topology_t &topology) { + // Not implemented + return false; + } + + bool + is_topology_the_same(const active_topology_t &a, const active_topology_t &b) { + // Not implemented + return false; + } + + bool + set_topology(const active_topology_t &) { + // Not implemented + return false; + } + + struct settings_t::audio_data_t { + // Not implemented + }; + + struct settings_t::persistent_data_t { + // Not implemented + }; + + settings_t::settings_t() { + // Not implemented + } + + settings_t::~settings_t() { + // Not implemented + } + + bool + settings_t::is_changing_settings_going_to_fail() const { + // Not implemented + return false; + } + + settings_t::apply_result_t + settings_t::apply_config(const parsed_config_t &) { + // Not implemented + return { apply_result_t::result_e::success }; + } + + bool + settings_t::revert_settings() { + // Not implemented + return true; + } + + void + settings_t::reset_persistence() { + // Not implemented + } + +} // namespace display_device diff --git a/src/platform/macos/display_device.cpp b/src/platform/macos/display_device.cpp new file mode 100644 index 00000000000..23e7aa923ba --- /dev/null +++ b/src/platform/macos/display_device.cpp @@ -0,0 +1,117 @@ +// local includes +#include "src/display_device/settings.h" + +namespace display_device { + + device_info_map_t + enum_available_devices() { + // Not implemented + return {}; + } + + std::string + get_display_name(const std::string &value) { + // Not implemented, but just passthrough the value + return value; + } + + device_display_mode_map_t + get_current_display_modes(const std::unordered_set &) { + // Not implemented + return {}; + } + + bool + set_display_modes(const device_display_mode_map_t &) { + // Not implemented + return false; + } + + bool + is_primary_device(const std::string &) { + // Not implemented + return false; + } + + bool + set_as_primary_device(const std::string &) { + // Not implemented + return false; + } + + hdr_state_map_t + get_current_hdr_states(const std::unordered_set &) { + // Not implemented + return {}; + } + + bool + set_hdr_states(const hdr_state_map_t &) { + // Not implemented + return false; + } + + active_topology_t + get_current_topology() { + // Not implemented + return {}; + } + + bool + is_topology_valid(const active_topology_t &topology) { + // Not implemented + return false; + } + + bool + is_topology_the_same(const active_topology_t &a, const active_topology_t &b) { + // Not implemented + return false; + } + + bool + set_topology(const active_topology_t &) { + // Not implemented + return false; + } + + struct settings_t::audio_data_t { + // Not implemented + }; + + struct settings_t::persistent_data_t { + // Not implemented + }; + + settings_t::settings_t() { + // Not implemented + } + + settings_t::~settings_t() { + // Not implemented + } + + bool + settings_t::is_changing_settings_going_to_fail() const { + // Not implemented + return false; + } + + settings_t::apply_result_t + settings_t::apply_config(const parsed_config_t &) { + // Not implemented + return { apply_result_t::result_e::success }; + } + + bool + settings_t::revert_settings() { + // Not implemented + return true; + } + + void + settings_t::reset_persistence() { + // Not implemented + } + +} // namespace display_device diff --git a/src/platform/windows/display_base.cpp b/src/platform/windows/display_base.cpp index 7d34529f27c..86e44e7aaee 100644 --- a/src/platform/windows/display_base.cpp +++ b/src/platform/windows/display_base.cpp @@ -15,6 +15,7 @@ typedef long NTSTATUS; #include "display.h" #include "misc.h" #include "src/config.h" +#include "src/display_device/display_device.h" #include "src/logging.h" #include "src/platform/common.h" #include "src/video.h" @@ -1079,7 +1080,8 @@ namespace platf { 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)) { + const auto output_display_name { display_device::get_display_name(config::video.output_name) }; + if (!dxgi::probe_for_gpu_preference(output_display_name)) { BOOST_LOG(warning) << "Failed to set GPU preference. Capture may not work!"sv; } diff --git a/src/platform/windows/display_device/device_hdr_states.cpp b/src/platform/windows/display_device/device_hdr_states.cpp new file mode 100644 index 00000000000..bebc626b520 --- /dev/null +++ b/src/platform/windows/display_device/device_hdr_states.cpp @@ -0,0 +1,108 @@ +// local includes +#include "src/display_device/to_string.h" +#include "src/logging.h" +#include "windows_utils.h" + +namespace display_device { + + namespace { + + /** + * @see set_hdr_states for a description as this was split off to reduce cognitive complexity. + */ + bool + do_set_states(const hdr_state_map_t &states) { + const auto display_data { w_utils::query_display_config(w_utils::ACTIVE_ONLY_DEVICES) }; + if (!display_data) { + // Error already logged + return false; + } + + for (const auto &[device_id, state] : states) { + const auto path { w_utils::get_active_path(device_id, display_data->paths) }; + if (!path) { + BOOST_LOG(error) << "Failed to find device for " << device_id << "!"; + return false; + } + + if (state == hdr_state_e::unknown) { + // We cannot change state to unknown, so we are just ignoring these entries + // for convenience. + continue; + } + + const auto current_state { w_utils::get_hdr_state(*path) }; + if (current_state == hdr_state_e::unknown) { + BOOST_LOG(error) << "HDR state cannot be changed for " << device_id << "!"; + return false; + } + + if (!w_utils::set_hdr_state(*path, state == hdr_state_e::enabled)) { + // Error already logged + return false; + } + } + + return true; + }; + + } // namespace + + hdr_state_map_t + get_current_hdr_states(const std::unordered_set &device_ids) { + if (device_ids.empty()) { + BOOST_LOG(error) << "Device id set is empty!"; + return {}; + } + + const auto display_data { w_utils::query_display_config(w_utils::ACTIVE_ONLY_DEVICES) }; + if (!display_data) { + // Error already logged + return {}; + } + + hdr_state_map_t states; + for (const auto &device_id : device_ids) { + const auto path { w_utils::get_active_path(device_id, display_data->paths) }; + if (!path) { + BOOST_LOG(error) << "Failed to find device for " << device_id << "!"; + return {}; + } + + states[device_id] = w_utils::get_hdr_state(*path); + } + + return states; + } + + bool + set_hdr_states(const hdr_state_map_t &states) { + if (states.empty()) { + BOOST_LOG(error) << "States map is empty!"; + return false; + } + + std::unordered_set device_ids; + for (const auto &[device_id, _] : states) { + if (!device_ids.insert(device_id).second) { + // Sanity check since, it's technically not possible with unordered map to have duplicate keys + BOOST_LOG(error) << "Duplicate device id provided: " << device_id << "!"; + return false; + } + } + + const auto original_states { get_current_hdr_states(device_ids) }; + if (original_states.empty()) { + // Error already logged + return false; + } + + if (!do_set_states(states)) { + do_set_states(original_states); // return value does not matter + return false; + } + + return true; + } + +} // namespace display_device diff --git a/src/platform/windows/display_device/device_modes.cpp b/src/platform/windows/display_device/device_modes.cpp new file mode 100644 index 00000000000..718f8bec7e4 --- /dev/null +++ b/src/platform/windows/display_device/device_modes.cpp @@ -0,0 +1,344 @@ +// local includes +#include "src/logging.h" +#include "windows_utils.h" + +namespace display_device { + + namespace { + + /** + * @brief Check if the refresh rates are almost equal. + * @param r1 First refresh rate. + * @param r2 Second refresh rate. + * @return True if refresh rates are almost equal, false otherwise. + * + * EXAMPLES: + * ```cpp + * const bool almost_equal = fuzzy_compare_refresh_rates(refresh_rate_t { 60, 1 }, refresh_rate_t { 5985, 100 }); + * const bool not_equal = fuzzy_compare_refresh_rates(refresh_rate_t { 60, 1 }, refresh_rate_t { 5585, 100 }); + * ``` + */ + bool + fuzzy_compare_refresh_rates(const refresh_rate_t &r1, const refresh_rate_t &r2) { + if (r1.denominator > 0 && r2.denominator > 0) { + const float r1_f { static_cast(r1.numerator) / static_cast(r1.denominator) }; + const float r2_f { static_cast(r2.numerator) / static_cast(r2.denominator) }; + return (std::abs(r1_f - r2_f) <= 1.f); + } + + return false; + } + + /** + * @brief Check if the display modes are almost equal. + * @param mode_a First mode. + * @param mode_b Second mode. + * @return True if display modes are almost equal, false otherwise. + * + * EXAMPLES: + * ```cpp + * const bool almost_equal = fuzzy_compare_refresh_rates(display_mode_t { { 1920, 1080 }, { 60, 1 } }, + * display_mode_t { { 1920, 1080 }, { 5985, 100 } }); + * const bool not_equal = fuzzy_compare_refresh_rates(display_mode_t { { 1920, 1080 }, { 60, 1 } }, + * display_mode_t { { 1920, 1080 }, { 5585, 100 } }); + * ``` + */ + bool + fuzzy_compare_modes(const display_mode_t &mode_a, const display_mode_t &mode_b) { + return mode_a.resolution.width == mode_b.resolution.width && + mode_a.resolution.height == mode_b.resolution.height && + fuzzy_compare_refresh_rates(mode_a.refresh_rate, mode_b.refresh_rate); + } + + /** + * @brief Get all the missing duplicate device ids for the provided device ids. + * @param device_ids Device ids to find the missing duplicate ids for. + * @returns A list of device ids containing the provided device ids and all unspecified ids + * for duplicated displays. + * + * EXAMPLES: + * ```cpp + * const auto device_ids_with_duplicates = get_all_duplicated_devices({ "MY_ID1" }); + * ``` + */ + std::unordered_set + get_all_duplicated_devices(const std::unordered_set &device_ids) { + const auto display_data { w_utils::query_display_config(w_utils::ACTIVE_ONLY_DEVICES) }; + if (!display_data) { + // Error already logged + return {}; + } + + std::unordered_set all_device_ids; + for (const auto &device_id : device_ids) { + if (device_id.empty()) { + BOOST_LOG(error) << "Device it is empty!"; + return {}; + } + + const auto provided_path { w_utils::get_active_path(device_id, display_data->paths) }; + if (!provided_path) { + BOOST_LOG(warning) << "Failed to find device for " << device_id << "!"; + return {}; + } + + const auto provided_path_source_mode { w_utils::get_source_mode(w_utils::get_source_index(*provided_path, display_data->modes), display_data->modes) }; + if (!provided_path_source_mode) { + BOOST_LOG(error) << "Active device does not have a source mode: " << device_id << "!"; + return {}; + } + + // We will now iterate over all the active paths (provided path included) and check if + // any of them are duplicated. + for (const auto &path : display_data->paths) { + const auto device_info { w_utils::get_device_info_for_valid_path(path, w_utils::ACTIVE_ONLY_DEVICES) }; + if (!device_info) { + continue; + } + + if (all_device_ids.count(device_info->device_id) > 0) { + // Already checked + continue; + } + + const auto source_mode { w_utils::get_source_mode(w_utils::get_source_index(path, display_data->modes), display_data->modes) }; + if (!source_mode) { + BOOST_LOG(error) << "Active device does not have a source mode: " << device_info->device_id << "!"; + return {}; + } + + if (!w_utils::are_modes_duplicated(*provided_path_source_mode, *source_mode)) { + continue; + } + + all_device_ids.insert(device_info->device_id); + } + } + + return all_device_ids; + } + + /** + * @see set_display_modes for a description as this was split off to reduce cognitive complexity. + */ + bool + do_set_modes(const device_display_mode_map_t &modes, bool allow_changes) { + auto display_data { w_utils::query_display_config(w_utils::ACTIVE_ONLY_DEVICES) }; + if (!display_data) { + // Error already logged + return false; + } + + bool changes_applied { false }; + for (const auto &[device_id, mode] : modes) { + const auto path { w_utils::get_active_path(device_id, display_data->paths) }; + if (!path) { + BOOST_LOG(error) << "Failed to find device for " << device_id << "!"; + return false; + } + + const auto source_mode { w_utils::get_source_mode(w_utils::get_source_index(*path, display_data->modes), display_data->modes) }; + if (!source_mode) { + BOOST_LOG(error) << "Active device does not have a source mode: " << device_id << "!"; + return false; + } + + bool new_changes { false }; + const bool resolution_changed { source_mode->width != mode.resolution.width || source_mode->height != mode.resolution.height }; + + bool refresh_rate_changed { false }; + if (allow_changes) { + refresh_rate_changed = !fuzzy_compare_refresh_rates(refresh_rate_t { path->targetInfo.refreshRate.Numerator, path->targetInfo.refreshRate.Denominator }, mode.refresh_rate); + } + else { + // Since we are in strict mode, do not fuzzy compare it + refresh_rate_changed = path->targetInfo.refreshRate.Numerator != mode.refresh_rate.numerator || + path->targetInfo.refreshRate.Denominator != mode.refresh_rate.denominator; + } + + if (resolution_changed) { + source_mode->width = mode.resolution.width; + source_mode->height = mode.resolution.height; + new_changes = true; + } + + if (refresh_rate_changed) { + path->targetInfo.refreshRate = { mode.refresh_rate.numerator, mode.refresh_rate.denominator }; + new_changes = true; + } + + if (new_changes) { + // Clear the target index so that Windows has to select/modify the target to best match the requirements. + w_utils::set_target_index(*path, boost::none); + w_utils::set_desktop_index(*path, boost::none); // Part of struct containing target index and so it needs to be cleared + } + + changes_applied = changes_applied || new_changes; + } + + if (!changes_applied) { + BOOST_LOG(debug) << "No changes were made to display modes as they are equal."; + return true; + } + + UINT32 flags { SDC_APPLY | SDC_USE_SUPPLIED_DISPLAY_CONFIG | SDC_SAVE_TO_DATABASE | SDC_VIRTUAL_MODE_AWARE }; + if (allow_changes) { + // It's probably best for Windows to select the "best" display settings for us. However, in case we + // have custom resolution set in nvidia control panel for example, this flag will prevent successfully applying + // settings to it. + flags |= SDC_ALLOW_CHANGES; + } + + const LONG result { SetDisplayConfig(display_data->paths.size(), display_data->paths.data(), display_data->modes.size(), display_data->modes.data(), flags) }; + if (result != ERROR_SUCCESS) { + BOOST_LOG(error) << w_utils::get_error_string(result) << " failed to set display mode!"; + return false; + } + + return true; + }; + + } // namespace + + device_display_mode_map_t + get_current_display_modes(const std::unordered_set &device_ids) { + if (device_ids.empty()) { + BOOST_LOG(error) << "Device id set is empty!"; + return {}; + } + + const auto display_data { w_utils::query_display_config(w_utils::ACTIVE_ONLY_DEVICES) }; + if (!display_data) { + // Error already logged + return {}; + } + + device_display_mode_map_t current_modes; + for (const auto &device_id : device_ids) { + if (device_id.empty()) { + BOOST_LOG(error) << "Device id is empty!"; + return {}; + } + + const auto path { w_utils::get_active_path(device_id, display_data->paths) }; + if (!path) { + BOOST_LOG(error) << "Failed to find device for " << device_id << "!"; + return {}; + } + + const auto source_mode { w_utils::get_source_mode(w_utils::get_source_index(*path, display_data->modes), display_data->modes) }; + if (!source_mode) { + BOOST_LOG(error) << "Active device does not have a source mode: " << device_id << "!"; + return {}; + } + + // For whatever reason they put refresh rate into path, but not the resolution. + const auto target_refresh_rate { path->targetInfo.refreshRate }; + current_modes[device_id] = display_mode_t { + { source_mode->width, source_mode->height }, + { target_refresh_rate.Numerator, target_refresh_rate.Denominator } + }; + } + + return current_modes; + } + + bool + set_display_modes(const device_display_mode_map_t &modes) { + if (modes.empty()) { + BOOST_LOG(error) << "Modes map is empty!"; + return false; + } + + std::unordered_set device_ids; + for (const auto &[device_id, _] : modes) { + if (!device_ids.insert(device_id).second) { + // Sanity check since, it's technically not possible with unordered map to have duplicate keys + BOOST_LOG(error) << "Duplicate device id provided: " << device_id << "!"; + return false; + } + } + + // Here it is important to check that we have all the necessary modes, otherwise + // setting modes will fail with ambiguous message. + // + // Duplicated devices can have different target modes (monitor) with different refresh rate, + // however this does not apply to the source mode (frame buffer?) and they must have same + // resolution. + // + // Without SDC_VIRTUAL_MODE_AWARE, devices would share the same source mode entry, but now + // they have separate entries that are more or less identical. + // + // To avoid surprising end-user with unexpected source mode change, we validate that all duplicate + // devices were provided instead of guessing modes automatically. This also resolve the problem of + // having to choose refresh rate for duplicate display - leave it to the end-user of this function... + const auto all_device_ids { get_all_duplicated_devices(device_ids) }; + if (all_device_ids.empty()) { + BOOST_LOG(error) << "Failed to get all duplicated devices!"; + return false; + } + + if (all_device_ids.size() != device_ids.size()) { + BOOST_LOG(error) << "Not all modes for duplicate displays were provided!"; + return false; + } + + const auto original_modes { get_current_display_modes(device_ids) }; + if (original_modes.empty()) { + // Error already logged + return false; + } + + constexpr bool allow_changes { true }; + if (!do_set_modes(modes, allow_changes)) { + // Error already logged + return false; + } + + const auto all_modes_match = [&modes](const device_display_mode_map_t ¤t_modes) { + for (const auto &[device_id, requested_mode] : modes) { + auto mode_it { current_modes.find(device_id) }; + if (mode_it == std::end(current_modes)) { + // This race condition of disconnecting display device is technically possible... + return false; + } + + if (!fuzzy_compare_modes(mode_it->second, requested_mode)) { + return false; + } + } + + return true; + }; + + auto current_modes { get_current_display_modes(device_ids) }; + if (!current_modes.empty()) { + if (all_modes_match(current_modes)) { + return true; + } + + // We have a problem when using SetDisplayConfig with SDC_ALLOW_CHANGES + // where it decides to use our new mode merely as a suggestion. + // + // This is good, since we don't have to be very precise with refresh rate, + // but also bad since it can just ignore our specified mode. + // + // However, it is possible that the user has created a custom display mode + // which is not exposed to the via Windows settings app. To allow this + // resolution to be selected, we actually need to omit SDC_ALLOW_CHANGES + // flag. + BOOST_LOG(info) << "Failed to change display modes using Windows recommended modes, trying to set modes more strictly!"; + if (do_set_modes(modes, !allow_changes)) { + current_modes = get_current_display_modes(device_ids); + if (!current_modes.empty() && all_modes_match(current_modes)) { + return true; + } + } + } + + do_set_modes(original_modes, allow_changes); // Return value does not matter as we are trying out best to undo + BOOST_LOG(error) << "Failed to set display mode(-s) completely!"; + return false; + } + +} // namespace display_device diff --git a/src/platform/windows/display_device/device_topology.cpp b/src/platform/windows/display_device/device_topology.cpp new file mode 100644 index 00000000000..e01e311537f --- /dev/null +++ b/src/platform/windows/display_device/device_topology.cpp @@ -0,0 +1,482 @@ +// lib includes +#include + +// local includes +#include "src/logging.h" +#include "windows_utils.h" + +namespace display_device { + + namespace { + + /** + * @brief Contains arbitrary data collected from queried display paths. + */ + struct path_data_t { + std::unordered_map source_id_to_path_index; /**< Maps source ids to its index in the path list. */ + LUID source_adapter_id {}; /**< Adapter id shared by all source ids. */ + boost::optional active_source; /**< Currently active source id. */ + }; + + /** + * @brief Ordered map of [DEVICE_ID -> path_data_t]. + * @see path_data_t + */ + using path_data_map_t = std::map; + + /** + * @brief Check if adapter ids are equal. + * @param id_a First id to check. + * @param id_b Second id to check. + * @return True if equal, false otherwise. + * + * EXAMPLES: + * ```cpp + * const bool equal = compareAdapterIds({ 12, 34 }, { 12, 34 }); + * const bool not_equal = compareAdapterIds({ 12, 34 }, { 12, 56 }); + * ``` + */ + bool + compareAdapterIds(const LUID &id_a, const LUID &id_b) { + return id_a.HighPart == id_b.HighPart && id_a.LowPart == id_b.LowPart; + } + + /** + * @brief Stringify adapter id. + * @param id Id to stringify. + * @return String representation of the id. + * + * EXAMPLES: + * ```cpp + * const bool id_string = to_string({ 12, 34 }); + * ``` + */ + std::string + to_string(const LUID &id) { + return std::to_string(id.HighPart) + std::to_string(id.LowPart); + } + + /** + * @brief Collect arbitrary data from provided paths. + * + * This function filters paths that can be used later on and + * collects some arbitrary data for a quick lookup. + * + * @param paths List of paths. + * @returns Data for valid paths. + * @see query_display_config on how to get paths from the system. + * @see make_new_paths_for_topology for the actual data use example. + * + * EXAMPLES: + * ```cpp + * std::vector paths; + * const auto path_data = make_device_path_data(paths); + * ``` + */ + path_data_map_t + make_device_path_data(const std::vector &paths) { + path_data_map_t path_data; + std::unordered_map paths_to_ids; + for (std::size_t index = 0; index < paths.size(); ++index) { + const auto &path { paths[index] }; + + const auto device_info { w_utils::get_device_info_for_valid_path(path, w_utils::ALL_DEVICES) }; + if (!device_info) { + // Path is not valid + continue; + } + + const auto prev_device_id_for_path_it { paths_to_ids.find(device_info->device_path) }; + if (prev_device_id_for_path_it != std::end(paths_to_ids)) { + if (prev_device_id_for_path_it->second != device_info->device_id) { + BOOST_LOG(error) << "Duplicate display device id found: " << device_info->device_id << " (device path: " << device_info->device_path << ")"; + return {}; + } + } + else { + BOOST_LOG(verbose) << "New valid device id entry for device " << device_info->device_id << " (device path: " << device_info->device_path << ")"; + paths_to_ids[device_info->device_path] = device_info->device_id; + } + + auto path_data_it { path_data.find(device_info->device_id) }; + if (path_data_it != std::end(path_data)) { + if (!compareAdapterIds(path_data_it->second.source_adapter_id, path.sourceInfo.adapterId)) { + // Sanity check, should not be possible since adapter in embedded in the device path + BOOST_LOG(error) << "Device path " << device_info->device_path << " has different adapters!"; + return {}; + } + + path_data_it->second.source_id_to_path_index[path.sourceInfo.id] = index; + } + else { + path_data[device_info->device_id] = path_data_t { + { { path.sourceInfo.id, index } }, + path.sourceInfo.adapterId, + // Since active paths are always in the front, this is the only time we check (when we add new entry) + w_utils::is_active(path) ? boost::make_optional(path.sourceInfo.id) : boost::none + }; + } + } + + return path_data; + } + + /** + * @brief Select the best possible paths to be used for the requested topology based on the data that is available to us. + * + * If the paths will be used for a completely new topology (Windows has never had it set), we need to take into + * account the source id availability per the adapter - duplicated displays must share the same source id + * (if they belong to the same adapter) and have different ids if they are not duplicated displays. + * + * There are limited amount of available ids (see comments in the code) so we will abort early if we are + * out of ids. + * + * The paths for a topology that already exists (Windows has set it at least once) does not have to follow + * the mentioned "source id" rule. Windows will simply ignore them (since we will ask it to later) and select + * paths that were previously configured (that might differ in source ids) based on the paths that we provide. + * + * @param new_topology Topology that we want to have in the end. + * @param path_data Collected arbitrary path data. + * @param paths Display paths. + * @return A list of path that will make up new topology, or an empty list if function fails. + */ + std::vector + make_new_paths_for_topology(const active_topology_t &new_topology, const path_data_map_t &path_data, const std::vector &paths) { + std::vector new_paths; + + UINT32 group_id { 0 }; + std::unordered_map> used_source_ids_per_adapter; + const auto is_source_id_already_used = [&used_source_ids_per_adapter](const LUID &adapter_id, UINT32 source_id) { + auto entry_it { used_source_ids_per_adapter.find(to_string(adapter_id)) }; + if (entry_it != std::end(used_source_ids_per_adapter)) { + return entry_it->second.count(source_id) > 0; + } + + return false; + }; + + for (const auto &group : new_topology) { + std::unordered_map used_source_ids_per_adapter_per_group; + const auto get_already_used_source_id_in_group = [&used_source_ids_per_adapter_per_group](const LUID &adapter_id) -> boost::optional { + auto entry_it { used_source_ids_per_adapter_per_group.find(to_string(adapter_id)) }; + if (entry_it != std::end(used_source_ids_per_adapter_per_group)) { + return entry_it->second; + } + + return boost::none; + }; + + for (const std::string &device_id : group) { + auto path_data_it { path_data.find(device_id) }; + if (path_data_it == std::end(path_data)) { + BOOST_LOG(error) << "Device " << device_id << " does not exist in the available topology data!"; + return {}; + } + + std::size_t selected_path_index {}; + const auto &device_data { path_data_it->second }; + + const auto already_used_source_id { get_already_used_source_id_in_group(device_data.source_adapter_id) }; + if (already_used_source_id) { + // Some device in the group is already using the source id, and we belong to the same adapter. + // This means we must also use the path with matching source id. + auto path_source_it { device_data.source_id_to_path_index.find(*already_used_source_id) }; + if (path_source_it == std::end(device_data.source_id_to_path_index)) { + BOOST_LOG(error) << "Device " << device_id << " does not have a path with a source id " << *already_used_source_id << "!"; + return {}; + } + + selected_path_index = path_source_it->second; + } + else { + // Here we want to select a path index that has the lowest index (the "best" of paths), but only + // if the source id is still free. Technically we don't need to find the lowest index, but that's + // what will match the Windows' behaviour the closest if we need to create new topology in the end. + boost::optional path_index_candidate; + UINT32 used_source_id {}; + for (const auto [source_id, index] : device_data.source_id_to_path_index) { + if (is_source_id_already_used(device_data.source_adapter_id, source_id)) { + continue; + } + + if (!path_index_candidate || index < *path_index_candidate) { + path_index_candidate = index; + used_source_id = source_id; + } + } + + if (!path_index_candidate) { + // Apparently nvidia GPU can only render 4 different sources at a time (according to Google). + // However, it seems to be true only for physical connections as we also have virtual displays. + // + // Virtual displays have different adapter ids than the physical connection ones, but GPU still + // has to render them, so I don't know how this 4 source limitation makes sense then? + // + // In short, this arbitrary limitation should not affect virtual displays when the GPU is at its limit. + BOOST_LOG(error) << "Device " << device_id << " cannot be enabled as the adapter has no more free source id (GPU limitation)!"; + return {}; + } + + selected_path_index = *path_index_candidate; + used_source_ids_per_adapter[to_string(device_data.source_adapter_id)].insert(used_source_id); + used_source_ids_per_adapter_per_group[to_string(device_data.source_adapter_id)] = used_source_id; + } + + auto selected_path { paths.at(selected_path_index) }; + + // All the indexes must be cleared and only the group id specified + w_utils::set_source_index(selected_path, boost::none); + w_utils::set_target_index(selected_path, boost::none); + w_utils::set_desktop_index(selected_path, boost::none); + w_utils::set_clone_group_id(selected_path, group_id); + w_utils::set_active(selected_path); // We also need to mark it as active... + + new_paths.push_back(selected_path); + } + + group_id++; + } + + return new_paths; + } + + /** + * @see set_topology for a description as this was split off to reduce cognitive complexity. + */ + bool + do_set_topology(const active_topology_t &new_topology) { + auto display_data { w_utils::query_display_config(w_utils::ALL_DEVICES) }; + if (!display_data) { + // Error already logged + return false; + } + + const auto path_data { make_device_path_data(display_data->paths) }; + if (path_data.empty()) { + // Error already logged + return false; + } + + auto paths { make_new_paths_for_topology(new_topology, path_data, display_data->paths) }; + if (paths.empty()) { + // Error already logged + return false; + } + + UINT32 flags { SDC_APPLY | SDC_TOPOLOGY_SUPPLIED | SDC_ALLOW_PATH_ORDER_CHANGES | SDC_VIRTUAL_MODE_AWARE }; + LONG result { SetDisplayConfig(paths.size(), paths.data(), 0, nullptr, flags) }; + if (result == ERROR_GEN_FAILURE) { + BOOST_LOG(warning) << w_utils::get_error_string(result) << " failed to change topology using the topology from Windows DB! Asking Windows to create the topology."; + + flags = SDC_APPLY | SDC_USE_SUPPLIED_DISPLAY_CONFIG | SDC_ALLOW_CHANGES /* This flag is probably not needed, but who knows really... (not MSDOCS at least) */ | SDC_VIRTUAL_MODE_AWARE | SDC_SAVE_TO_DATABASE; + result = SetDisplayConfig(paths.size(), paths.data(), 0, nullptr, flags); + if (result != ERROR_SUCCESS) { + BOOST_LOG(error) << w_utils::get_error_string(result) << " failed to create new topology configuration!"; + return false; + } + } + else if (result != ERROR_SUCCESS) { + BOOST_LOG(error) << w_utils::get_error_string(result) << " failed to change topology configuration!"; + return false; + } + + return true; + } + + } // namespace + + device_info_map_t + enum_available_devices() { + auto display_data { w_utils::query_display_config(w_utils::ALL_DEVICES) }; + if (!display_data) { + // Error already logged + return {}; + } + + device_info_map_t available_devices; + const auto topology_data { make_device_path_data(display_data->paths) }; + if (topology_data.empty()) { + // Error already logged + return {}; + } + + for (const auto &[device_id, data] : topology_data) { + const auto &path { display_data->paths.at(data.source_id_to_path_index.at(data.active_source.get_value_or(0))) }; + + if (w_utils::is_active(path)) { + const auto mode { w_utils::get_source_mode(w_utils::get_source_index(path, display_data->modes), display_data->modes) }; + + available_devices[device_id] = device_info_t { + w_utils::get_display_name(path), + w_utils::get_friendly_name(path), + mode && w_utils::is_primary(*mode) ? device_state_e::primary : device_state_e::active, + w_utils::get_hdr_state(path) + }; + } + else { + available_devices[device_id] = device_info_t { + std::string {}, // Inactive devices can have multiple display names, so it's just meaningless use any + w_utils::get_friendly_name(path), + device_state_e::inactive, + hdr_state_e::unknown + }; + } + } + + return available_devices; + } + + active_topology_t + get_current_topology() { + const auto display_data { w_utils::query_display_config(w_utils::ACTIVE_ONLY_DEVICES) }; + if (!display_data) { + // Error already logged + return {}; + } + + // Duplicate displays can be identified by having the same x/y position. Here we have a + // "position to index" map for a simple and lazy lookup in case we have to add a device to the + // topology group. + std::unordered_map position_to_topology_index; + active_topology_t topology; + for (const auto &path : display_data->paths) { + const auto device_info { w_utils::get_device_info_for_valid_path(path, w_utils::ACTIVE_ONLY_DEVICES) }; + if (!device_info) { + continue; + } + + const auto source_mode { w_utils::get_source_mode(w_utils::get_source_index(path, display_data->modes), display_data->modes) }; + if (!source_mode) { + BOOST_LOG(error) << "Active device does not have a source mode: " << device_info->device_id << "!"; + return {}; + } + + const std::string lazy_lookup { std::to_string(source_mode->position.x) + std::to_string(source_mode->position.y) }; + auto index_it { position_to_topology_index.find(lazy_lookup) }; + + if (index_it == std::end(position_to_topology_index)) { + position_to_topology_index[lazy_lookup] = topology.size(); + topology.push_back({ device_info->device_id }); + } + else { + topology.at(index_it->second).push_back(device_info->device_id); + } + } + + return topology; + } + + bool + is_topology_valid(const active_topology_t &topology) { + if (topology.empty()) { + BOOST_LOG(warning) << "Topology input is empty!"; + return false; + } + + std::unordered_set device_ids; + for (const auto &group : topology) { + // Size 2 is a Windows' limitation. + // You CAN set the group to be more than 2, but then + // Windows' settings app breaks since it was not designed for this :/ + if (group.empty() || group.size() > 2) { + BOOST_LOG(warning) << "Topology group is invalid!"; + return false; + } + + for (const auto &device_id : group) { + if (device_ids.count(device_id) > 0) { + BOOST_LOG(warning) << "Duplicate device ids found!"; + return false; + } + + device_ids.insert(device_id); + } + } + + return true; + } + + bool + is_topology_the_same(const active_topology_t &topology_a, const active_topology_t &topology_b) { + const auto sort_topology = [](active_topology_t &topology) { + for (auto &group : topology) { + std::sort(std::begin(group), std::end(group)); + } + + std::sort(std::begin(topology), std::end(topology)); + }; + + auto a_copy { topology_a }; + auto b_copy { topology_b }; + + // On Windows order does not matter. + sort_topology(a_copy); + sort_topology(b_copy); + + return a_copy == b_copy; + } + + bool + set_topology(const active_topology_t &new_topology) { + if (!is_topology_valid(new_topology)) { + BOOST_LOG(error) << "Topology input is invalid!"; + return false; + } + + const auto current_topology { get_current_topology() }; + if (current_topology.empty()) { + BOOST_LOG(error) << "Failed to get current topology!"; + return false; + } + + if (is_topology_the_same(current_topology, new_topology)) { + BOOST_LOG(debug) << "Same topology provided."; + return true; + } + + if (do_set_topology(new_topology)) { + const auto updated_topology { get_current_topology() }; + if (!updated_topology.empty()) { + if (is_topology_the_same(new_topology, updated_topology)) { + return true; + } + else { + // There is an interesting bug in Windows when you have nearly + // identical devices, drivers or something. For example, imagine you have: + // AM - Actual Monitor + // IDD1 - Virtual display 1 + // IDD2 - Virtual display 2 + // + // You can have the following topology: + // [[AM, IDD1]] + // but not this: + // [[AM, IDD2]] + // + // Windows API will just default to: + // [[AM, IDD1]] + // even if you provide the second variant. Windows API will think + // it's OK and just return ERROR_SUCCESS in this case and there is + // nothing you can do. Even the Windows' settings app will not + // be able to set the desired topology. + // + // There seems to be a workaround - you need to make sure the IDD1 + // device is used somewhere else in the topology, like: + // [[AM, IDD2], [IDD1]] + // + // However, since we have this bug an additional sanity check is needed + // regardless of what Windows report back to us. + BOOST_LOG(error) << "Failed to change topology due to Windows bug or because the display is in deep sleep!"; + } + } + else { + BOOST_LOG(error) << "Failed to get updated topology!"; + } + + // Revert back to the original topology + do_set_topology(current_topology); // Return value does not matter + } + + return false; + } + +} // namespace display_device diff --git a/src/platform/windows/display_device/general_functions.cpp b/src/platform/windows/display_device/general_functions.cpp new file mode 100644 index 00000000000..0e2cf06e6f2 --- /dev/null +++ b/src/platform/windows/display_device/general_functions.cpp @@ -0,0 +1,138 @@ +// standard includes +#include + +// local includes +#include "src/logging.h" +#include "windows_utils.h" + +namespace display_device { + + std::string + get_display_name(const std::string &device_id) { + if (device_id.empty()) { + // Valid return, no error + return {}; + } + + const auto display_data { w_utils::query_display_config(w_utils::ACTIVE_ONLY_DEVICES) }; + if (!display_data) { + // Error already logged + return {}; + } + + const auto path { w_utils::get_active_path(device_id, display_data->paths) }; + if (!path) { + // Debug level, because inactive device is valid case for this function + BOOST_LOG(debug) << "Failed to find device for " << device_id << "!"; + return {}; + } + + const auto display_name { w_utils::get_display_name(*path) }; + if (display_name.empty()) { + BOOST_LOG(error) << "Device " << device_id << " has no display name assigned."; + } + + return display_name; + } + + bool + is_primary_device(const std::string &device_id) { + if (device_id.empty()) { + BOOST_LOG(error) << "Device id is empty!"; + return false; + } + + auto display_data { w_utils::query_display_config(w_utils::ACTIVE_ONLY_DEVICES) }; + if (!display_data) { + // Error already logged + return false; + } + + const auto path { w_utils::get_active_path(device_id, display_data->paths) }; + if (!path) { + BOOST_LOG(error) << "Failed to find device for " << device_id << "!"; + return false; + } + + const auto source_mode { w_utils::get_source_mode(w_utils::get_source_index(*path, display_data->modes), display_data->modes) }; + if (!source_mode) { + BOOST_LOG(error) << "Active device does not have a source mode: " << device_id << "!"; + return false; + } + + return w_utils::is_primary(*source_mode); + } + + bool + set_as_primary_device(const std::string &device_id) { + if (device_id.empty()) { + BOOST_LOG(error) << "Device id is empty!"; + return false; + } + + auto display_data { w_utils::query_display_config(w_utils::ACTIVE_ONLY_DEVICES) }; + if (!display_data) { + // Error already logged + return false; + } + + // Get the current origin point of the device (the one that we want to make primary) + POINTL origin; + { + const auto path { w_utils::get_active_path(device_id, display_data->paths) }; + if (!path) { + BOOST_LOG(error) << "Failed to find device for " << device_id << "!"; + return false; + } + + const auto source_mode { w_utils::get_source_mode(w_utils::get_source_index(*path, display_data->modes), display_data->modes) }; + if (!source_mode) { + BOOST_LOG(error) << "Active device does not have a source mode: " << device_id << "!"; + return false; + } + + if (w_utils::is_primary(*source_mode)) { + BOOST_LOG(debug) << "Device " << device_id << " is already a primary device."; + return true; + } + + origin = source_mode->position; + } + + // Without verifying if the paths are valid or not (SetDisplayConfig will verify for us), + // shift their source mode origin points accordingly, so that the provided + // device moves to (0, 0) position and others to their new positions. + std::unordered_set modified_modes; + for (auto &path : display_data->paths) { + const auto current_id { w_utils::get_device_id(path) }; + const auto source_index { w_utils::get_source_index(path, display_data->modes) }; + auto source_mode { w_utils::get_source_mode(source_index, display_data->modes) }; + + if (!source_index || !source_mode) { + BOOST_LOG(error) << "Active device does not have a source mode: " << current_id << "!"; + return false; + } + + if (modified_modes.find(*source_index) != std::end(modified_modes)) { + // Happens when VIRTUAL_MODE_AWARE is not specified when querying paths, probably will never happen in our case, but just to be safe... + BOOST_LOG(debug) << "Device " << current_id << " shares the same mode index as a previous device. Device is duplicated. Skipping."; + continue; + } + + source_mode->position.x -= origin.x; + source_mode->position.y -= origin.y; + + modified_modes.insert(*source_index); + } + + const UINT32 flags { SDC_APPLY | SDC_USE_SUPPLIED_DISPLAY_CONFIG | SDC_SAVE_TO_DATABASE | SDC_VIRTUAL_MODE_AWARE }; + const LONG result { SetDisplayConfig(display_data->paths.size(), display_data->paths.data(), display_data->modes.size(), display_data->modes.data(), flags) }; + if (result != ERROR_SUCCESS) { + BOOST_LOG(error) << w_utils::get_error_string(result) << " failed to set primary mode for " << device_id << "!"; + return false; + } + + return true; + } + +} // namespace display_device diff --git a/src/platform/windows/display_device/settings.cpp b/src/platform/windows/display_device/settings.cpp new file mode 100644 index 00000000000..bd42e0914f3 --- /dev/null +++ b/src/platform/windows/display_device/settings.cpp @@ -0,0 +1,756 @@ +// standard includes +#include +#include + +// local includes +#include "settings_topology.h" +#include "src/audio.h" +#include "src/display_device/to_string.h" +#include "src/logging.h" +#include "windows_utils.h" + +namespace display_device { + + struct settings_t::persistent_data_t { + topology_pair_t topology; /**< Contains topology before the modification and the one we modified. */ + std::string original_primary_display; /**< Original primary display in the topology we modified. Empty value if we didn't modify it. */ + device_display_mode_map_t original_modes; /**< Original display modes in the topology we modified. Empty value if we didn't modify it. */ + hdr_state_map_t original_hdr_states; /**< Original display HDR states in the topology we modified. Empty value if we didn't modify it. */ + + /** + * @brief Check if the persistent data contains any meaningful modifications that need to be reverted. + * @returns True if the data contains something that needs to be reverted, false otherwise. + * + * EXAMPLES: + * ```cpp + * settings_t::persistent_data_t data; + * if (data.contains_modifications()) { + * // save persistent data + * } + * ``` + */ + [[nodiscard]] bool + contains_modifications() const { + return !is_topology_the_same(topology.initial, topology.modified) || + !original_primary_display.empty() || + !original_modes.empty() || + !original_hdr_states.empty(); + } + + // For JSON serialization + NLOHMANN_DEFINE_TYPE_INTRUSIVE(persistent_data_t, topology, original_primary_display, original_modes, original_hdr_states) + }; + + struct settings_t::audio_data_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() }; + }; + + namespace { + + /** + * @brief Get one of the primary display ids found in the topology metadata. + * @param metadata Topology metadata that also includes current active topology. + * @return Device id for the primary device, or empty string if primary device not found somehow. + * + * EXAMPLES: + * ```cpp + * topology_metadata_t metadata; + * const std::string primary_device_id = get_current_primary_display(metadata); + * ``` + */ + std::string + get_current_primary_display(const topology_metadata_t &metadata) { + for (const auto &group : metadata.current_topology) { + for (const auto &device_id : group) { + if (is_primary_device(device_id)) { + return device_id; + } + } + } + + return std::string {}; + } + + /** + * @brief Compute the new primary display id based on the information we have. + * @param original_primary_display Original device id (the one before our first modification or from current topology). + * @param metadata The current metadata that we are evaluating. + * @return Primary display id that matches the requirements. + * + * EXAMPLES: + * ```cpp + * topology_metadata_t metadata; + * const std::string primary_device_id = determine_new_primary_display("MY_DEVICE_ID", metadata); + * ``` + */ + std::string + determine_new_primary_display(const std::string &original_primary_display, const topology_metadata_t &metadata) { + if (metadata.primary_device_requested) { + // Primary device was requested - no device was specified by user. + // This means we are keeping whatever display we have. + return original_primary_display; + } + + // For primary devices it is enough to set 1 as a primary display, as the whole duplicated group + // will become primary displays. + const auto new_primary_device { metadata.duplicated_devices.front() }; + return new_primary_device; + } + + /** + * @brief Change the primary display based on the configuration and previously configured primary display. + * + * The function performs the necessary steps for changing the primary display if needed. + * It also evaluates for possible changes in the configuration and undoes the changes + * we have made before. + * + * @param device_prep Device preparation value from the configuration. + * @param previous_primary_display Device id of the original primary display we have initially changed (can be empty). + * @param metadata Additional data with info about the current topology. + * @return Device id to be used when reverting all settings (can be empty string), or an empty optional if the function fails. + */ + boost::optional + handle_primary_display_configuration(const parsed_config_t::device_prep_e &device_prep, const std::string &previous_primary_display, const topology_metadata_t &metadata) { + if (device_prep == parsed_config_t::device_prep_e::ensure_primary) { + const auto original_primary_display { previous_primary_display.empty() ? get_current_primary_display(metadata) : previous_primary_display }; + const auto new_primary_display { determine_new_primary_display(original_primary_display, metadata) }; + + BOOST_LOG(info) << "Changing primary display to: " << new_primary_display; + if (!set_as_primary_device(new_primary_display)) { + // Error already logged + return boost::none; + } + + // Here we preserve the data from persistence (unless there's none) as in the end that is what we want to go back to. + return original_primary_display; + } + + if (!previous_primary_display.empty()) { + BOOST_LOG(info) << "Changing primary display back to: " << previous_primary_display; + if (!set_as_primary_device(previous_primary_display)) { + // Error already logged + return boost::none; + } + } + + return std::string {}; + } + + /** + * @brief Compute the new display modes based on the information we have. + * @param resolution Resolution value from the configuration. + * @param refresh_rate Refresh rate value from the configuration. + * @param original_display_modes Original display modes (the ones before our first modification or from current topology) + * that we use as a base we will apply changes to. + * @param metadata The current metadata that we are evaluating. + * @return New display modes for the topology. + */ + device_display_mode_map_t + determine_new_display_modes(const boost::optional &resolution, const boost::optional &refresh_rate, const device_display_mode_map_t &original_display_modes, const topology_metadata_t &metadata) { + device_display_mode_map_t new_modes { original_display_modes }; + + if (resolution) { + // For duplicate devices the resolution must match no matter what, otherwise + // they cannot be duplicated, which breaks Windows' rules. + for (const auto &device_id : metadata.duplicated_devices) { + new_modes[device_id].resolution = *resolution; + } + } + + if (refresh_rate) { + if (metadata.primary_device_requested) { + // No device has been specified, so if they're all are primary devices + // we need to apply the refresh rate change to all duplicates + for (const auto &device_id : metadata.duplicated_devices) { + new_modes[device_id].refresh_rate = *refresh_rate; + } + } + else { + // Even if we have duplicate devices, their refresh rate may differ + // and since the device was specified, let's apply the refresh + // rate only to the specified device. + new_modes[metadata.duplicated_devices.front()].refresh_rate = *refresh_rate; + } + } + + return new_modes; + } + + /** + * @brief Modify the display modes based on the configuration and previously configured display modes. + * + * The function performs the necessary steps for changing the display modes if needed. + * It also evaluates for possible changes in the configuration and undoes the changes + * we have made before. + * + * @param resolution Resolution value from the configuration. + * @param refresh_rate Refresh rate value from the configuration. + * @param previous_display_modes Original display modes that we have initially changed (can be empty). + * @param metadata Additional data with info about the current topology. + * @return Display modes to be used when reverting all settings (can be empty map), or an empty optional if the function fails. + */ + boost::optional + handle_display_mode_configuration(const boost::optional &resolution, const boost::optional &refresh_rate, const device_display_mode_map_t &previous_display_modes, const topology_metadata_t &metadata) { + if (resolution || refresh_rate) { + const auto original_display_modes { previous_display_modes.empty() ? get_current_display_modes(get_device_ids_from_topology(metadata.current_topology)) : previous_display_modes }; + const auto new_display_modes { determine_new_display_modes(resolution, refresh_rate, original_display_modes, metadata) }; + + BOOST_LOG(info) << "Changing display modes to: " << to_string(new_display_modes); + if (!set_display_modes(new_display_modes)) { + // Error already logged + return boost::none; + } + + // Here we preserve the data from persistence (unless there's none) as in the end that is what we want to go back to. + return original_display_modes; + } + + if (!previous_display_modes.empty()) { + BOOST_LOG(info) << "Changing display modes back to: " << to_string(previous_display_modes); + if (!set_display_modes(previous_display_modes)) { + // Error already logged + return boost::none; + } + } + + return device_display_mode_map_t {}; + } + + /** + * @brief Reverse ("blank") HDR states for newly enabled devices. + * + * Some newly enabled displays do not handle HDR state correctly (IDD HDR display for example). + * The colors can become very blown out/high contrast. A simple workaround is to toggle the HDR state + * once the display has "settled down" or something. + * + * This is what this function does, it changes the HDR state to the opposite states that we will have in the + * end, sleeps for a little and then allows us to continue changing HDR states to the final ones. + * + * "blank" comes as an inspiration from "vblank" as this function is meant to be used before changing the HDR + * states to clean up something. + * + * @param states Final states for the devices that we want to blank. + * @param newly_enabled_devices Devices to perform blanking for. + * @return False if the function has failed to set HDR states, true otherwise. + * + * EXAMPLES: + * ```cpp + * hdr_state_map_t new_states; + * const bool success = blank_hdr_states(new_states, { "DEVICE_ID" }); + * ``` + */ + bool + blank_hdr_states(const hdr_state_map_t &states, const std::unordered_set &newly_enabled_devices) { + const std::chrono::milliseconds delay { 1500 }; + if (delay > std::chrono::milliseconds::zero()) { + bool state_changed { false }; + auto toggled_states { states }; + for (const auto &device_id : newly_enabled_devices) { + auto state_it { toggled_states.find(device_id) }; + if (state_it == std::end(toggled_states)) { + continue; + } + + if (state_it->second == hdr_state_e::enabled) { + state_it->second = hdr_state_e::disabled; + state_changed = true; + } + else if (state_it->second == hdr_state_e::disabled) { + state_it->second = hdr_state_e::enabled; + state_changed = true; + } + } + + if (state_changed) { + BOOST_LOG(debug) << "Toggling HDR states for newly enabled devices and waiting for " << delay.count() << "ms before actually applying the correct states."; + if (!set_hdr_states(toggled_states)) { + // Error already logged + return false; + } + + std::this_thread::sleep_for(delay); + } + } + + return true; + } + + /** + * @brief Compute the new HDR states based on the information we have. + * @param change_hdr_state HDR state value from the configuration. + * @param original_hdr_states Original HDR states (the ones before our first modification or from current topology) + * that we use as a base we will apply changes to. + * @param metadata The current metadata that we are evaluating. + * @return New HDR states for the topology. + */ + hdr_state_map_t + determine_new_hdr_states(const boost::optional &change_hdr_state, const hdr_state_map_t &original_hdr_states, const topology_metadata_t &metadata) { + hdr_state_map_t new_states { original_hdr_states }; + + if (change_hdr_state) { + const hdr_state_e final_state { *change_hdr_state ? hdr_state_e::enabled : hdr_state_e::disabled }; + const auto try_update_new_state = [&new_states, final_state](const std::string &device_id) { + const auto current_state { new_states[device_id] }; + if (current_state == hdr_state_e::unknown) { + return; + } + + new_states[device_id] = final_state; + }; + + if (metadata.primary_device_requested) { + // No device has been specified, so if they're all are primary devices + // we need to apply the HDR state change to all duplicates + for (const auto &device_id : metadata.duplicated_devices) { + try_update_new_state(device_id); + } + } + else { + // Even if we have duplicate devices, their HDR states may differ + // and since the device was specified, let's apply the HDR state + // only to the specified device. + try_update_new_state(metadata.duplicated_devices.front()); + } + } + + return new_states; + } + + /** + * @brief Modify the display HDR states based on the configuration and previously configured display HDR states. + * + * The function performs the necessary steps for changing the display HDR states if needed. + * It also evaluates for possible changes in the configuration and undoes the changes + * we have made before. + * + * @param change_hdr_state HDR state value from the configuration. + * @param previous_hdr_states Original display HDR states have initially changed (can be empty). + * @param metadata Additional data with info about the current topology. + * @return Display HDR states to be used when reverting all settings (can be empty map), or an empty optional if the function fails. + */ + boost::optional + handle_hdr_state_configuration(const boost::optional &change_hdr_state, const hdr_state_map_t &previous_hdr_states, const topology_metadata_t &metadata) { + if (change_hdr_state) { + const auto original_hdr_states { previous_hdr_states.empty() ? get_current_hdr_states(get_device_ids_from_topology(metadata.current_topology)) : previous_hdr_states }; + const auto new_hdr_states { determine_new_hdr_states(change_hdr_state, original_hdr_states, metadata) }; + + BOOST_LOG(info) << "Changing hdr states to: " << to_string(new_hdr_states); + if (!blank_hdr_states(new_hdr_states, metadata.newly_enabled_devices) || !set_hdr_states(new_hdr_states)) { + // Error already logged + return boost::none; + } + + // Here we preserve the data from persistence (unless there's none) as in the end that is what we want to go back to. + return original_hdr_states; + } + + if (!previous_hdr_states.empty()) { + BOOST_LOG(info) << "Changing hdr states back to: " << to_string(previous_hdr_states); + if (!blank_hdr_states(previous_hdr_states, metadata.newly_enabled_devices) || !set_hdr_states(previous_hdr_states)) { + // Error already logged + return boost::none; + } + } + + return hdr_state_map_t {}; + } + + /** + * @brief Revert settings to the ones found in the persistent data. + * @param data Reference to persistent data containing original settings. + * @param data_modified Reference to a boolean that is set to true if changes are made to the persistent data reference. + * @return True if all settings within persistent data have been reverted, false otherwise. + * + * EXAMPLES: + * ```cpp + * bool data_modified { false }; + * settings_t::persistent_data_t data; + * + * if (!try_revert_settings(data, data_modified)) { + * if (data_modified) { + * // Update the persistent file + * } + * } + * ``` + */ + bool + try_revert_settings(settings_t::persistent_data_t &data, bool &data_modified) { + try { + nlohmann::json json_data = data; + BOOST_LOG(debug) << "Reverting persistent display settings from:\n" + << json_data.dump(4); + } + catch (const std::exception &err) { + BOOST_LOG(error) << "Failed to dump persistent display settings: " << err.what(); + } + + if (!data.contains_modifications()) { + return true; + } + + const bool have_changes_for_modified_topology { !data.original_primary_display.empty() || !data.original_modes.empty() || !data.original_hdr_states.empty() }; + std::unordered_set newly_enabled_devices; + bool partially_failed { false }; + auto current_topology { get_current_topology() }; + + if (have_changes_for_modified_topology) { + if (set_topology(data.topology.modified)) { + newly_enabled_devices = get_newly_enabled_devices_from_topology(current_topology, data.topology.modified); + current_topology = data.topology.modified; + + if (!data.original_hdr_states.empty()) { + BOOST_LOG(info) << "Changing back the HDR states to: " << to_string(data.original_hdr_states); + if (set_hdr_states(data.original_hdr_states)) { + data.original_hdr_states.clear(); + data_modified = true; + } + else { + partially_failed = true; + } + } + + if (!data.original_modes.empty()) { + BOOST_LOG(info) << "Changing back the display modes to: " << to_string(data.original_modes); + if (set_display_modes(data.original_modes)) { + data.original_modes.clear(); + data_modified = true; + } + else { + partially_failed = true; + } + } + + if (!data.original_primary_display.empty()) { + BOOST_LOG(info) << "Changing back the primary device to: " << data.original_primary_display; + if (set_as_primary_device(data.original_primary_display)) { + data.original_primary_display.clear(); + data_modified = true; + } + else { + partially_failed = true; + } + } + } + else { + BOOST_LOG(error) << "Cannot switch to the topology to undo changes!"; + partially_failed = true; + } + } + + BOOST_LOG(info) << "Changing display topology back to: " << to_string(data.topology.initial); + if (set_topology(data.topology.initial)) { + newly_enabled_devices.merge(get_newly_enabled_devices_from_topology(current_topology, data.topology.initial)); + current_topology = data.topology.initial; + data_modified = true; + } + else { + BOOST_LOG(error) << "Failed to switch back to the initial topology!"; + partially_failed = true; + } + + if (!newly_enabled_devices.empty()) { + const auto current_hdr_states { get_current_hdr_states(get_device_ids_from_topology(current_topology)) }; + + BOOST_LOG(debug) << "Trying to fix HDR states (if needed)."; + blank_hdr_states(current_hdr_states, newly_enabled_devices); // Return value ignored + set_hdr_states(current_hdr_states); // Return value ignored + } + + return !partially_failed; + } + + /** + * @brief Save settings to the JSON file. + * @param filepath Filepath for the persistent data. + * @param data Persistent data to save. + * @return True if the filepath is empty or the data was saved to the file, false otherwise. + * + * EXAMPLES: + * ```cpp + * settings_t::persistent_data_t data; + * + * if (save_settings("/foo/bar.json", data)) { + * // Do stuff... + * } + * ``` + */ + bool + save_settings(const std::filesystem::path &filepath, const settings_t::persistent_data_t &data) { + if (filepath.empty()) { + BOOST_LOG(warning) << "No filename was specified for persistent display device configuration."; + return true; + } + + try { + std::ofstream file(filepath, std::ios::out | std::ios::trunc); + nlohmann::json json_data = data; + + // Write json with indentation + file << std::setw(4) << json_data << std::endl; + BOOST_LOG(debug) << "Saved persistent display settings:\n" + << json_data.dump(4); + return true; + } + catch (const std::exception &err) { + BOOST_LOG(error) << "Failed to save display settings: " << err.what(); + } + + return false; + } + + /** + * @brief Load persistent data from the JSON file. + * @param filepath Filepath to load data from. + * @return Unique pointer to the persistent data if it was loaded successfully, nullptr otherwise. + * + * EXAMPLES: + * ```cpp + * auto data = load_settings("/foo/bar.json"); + * ``` + */ + std::unique_ptr + load_settings(const std::filesystem::path &filepath) { + try { + if (!filepath.empty() && std::filesystem::exists(filepath)) { + std::ifstream file(filepath); + return std::make_unique(nlohmann::json::parse(file)); + } + } + catch (const std::exception &err) { + BOOST_LOG(error) << "Failed to load saved display settings: " << err.what(); + } + + return nullptr; + } + + /** + * @brief Remove the file. + * @param filepath Filepath to remove. + * + * EXAMPLES: + * ```cpp + * remove_file("/foo/bar.json"); + * ``` + */ + void + remove_file(const std::filesystem::path &filepath) { + try { + if (!filepath.empty()) { + std::filesystem::remove(filepath); + } + } + catch (const std::exception &err) { + BOOST_LOG(error) << "Failed to remove " << filepath << ". Error: " << err.what(); + } + } + + } // namespace + + settings_t::settings_t() = default; + + settings_t::~settings_t() = default; + + bool + settings_t::is_changing_settings_going_to_fail() const { + return w_utils::is_user_session_locked() || w_utils::test_no_access_to_ccd_api(); + } + + settings_t::apply_result_t + settings_t::apply_config(const parsed_config_t &config) { + const auto do_apply_config { [this](const parsed_config_t &config) -> settings_t::apply_result_t { + bool failed_while_reverting_settings { false }; + const boost::optional previously_configured_topology { persistent_data ? boost::make_optional(persistent_data->topology) : boost::none }; + + // On Windows the display settings are kept per an active topology list - each topology + // has separate configuration saved in the database. Therefore, we must always switch + // to the topology we want to modify before we actually start applying settings. + const auto topology_result { handle_device_topology_configuration(config, previously_configured_topology, [&]() { + const bool audio_sink_was_captured { audio_data != nullptr }; + if (!revert_settings()) { + failed_while_reverting_settings = true; + return false; + } + + if (audio_sink_was_captured && !audio_data) { + audio_data = std::make_unique(); + } + return true; + }) }; + if (!topology_result) { + // Error already logged + return { failed_while_reverting_settings ? apply_result_t::result_e::revert_fail : apply_result_t::result_e::topology_fail }; + } + + // Once we have switched to the correct topology, we need to select where we want to + // save persistent data. + // + // If we already have cached persistent data, we want to use that, however we must NOT + // take over the topology "pair" from the result as the initial topology doest not + // reflect the actual initial topology before we made our first changes. + // + // There is no better way to somehow always guess the initial topology we want to revert to. + // The user could have switched topology when the stream was paused, then technically we could + // try to switch back to that topology. However, the display could have also turned off and the + // topology was automatically changed by Windows. In this case we don't want to switch back to + // that topology since it was not the user's decision. + // + // Therefore, we are always sticking with the first initial topology before the first configuration + // was applied. + persistent_data_t new_settings { topology_result->pair }; + persistent_data_t ¤t_settings { persistent_data ? *persistent_data : new_settings }; + + const auto persist_settings = [&]() -> apply_result_t { + if (current_settings.contains_modifications()) { + if (!persistent_data) { + persistent_data = std::make_unique(new_settings); + } + + if (!save_settings(filepath, *persistent_data)) { + return { apply_result_t::result_e::file_save_fail }; + } + } + else if (persistent_data) { + if (!revert_settings()) { + // Sanity check, as the revert_settings should always pass + // at this point since our settings contain no modifications. + return { apply_result_t::result_e::revert_fail }; + } + } + + return { apply_result_t::result_e::success }; + }; + + // Since we will be modifying system state in multiple steps, we + // have no choice, but to save any changes we have made so + // that we can undo them if anything fails. + auto save_guard = util::fail_guard([&]() { + persist_settings(); // Ignoring the return value + }); + + // Here each of the handler returns full set of their specific settings for + // all the displays in the topology. + // + // We have the same train of though here as with the topology - if we are + // controlling some parts of the display settings, we are taking what + // we have before any modification by us are sticking with it until we + // release the control. + // + // Also, since we keep settings for all the displays (not only the ones that + // we modify), we can use these settings as a base that will revert whatever + // we did before if we are re-applying settings with different configuration. + // + // User modified the resolution manually? Well, he shouldn't have. If we + // are responsible for the resolution, then hands off! Initial settings + // will be re-applied when the paused session is resumed. + + const auto original_primary_display { handle_primary_display_configuration(config.device_prep, current_settings.original_primary_display, topology_result->metadata) }; + if (!original_primary_display) { + // Error already logged + return { apply_result_t::result_e::primary_display_fail }; + } + current_settings.original_primary_display = *original_primary_display; + + const auto original_modes { handle_display_mode_configuration(config.resolution, config.refresh_rate, current_settings.original_modes, topology_result->metadata) }; + if (!original_modes) { + // Error already logged + return { apply_result_t::result_e::modes_fail }; + } + current_settings.original_modes = *original_modes; + + const auto original_hdr_states { handle_hdr_state_configuration(config.change_hdr_state, current_settings.original_hdr_states, topology_result->metadata) }; + if (!original_hdr_states) { + // Error already logged + return { apply_result_t::result_e::hdr_states_fail }; + } + current_settings.original_hdr_states = *original_hdr_states; + + save_guard.disable(); + return persist_settings(); + } }; + + BOOST_LOG(info) << "Applying configuration to the display device."; + const bool display_may_change { config.device_prep == parsed_config_t::device_prep_e::ensure_only_display }; + if (display_may_change && !audio_data) { + // It is very likely that in this situation our "current" audio device will be gone, so we + // want to capture the audio sink immediately and extend the audio session until we revert our changes. + BOOST_LOG(debug) << "Capturing audio sink before changing display"; + audio_data = std::make_unique(); + } + + const auto result { do_apply_config(config) }; + if (result) { + if (!display_may_change && audio_data) { + // Just to be safe in the future when the video config can be reloaded + // without Sunshine restarting, we should clean up, because in this situation + // we have had to revert the changes that turned off other displays. Thus, extending + // the session for a display that again exist is pointless. + BOOST_LOG(debug) << "Releasing captured audio sink"; + audio_data = nullptr; + } + } + + if (!result) { + BOOST_LOG(error) << "Failed to configure display:\n" + << result.get_error_message(); + } + else { + BOOST_LOG(info) << "Display device configuration applied."; + } + return result; + } + + bool + settings_t::revert_settings() { + if (!persistent_data) { + BOOST_LOG(info) << "Loading persistent display device settings."; + persistent_data = load_settings(filepath); + } + + if (persistent_data) { + BOOST_LOG(info) << "Reverting display device settings."; + + bool data_updated { false }; + if (!try_revert_settings(*persistent_data, data_updated)) { + if (data_updated) { + save_settings(filepath, *persistent_data); // Ignoring return value + } + + BOOST_LOG(error) << "Failed to revert display device settings!"; + return false; + } + + remove_file(filepath); + persistent_data = nullptr; + + if (audio_data) { + BOOST_LOG(debug) << "Releasing captured audio sink"; + audio_data = nullptr; + } + + BOOST_LOG(info) << "Display device configuration reverted."; + } + + return true; + } + + void + settings_t::reset_persistence() { + BOOST_LOG(info) << "Purging persistent display device data (trying to reset settings one last time)."; + if (persistent_data && !revert_settings()) { + BOOST_LOG(info) << "Failed to revert settings - proceeding to reset persistence."; + } + + remove_file(filepath); + persistent_data = nullptr; + + if (audio_data) { + BOOST_LOG(debug) << "Releasing captured audio sink"; + audio_data = nullptr; + } + } + +} // namespace display_device diff --git a/src/platform/windows/display_device/settings_topology.cpp b/src/platform/windows/display_device/settings_topology.cpp new file mode 100644 index 00000000000..abdce2a03a0 --- /dev/null +++ b/src/platform/windows/display_device/settings_topology.cpp @@ -0,0 +1,277 @@ +// local includes +#include "settings_topology.h" +#include "src/display_device/to_string.h" +#include "src/logging.h" + +namespace display_device { + + namespace { + + /** + * @brief Enumerate and get one of the devices matching the id or + * any of the primary devices if id is unspecified. + * @param device_id Id to find in enumerated devices. + * @return Device id, or empty string if an error has occurred. + * + * EXAMPLES: + * ```cpp + * const std::string primary_device = find_one_of_the_available_devices(""); + * const std::string id_that_matches_provided_id = find_one_of_the_available_devices(primary_device); + * ``` + */ + std::string + find_one_of_the_available_devices(const std::string &device_id) { + const auto devices { enum_available_devices() }; + if (devices.empty()) { + BOOST_LOG(error) << "Display device list is empty!"; + return {}; + } + BOOST_LOG(info) << "Available display devices: " << to_string(devices); + + const auto device_it { std::find_if(std::begin(devices), std::end(devices), [&device_id](const auto &entry) { + return device_id.empty() ? entry.second.device_state == device_state_e::primary : entry.first == device_id; + }) }; + if (device_it == std::end(devices)) { + BOOST_LOG(error) << "Device " << (device_id.empty() ? "PRIMARY" : device_id) << " not found in the list of available devices!"; + return {}; + } + + return device_it->first; + } + + /** + * @brief Get all device ids that belong in the same group as provided ids (duplicated displays). + * @param device_id Device id to search for in the topology. + * @param topology Topology to search. + * @return A list of device ids, with the provided device id always at the front. + * + * EXAMPLES: + * ```cpp + * const auto duplicated_devices = get_duplicate_devices("MY_DEVICE_ID", get_current_topology()); + * ``` + */ + std::vector + get_duplicate_devices(const std::string &device_id, const active_topology_t &topology) { + std::vector duplicated_devices; + + duplicated_devices.clear(); + duplicated_devices.push_back(device_id); + + for (const auto &group : topology) { + for (const auto &group_device_id : group) { + if (device_id == group_device_id) { + std::copy_if(std::begin(group), std::end(group), std::back_inserter(duplicated_devices), [&](const auto &id) { + return id != device_id; + }); + break; + } + } + } + + return duplicated_devices; + } + + /** + * @brief Check if device id is found in the active topology. + * @param device_id Device id to search for in the topology. + * @param topology Topology to search. + * @return True if device id is in the topology, false otherwise. + * + * EXAMPLES: + * ```cpp + * const bool is_in_topology = is_device_found_in_active_topology("MY_DEVICE_ID", get_current_topology()); + * ``` + */ + bool + is_device_found_in_active_topology(const std::string &device_id, const active_topology_t &topology) { + for (const auto &group : topology) { + for (const auto &group_device_id : group) { + if (device_id == group_device_id) { + return true; + } + } + } + + return false; + } + + /** + * @brief Compute the final topology based on the information we have. + * @param device_prep The device preparation setting from user configuration. + * @param primary_device_requested Indicates that the user did NOT specify device id to be used. + * @param duplicated_devices Devices that we need to handle. + * @param topology The current topology that we are evaluating. + * @return Topology that matches requirements and should be set. + */ + active_topology_t + determine_final_topology(parsed_config_t::device_prep_e device_prep, const bool primary_device_requested, const std::vector &duplicated_devices, const active_topology_t &topology) { + boost::optional final_topology; + + const bool topology_change_requested { device_prep != parsed_config_t::device_prep_e::no_operation }; + if (topology_change_requested) { + if (device_prep == parsed_config_t::device_prep_e::ensure_only_display) { + // Device needs to be the only one that's active or if it's a PRIMARY device, + // only the whole PRIMARY group needs to be active (in case they are duplicated) + + if (primary_device_requested) { + if (topology.size() > 1) { + // There are other topology groups other than the primary devices, + // so we need to change that + final_topology = active_topology_t { { duplicated_devices } }; + } + else { + // Primary device group is the only one active, nothing to do + } + } + else { + // Since primary_device_requested == false, it means a device was specified via config by the user + // and is the only device that needs to be enabled + + if (is_device_found_in_active_topology(duplicated_devices.front(), topology)) { + // Device is currently active in the active topology group + + if (duplicated_devices.size() > 1 || topology.size() > 1) { + // We have more than 1 device in the group, or we have more than 1 topology groups. + // We need to disable all other devices + final_topology = active_topology_t { { duplicated_devices.front() } }; + } + else { + // Our device is the only one that's active, nothing to do + } + } + else { + // Our device is not active, we need to activate it and ONLY it + final_topology = active_topology_t { { duplicated_devices.front() } }; + } + } + } + // device_prep_e::ensure_active || device_prep_e::ensure_primary + else { + // The device needs to be active at least. + + if (primary_device_requested || is_device_found_in_active_topology(duplicated_devices.front(), topology)) { + // Device is already active, nothing to do here + } + else { + // Create the extended topology as it's probably what makes sense the most... + final_topology = topology; + final_topology->push_back({ duplicated_devices.front() }); + } + } + } + + return final_topology ? *final_topology : topology; + } + + } // namespace + + std::unordered_set + get_device_ids_from_topology(const active_topology_t &topology) { + std::unordered_set device_ids; + for (const auto &group : topology) { + for (const auto &device_id : group) { + device_ids.insert(device_id); + } + } + + return device_ids; + } + + std::unordered_set + get_newly_enabled_devices_from_topology(const active_topology_t &previous_topology, const active_topology_t &new_topology) { + const auto prev_ids { get_device_ids_from_topology(previous_topology) }; + auto new_ids { get_device_ids_from_topology(new_topology) }; + + for (auto &id : prev_ids) { + new_ids.erase(id); + } + + return new_ids; + } + + boost::optional + handle_device_topology_configuration(const parsed_config_t &config, const boost::optional &previously_configured_topology, const std::function &revert_settings) { + const bool primary_device_requested { config.device_id.empty() }; + const std::string requested_device_id { find_one_of_the_available_devices(config.device_id) }; + if (requested_device_id.empty()) { + // Error already logged + return boost::none; + } + + // If we still have a previously configured topology, we could potentially skip making any changes to the topology. + // However, it could also mean that we need to revert any previous changes in case the final topology has changed somehow. + if (previously_configured_topology) { + // Here we are pretending to be in an initial topology and want to perform reevaluation in case the + // user has changed the settings while the stream was paused. For the proper "evaluation" order, + // see logic outside this conditional. + const auto prev_duplicated_devices { get_duplicate_devices(requested_device_id, previously_configured_topology->initial) }; + const auto prev_final_topology { determine_final_topology(config.device_prep, primary_device_requested, prev_duplicated_devices, previously_configured_topology->initial) }; + + // There is also an edge case where we can have a different number of primary duplicated devices, which wasn't the case + // during the initial topology configuration. If the user requested to use the primary device, + // the prev_final_topology would not reflect that change in primary duplicated devices. Therefore, we also need + // to evaluate current topology (which would have the new state of primary devices) and arrive at the + // same final topology as the prev_final_topology. + const auto current_topology { get_current_topology() }; + const auto duplicated_devices { get_duplicate_devices(requested_device_id, current_topology) }; + const auto final_topology { determine_final_topology(config.device_prep, primary_device_requested, duplicated_devices, current_topology) }; + + // If the topology we are switching to is the same as the final topology we had before, that means + // user did not change anything, and we don't need to revert changes. + if (!is_topology_the_same(previously_configured_topology->modified, prev_final_topology) || + !is_topology_the_same(previously_configured_topology->modified, final_topology)) { + BOOST_LOG(warning) << "Previous topology does not match the new one. Reverting previous changes!"; + if (!revert_settings()) { + return boost::none; + } + } + } + + // Regardless of whether the user has made any changes to the user configuration or not, we always + // need to evaluate the current topology and perform the switch if needed as the user might + // have been playing around with active displays while the stream was paused. + + const auto current_topology { get_current_topology() }; + if (!is_topology_valid(current_topology)) { + BOOST_LOG(error) << "Display topology is invalid!"; + return boost::none; + } + + // When dealing with the "requested device" here and in other functions we need to keep + // in mind that it could belong to a duplicated display and thus all of them + // need to be taken into account, which complicates everything... + auto duplicated_devices { get_duplicate_devices(requested_device_id, current_topology) }; + const auto final_topology { determine_final_topology(config.device_prep, primary_device_requested, duplicated_devices, current_topology) }; + + BOOST_LOG(debug) << "Current display topology: " << to_string(current_topology); + if (!is_topology_the_same(current_topology, final_topology)) { + BOOST_LOG(info) << "Changing display topology to: " << to_string(final_topology); + if (!set_topology(final_topology)) { + // Error already logged. + return boost::none; + } + + // It is possible that we no longer have duplicate displays, so we need to update the list + duplicated_devices = get_duplicate_devices(requested_device_id, final_topology); + } + + // This check is mainly to cover the case for "config.device_prep == no_operation" as we at least + // have to validate that the device exists, but it doesn't hurt to double-check it in all cases. + if (!is_device_found_in_active_topology(requested_device_id, final_topology)) { + BOOST_LOG(error) << "Device " << requested_device_id << " is not active!"; + return boost::none; + } + + return handled_topology_result_t { + topology_pair_t { + current_topology, + final_topology }, + topology_metadata_t { + final_topology, + get_newly_enabled_devices_from_topology(current_topology, final_topology), + primary_device_requested, + duplicated_devices } + }; + } + +} // namespace display_device diff --git a/src/platform/windows/display_device/settings_topology.h b/src/platform/windows/display_device/settings_topology.h new file mode 100644 index 00000000000..4879b3423f5 --- /dev/null +++ b/src/platform/windows/display_device/settings_topology.h @@ -0,0 +1,88 @@ +#pragma once + +// local includes +#include "src/display_device/settings.h" + +namespace display_device { + + /** + * @brief Contains metadata about the current topology. + */ + struct topology_metadata_t { + active_topology_t current_topology; /**< The currently active topology. */ + std::unordered_set newly_enabled_devices; /**< A list of device ids that were newly enabled after changing topology. */ + bool primary_device_requested; /**< Indicates that the user did NOT specify device id to be used. */ + std::vector duplicated_devices; /**< A list of devices id that we need to handle. If user specified device id, it will always be the first entry. */ + }; + + /** + * @brief Container for active topologies. + * @note Both topologies can be the same. + */ + struct topology_pair_t { + active_topology_t initial; /**< The initial topology that we had before we switched. */ + active_topology_t modified; /**< The topology that we have modified. */ + + // For JSON serialization + NLOHMANN_DEFINE_TYPE_INTRUSIVE(topology_pair_t, initial, modified) + }; + + /** + * @brief Contains the result after handling the configuration. + * @see handle_device_topology_configuration + */ + struct handled_topology_result_t { + topology_pair_t pair; + topology_metadata_t metadata; + }; + + /** + * @brief Get all ids from the active topology structure. + * @param topology Topology to get ids from. + * @returns A list of device ids. + * + * EXAMPLES: + * ```cpp + * const auto device_ids = get_device_ids_from_topology(get_current_topology()); + * ``` + */ + std::unordered_set + get_device_ids_from_topology(const active_topology_t &topology); + + /** + * @brief Get new device ids that were not present in previous topology. + * @param previous_topology The previous topology. + * @param new_topology A new topology. + * @return A list of devices ids. + * + * EXAMPLES: + * ```cpp + * active_topology_t old_topology { { "ID_1" } }; + * active_topology_t new_topology { { "ID_1" }, { "ID_2" } }; + * const auto device_ids = get_newly_enabled_devices_from_topology(old_topology, new_topology); + * // device_ids contains "ID_2" + * ``` + */ + std::unordered_set + get_newly_enabled_devices_from_topology(const active_topology_t &previous_topology, const active_topology_t &new_topology); + + /** + * @brief Modify the topology based on the configuration and previously configured topology. + * + * The function performs the necessary steps for changing topology if needed. + * It evaluates the previous configuration in case we are just updating + * some of the settings (like resolution) where topology change might not be necessary. + * + * In case the function determines that we need to revert all of the previous settings + * since the new topology is not compatible with the previously configured one, the revert_settings + * parameter will be called to completely revert all changes. + * + * @param config Configuration to be evaluated. + * @param previously_configured_topology A result from a earlier call of this function. + * @param revert_settings A function-proxy that can be used to revert all of the changes made to the device displays. + * @return A result object, or an empty optional if the function fails. + */ + boost::optional + handle_device_topology_configuration(const parsed_config_t &config, const boost::optional &previously_configured_topology, const std::function &revert_settings); + +} // namespace display_device diff --git a/src/platform/windows/display_device/windows_utils.cpp b/src/platform/windows/display_device/windows_utils.cpp new file mode 100644 index 00000000000..176c8a7b7fa --- /dev/null +++ b/src/platform/windows/display_device/windows_utils.cpp @@ -0,0 +1,654 @@ +// lib includes +#include +#include +#include +#include + +// standard includes +#include +#include + +// local includes +#include "src/logging.h" +#include "src/platform/windows/misc.h" +#include "src/utility.h" +#include "windows_utils.h" + +// Windows includes after "windows.h" +#include +#include + +namespace display_device::w_utils { + + namespace { + + /** + * @see get_monitor_device_path description for more information as this + * function is identical except that it returns wide-string instead + * of a normal one. + */ + std::wstring + get_monitor_device_path_wstr(const DISPLAYCONFIG_PATH_INFO &path) { + DISPLAYCONFIG_TARGET_DEVICE_NAME target_name = {}; + target_name.header.adapterId = path.targetInfo.adapterId; + target_name.header.id = path.targetInfo.id; + target_name.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME; + target_name.header.size = sizeof(target_name); + + LONG result { DisplayConfigGetDeviceInfo(&target_name.header) }; + if (result != ERROR_SUCCESS) { + BOOST_LOG(error) << get_error_string(result) << " failed to get target device name!"; + return {}; + } + + return std::wstring { target_name.monitorDevicePath }; + } + + /** + * @brief Helper method for dealing with SetupAPI. + * @returns True if device interface path was retrieved and is non-empty, false otherwise. + * @see get_device_id implementation for more context regarding this madness. + */ + bool + get_device_interface_detail(HDEVINFO dev_info_handle, SP_DEVICE_INTERFACE_DATA &dev_interface_data, std::wstring &dev_interface_path, SP_DEVINFO_DATA &dev_info_data) { + DWORD required_size_in_bytes { 0 }; + if (SetupDiGetDeviceInterfaceDetailW(dev_info_handle, &dev_interface_data, nullptr, 0, &required_size_in_bytes, nullptr)) { + BOOST_LOG(error) << "\"SetupDiGetDeviceInterfaceDetailW\" did not fail, what?!"; + return false; + } + else if (required_size_in_bytes <= 0) { + BOOST_LOG(error) << get_error_string(static_cast(GetLastError())) << " \"SetupDiGetDeviceInterfaceDetailW\" failed while getting size."; + return false; + } + + std::vector buffer; + buffer.resize(required_size_in_bytes); + + // This part is just EVIL! + auto detail_data { reinterpret_cast(buffer.data()) }; + detail_data->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA_W); + + if (!SetupDiGetDeviceInterfaceDetailW(dev_info_handle, &dev_interface_data, detail_data, required_size_in_bytes, nullptr, &dev_info_data)) { + BOOST_LOG(error) << get_error_string(static_cast(GetLastError())) << " \"SetupDiGetDeviceInterfaceDetailW\" failed."; + return false; + } + + dev_interface_path = std::wstring { detail_data->DevicePath }; + return !dev_interface_path.empty(); + } + + /** + * @brief Helper method for dealing with SetupAPI. + * @returns True if instance id was retrieved and is non-empty, false otherwise. + * @see get_device_id implementation for more context regarding this madness. + */ + bool + get_device_instance_id(HDEVINFO dev_info_handle, SP_DEVINFO_DATA &dev_info_data, std::wstring &instance_id) { + DWORD required_size_in_characters { 0 }; + if (SetupDiGetDeviceInstanceIdW(dev_info_handle, &dev_info_data, nullptr, 0, &required_size_in_characters)) { + BOOST_LOG(error) << "\"SetupDiGetDeviceInstanceIdW\" did not fail, what?!"; + return false; + } + else if (required_size_in_characters <= 0) { + BOOST_LOG(error) << get_error_string(static_cast(GetLastError())) << " \"SetupDiGetDeviceInstanceIdW\" failed while getting size."; + return false; + } + + instance_id.resize(required_size_in_characters); + if (!SetupDiGetDeviceInstanceIdW(dev_info_handle, &dev_info_data, instance_id.data(), instance_id.size(), nullptr)) { + BOOST_LOG(error) << get_error_string(static_cast(GetLastError())) << " \"SetupDiGetDeviceInstanceIdW\" failed."; + return false; + } + + return !instance_id.empty(); + } + + /** + * @brief Helper method for dealing with SetupAPI. + * @returns True if EDID was retrieved and is non-empty, false otherwise. + * @see get_device_id implementation for more context regarding this madness. + */ + bool + get_device_edid(HDEVINFO dev_info_handle, SP_DEVINFO_DATA &dev_info_data, std::vector &edid) { + // We could just directly open the registry key as the path is known, but we can also use the this + HKEY reg_key { SetupDiOpenDevRegKey(dev_info_handle, &dev_info_data, DICS_FLAG_GLOBAL, 0, DIREG_DEV, KEY_READ) }; + if (reg_key == INVALID_HANDLE_VALUE) { + BOOST_LOG(error) << get_error_string(static_cast(GetLastError())) << " \"SetupDiOpenDevRegKey\" failed."; + return false; + } + + const auto reg_key_cleanup { + util::fail_guard([®_key]() { + const auto status { RegCloseKey(reg_key) }; + if (status != ERROR_SUCCESS) { + BOOST_LOG(error) << get_error_string(status) << " \"RegCloseKey\" failed."; + } + }) + }; + + DWORD required_size_in_bytes { 0 }; + auto status { RegQueryValueExW(reg_key, L"EDID", nullptr, nullptr, nullptr, &required_size_in_bytes) }; + if (status != ERROR_SUCCESS) { + BOOST_LOG(error) << get_error_string(status) << " \"RegQueryValueExW\" failed when getting size."; + return false; + } + + edid.resize(required_size_in_bytes); + + status = RegQueryValueExW(reg_key, L"EDID", nullptr, nullptr, edid.data(), &required_size_in_bytes); + if (status != ERROR_SUCCESS) { + BOOST_LOG(error) << get_error_string(status) << " \"RegQueryValueExW\" failed when getting size."; + return false; + } + + return !edid.empty(); + } + + } // namespace + + std::string + get_error_string(LONG error_code) { + std::stringstream error; + error << "[code: "; + switch (error_code) { + case ERROR_INVALID_PARAMETER: + error << "ERROR_INVALID_PARAMETER"; + break; + case ERROR_NOT_SUPPORTED: + error << "ERROR_NOT_SUPPORTED"; + break; + case ERROR_ACCESS_DENIED: + error << "ERROR_ACCESS_DENIED"; + break; + case ERROR_INSUFFICIENT_BUFFER: + error << "ERROR_INSUFFICIENT_BUFFER"; + break; + case ERROR_GEN_FAILURE: + error << "ERROR_GEN_FAILURE"; + break; + case ERROR_SUCCESS: + error << "ERROR_SUCCESS"; + break; + default: + error << error_code; + break; + } + error << ", message: " << std::system_category().message(static_cast(error_code)) << "]"; + return error.str(); + } + + bool + is_primary(const DISPLAYCONFIG_SOURCE_MODE &mode) { + return mode.position.x == 0 && mode.position.y == 0; + } + + bool + are_modes_duplicated(const DISPLAYCONFIG_SOURCE_MODE &mode_a, const DISPLAYCONFIG_SOURCE_MODE &mode_b) { + return mode_a.position.x == mode_b.position.x && mode_a.position.y == mode_b.position.y; + } + + bool + is_available(const DISPLAYCONFIG_PATH_INFO &path) { + return path.targetInfo.targetAvailable == TRUE; + } + + bool + is_active(const DISPLAYCONFIG_PATH_INFO &path) { + return static_cast(path.flags & DISPLAYCONFIG_PATH_ACTIVE); + } + + void + set_active(DISPLAYCONFIG_PATH_INFO &path) { + path.flags |= DISPLAYCONFIG_PATH_ACTIVE; + } + + std::string + get_device_id(const DISPLAYCONFIG_PATH_INFO &path) { + const auto device_path { get_monitor_device_path_wstr(path) }; + if (device_path.empty()) { + // Error already logged + return {}; + } + + static const GUID monitor_guid { 0xe6f07b5f, 0xee97, 0x4a90, { 0xb0, 0x76, 0x33, 0xf5, 0x7b, 0xf4, 0xea, 0xa7 } }; + std::vector device_id_data; + + HDEVINFO dev_info_handle { SetupDiGetClassDevsW(&monitor_guid, nullptr, nullptr, DIGCF_DEVICEINTERFACE) }; + if (dev_info_handle) { + const auto dev_info_handle_cleanup { + util::fail_guard([&dev_info_handle]() { + if (!SetupDiDestroyDeviceInfoList(dev_info_handle)) { + BOOST_LOG(error) << get_error_string(static_cast(GetLastError())) << " \"SetupDiDestroyDeviceInfoList\" failed."; + } + }) + }; + + SP_DEVICE_INTERFACE_DATA dev_interface_data {}; + dev_interface_data.cbSize = sizeof(dev_interface_data); + for (DWORD monitor_index = 0;; ++monitor_index) { + if (!SetupDiEnumDeviceInterfaces(dev_info_handle, nullptr, &monitor_guid, monitor_index, &dev_interface_data)) { + const DWORD error_code { GetLastError() }; + if (error_code == ERROR_NO_MORE_ITEMS) { + break; + } + + BOOST_LOG(warning) << get_error_string(static_cast(error_code)) << " \"SetupDiEnumDeviceInterfaces\" failed."; + continue; + } + + std::wstring dev_interface_path; + SP_DEVINFO_DATA dev_info_data {}; + dev_info_data.cbSize = sizeof(dev_info_data); + if (!get_device_interface_detail(dev_info_handle, dev_interface_data, dev_interface_path, dev_info_data)) { + // Error already logged + continue; + } + + if (!boost::iequals(dev_interface_path, device_path)) { + continue; + } + + // Instance ID is unique in the system and persists restarts, but not driver re-installs. + // It looks like this: + // DISPLAY\ACI27EC\5&4FD2DE4&5&UID4352 (also used in the device path it seems) + // a b c d e + // + // a) Hardware ID - stable + // b) Either a bus number or has something to do with device capabilities - stable + // c) Another ID, somehow tied to adapter (not an adapter ID from path object) - stable + // d) Some sort of rotating counter thing, changes after driver reinstall - unstable + // e) Seems to be the same as a target ID from path, it changes based on GPU port - semi-stable + // + // The instance ID also seems to be a part of the registry key (in case some other info is needed in the future): + // HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Enum\DISPLAY\ACI27EC\5&4fd2de4&5&UID4352 + + std::wstring instance_id; + if (!get_device_instance_id(dev_info_handle, dev_info_data, instance_id)) { + // Error already logged + break; + } + + if (!get_device_edid(dev_info_handle, dev_info_data, device_id_data)) { + // Error already logged + break; + } + + // We are going to discard the unstable parts of the instance ID and merge the stable parts with the edid buffer (if available) + auto unstable_part_index = instance_id.find_first_of(L'&', 0); + if (unstable_part_index != std::wstring::npos) { + unstable_part_index = instance_id.find_first_of(L'&', unstable_part_index + 1); + } + + if (unstable_part_index == std::wstring::npos) { + BOOST_LOG(error) << "Failed to split off the stable part from instance id string " << platf::to_utf8(instance_id); + break; + } + + auto semi_stable_part_index = instance_id.find_first_of(L'&', unstable_part_index + 1); + if (semi_stable_part_index == std::wstring::npos) { + BOOST_LOG(error) << "Failed to split off the semi-stable part from instance id string " << platf::to_utf8(instance_id); + break; + } + + BOOST_LOG(verbose) << "Creating device id for path " << platf::to_utf8(device_path) << " from EDID and instance ID: " << platf::to_utf8({ std::begin(instance_id), std::begin(instance_id) + unstable_part_index }) << platf::to_utf8({ std::begin(instance_id) + semi_stable_part_index, std::end(instance_id) }); + device_id_data.insert(std::end(device_id_data), + reinterpret_cast(instance_id.data()), + reinterpret_cast(instance_id.data() + unstable_part_index)); + device_id_data.insert(std::end(device_id_data), + reinterpret_cast(instance_id.data() + semi_stable_part_index), + reinterpret_cast(instance_id.data() + instance_id.size())); + break; + } + } + + if (device_id_data.empty()) { + // Using the device path as a fallback, which is always unique, but not as stable as the preferred one + BOOST_LOG(verbose) << "Creating device id from path " << platf::to_utf8(device_path); + device_id_data.insert(std::end(device_id_data), + reinterpret_cast(device_path.data()), + reinterpret_cast(device_path.data() + device_path.size())); + } + + static constexpr boost::uuids::uuid ns_id {}; // null namespace = no salt + const auto boost_uuid { boost::uuids::name_generator_sha1 { ns_id }(device_id_data.data(), device_id_data.size()) }; + return "{" + boost::uuids::to_string(boost_uuid) + "}"; + } + + std::string + get_monitor_device_path(const DISPLAYCONFIG_PATH_INFO &path) { + return platf::to_utf8(get_monitor_device_path_wstr(path)); + } + + std::string + get_friendly_name(const DISPLAYCONFIG_PATH_INFO &path) { + DISPLAYCONFIG_TARGET_DEVICE_NAME target_name = {}; + target_name.header.adapterId = path.targetInfo.adapterId; + target_name.header.id = path.targetInfo.id; + target_name.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME; + target_name.header.size = sizeof(target_name); + + LONG result { DisplayConfigGetDeviceInfo(&target_name.header) }; + if (result != ERROR_SUCCESS) { + BOOST_LOG(error) << get_error_string(result) << " failed to get target device name!"; + return {}; + } + + return target_name.flags.friendlyNameFromEdid ? platf::to_utf8(target_name.monitorFriendlyDeviceName) : std::string {}; + } + + std::string + get_display_name(const DISPLAYCONFIG_PATH_INFO &path) { + DISPLAYCONFIG_SOURCE_DEVICE_NAME source_name = {}; + source_name.header.id = path.sourceInfo.id; + source_name.header.adapterId = path.sourceInfo.adapterId; + source_name.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME; + source_name.header.size = sizeof(source_name); + + LONG result { DisplayConfigGetDeviceInfo(&source_name.header) }; + if (result != ERROR_SUCCESS) { + BOOST_LOG(error) << get_error_string(result) << " failed to get display name! "; + return {}; + } + + return platf::to_utf8(source_name.viewGdiDeviceName); + } + + hdr_state_e + get_hdr_state(const DISPLAYCONFIG_PATH_INFO &path) { + if (!is_active(path)) { + // Checking if active to suppress the error message below. + return hdr_state_e::unknown; + } + + DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO color_info = {}; + color_info.header.adapterId = path.targetInfo.adapterId; + color_info.header.id = path.targetInfo.id; + color_info.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_ADVANCED_COLOR_INFO; + color_info.header.size = sizeof(color_info); + + LONG result { DisplayConfigGetDeviceInfo(&color_info.header) }; + if (result != ERROR_SUCCESS) { + BOOST_LOG(error) << get_error_string(result) << " failed to get advanced color info! "; + return hdr_state_e::unknown; + } + + return color_info.advancedColorSupported ? color_info.advancedColorEnabled ? hdr_state_e::enabled : hdr_state_e::disabled : hdr_state_e::unknown; + } + + bool + set_hdr_state(const DISPLAYCONFIG_PATH_INFO &path, bool enable) { + DISPLAYCONFIG_SET_ADVANCED_COLOR_STATE color_state = {}; + color_state.header.adapterId = path.targetInfo.adapterId; + color_state.header.id = path.targetInfo.id; + color_state.header.type = DISPLAYCONFIG_DEVICE_INFO_SET_ADVANCED_COLOR_STATE; + color_state.header.size = sizeof(color_state); + + color_state.enableAdvancedColor = enable ? 1 : 0; + + LONG result { DisplayConfigSetDeviceInfo(&color_state.header) }; + if (result != ERROR_SUCCESS) { + BOOST_LOG(error) << get_error_string(result) << " failed to set advanced color info!"; + return false; + } + + return true; + } + + boost::optional + get_source_index(const DISPLAYCONFIG_PATH_INFO &path, const std::vector &modes) { + // The MS docs is not clear when to access union struct or not. It appears that union struct is available, + // whenever QDC_VIRTUAL_MODE_AWARE is specified when querying. + // + // The docs state, however, that it is only available when + // DISPLAYCONFIG_PATH_SUPPORT_VIRTUAL_MODE flag is set, but that is just BS (maybe copy-pasta mistake), because some cases + // were found where the flag is not set and the union is still being used. + + const UINT32 index { path.sourceInfo.sourceModeInfoIdx }; + if (index == DISPLAYCONFIG_PATH_SOURCE_MODE_IDX_INVALID) { + return boost::none; + } + + if (index >= modes.size()) { + BOOST_LOG(error) << "Source index " << index << " is out of range " << modes.size(); + return boost::none; + } + + return index; + } + + void + set_source_index(DISPLAYCONFIG_PATH_INFO &path, const boost::optional &index) { + // The MS docs is not clear when to access union struct or not. It appears that union struct is available, + // whenever QDC_VIRTUAL_MODE_AWARE is specified when querying. + // + // The docs state, however, that it is only available when + // DISPLAYCONFIG_PATH_SUPPORT_VIRTUAL_MODE flag is set, but that is just BS (maybe copy-pasta mistake), because some cases + // were found where the flag is not set and the union is still being used. + + if (index) { + path.sourceInfo.sourceModeInfoIdx = *index; + } + else { + path.sourceInfo.sourceModeInfoIdx = DISPLAYCONFIG_PATH_SOURCE_MODE_IDX_INVALID; + } + } + + void + set_target_index(DISPLAYCONFIG_PATH_INFO &path, const boost::optional &index) { + // The MS docs is not clear when to access union struct or not. It appears that union struct is available, + // whenever QDC_VIRTUAL_MODE_AWARE is specified when querying. + // + // The docs state, however, that it is only available when + // DISPLAYCONFIG_PATH_SUPPORT_VIRTUAL_MODE flag is set, but that is just BS (maybe copy-pasta mistake), because some cases + // were found where the flag is not set and the union is still being used. + + if (index) { + path.targetInfo.targetModeInfoIdx = *index; + } + else { + path.targetInfo.targetModeInfoIdx = DISPLAYCONFIG_PATH_TARGET_MODE_IDX_INVALID; + } + } + + void + set_desktop_index(DISPLAYCONFIG_PATH_INFO &path, const boost::optional &index) { + // The MS docs is not clear when to access union struct or not. It appears that union struct is available, + // whenever QDC_VIRTUAL_MODE_AWARE is specified when querying. + // + // The docs state, however, that it is only available when + // DISPLAYCONFIG_PATH_SUPPORT_VIRTUAL_MODE flag is set, but that is just BS (maybe copy-pasta mistake), because some cases + // were found where the flag is not set and the union is still being used. + + if (index) { + path.targetInfo.desktopModeInfoIdx = *index; + } + else { + path.targetInfo.desktopModeInfoIdx = DISPLAYCONFIG_PATH_DESKTOP_IMAGE_IDX_INVALID; + } + } + + void + set_clone_group_id(DISPLAYCONFIG_PATH_INFO &path, const boost::optional &id) { + // The MS docs is not clear when to access union struct or not. It appears that union struct is available, + // whenever QDC_VIRTUAL_MODE_AWARE is specified when querying. + // + // The docs state, however, that it is only available when + // DISPLAYCONFIG_PATH_SUPPORT_VIRTUAL_MODE flag is set, but that is just BS (maybe copy-pasta mistake), because some cases + // were found where the flag is not set and the union is still being used. + + if (id) { + path.sourceInfo.cloneGroupId = *id; + } + else { + path.sourceInfo.cloneGroupId = DISPLAYCONFIG_PATH_CLONE_GROUP_INVALID; + } + } + + const DISPLAYCONFIG_SOURCE_MODE * + get_source_mode(const boost::optional &index, const std::vector &modes) { + if (!index) { + return nullptr; + } + + if (*index >= modes.size()) { + BOOST_LOG(error) << "Source index " << *index << " is out of range " << modes.size(); + return nullptr; + } + + const auto &mode { modes[*index] }; + if (mode.infoType != DISPLAYCONFIG_MODE_INFO_TYPE_SOURCE) { + BOOST_LOG(error) << "Mode at index " << *index << " is not source mode!"; + return nullptr; + } + + return &mode.sourceMode; + } + + DISPLAYCONFIG_SOURCE_MODE * + get_source_mode(const boost::optional &index, std::vector &modes) { + return const_cast(get_source_mode(index, const_cast &>(modes))); + } + + boost::optional + get_device_info_for_valid_path(const DISPLAYCONFIG_PATH_INFO &path, bool must_be_active) { + if (!is_available(path)) { + // Could be transient issue according to MSDOCS (no longer available, but still "active") + return boost::none; + } + + if (must_be_active) { + if (!is_active(path)) { + return boost::none; + } + } + + const auto device_path { get_monitor_device_path(path) }; + if (device_path.empty()) { + return boost::none; + } + + const auto device_id { get_device_id(path) }; + if (device_id.empty()) { + return boost::none; + } + + const auto display_name { get_display_name(path) }; + if (display_name.empty()) { + return boost::none; + } + + return device_info_t { device_path, device_id }; + } + + boost::optional + query_display_config(bool active_only) { + std::vector paths; + std::vector modes; + LONG result = ERROR_SUCCESS; + + // When we want to enable/disable displays, we need to get all paths as they will not be active. + // This will require some additional filtering of duplicate and otherwise useless paths. + UINT32 flags = active_only ? QDC_ONLY_ACTIVE_PATHS : QDC_ALL_PATHS; + flags |= QDC_VIRTUAL_MODE_AWARE; // supported from W10 onwards + + do { + UINT32 path_count { 0 }; + UINT32 mode_count { 0 }; + + result = GetDisplayConfigBufferSizes(flags, &path_count, &mode_count); + if (result != ERROR_SUCCESS) { + BOOST_LOG(error) << get_error_string(result) << " failed to get display paths and modes!"; + return boost::none; + } + + paths.resize(path_count); + modes.resize(mode_count); + result = QueryDisplayConfig(flags, &path_count, paths.data(), &mode_count, modes.data(), nullptr); + + // The function may have returned fewer paths/modes than estimated + paths.resize(path_count); + modes.resize(mode_count); + + // It's possible that between the call to GetDisplayConfigBufferSizes and QueryDisplayConfig + // that the display state changed, so loop on the case of ERROR_INSUFFICIENT_BUFFER. + } while (result == ERROR_INSUFFICIENT_BUFFER); + + if (result != ERROR_SUCCESS) { + BOOST_LOG(error) << get_error_string(result) << " failed to query display paths and modes!"; + return boost::none; + } + + return path_and_mode_data_t { paths, modes }; + } + + const DISPLAYCONFIG_PATH_INFO * + get_active_path(const std::string &device_id, const std::vector &paths) { + for (const auto &path : paths) { + const auto device_info { get_device_info_for_valid_path(path, ACTIVE_ONLY_DEVICES) }; + if (!device_info) { + continue; + } + + if (device_info->device_id == device_id) { + return &path; + } + } + + return nullptr; + } + + DISPLAYCONFIG_PATH_INFO * + get_active_path(const std::string &device_id, std::vector &paths) { + return const_cast(get_active_path(device_id, const_cast &>(paths))); + } + + bool + is_user_session_locked() { + LPWSTR buffer { nullptr }; + const auto cleanup_guard { + util::fail_guard([&buffer]() { + if (buffer) { + WTSFreeMemory(buffer); + } + }) + }; + + DWORD buffer_size_in_bytes { 0 }; + if (WTSQuerySessionInformationW(WTS_CURRENT_SERVER_HANDLE, WTSGetActiveConsoleSessionId(), WTSSessionInfoEx, &buffer, &buffer_size_in_bytes)) { + if (buffer_size_in_bytes > 0) { + const auto wts_info { reinterpret_cast(buffer) }; + if (wts_info && wts_info->Level == 1) { + const bool is_locked { wts_info->Data.WTSInfoExLevel1.SessionFlags == WTS_SESSIONSTATE_LOCK }; + BOOST_LOG(debug) << "is_user_session_locked: " << is_locked; + return is_locked; + } + } + + BOOST_LOG(warning) << "Failed to get session info in is_user_session_locked."; + } + else { + BOOST_LOG(error) << get_error_string(GetLastError()) << " failed while calling WTSQuerySessionInformationW!"; + } + + return false; + } + + bool + test_no_access_to_ccd_api() { + auto display_data { query_display_config(w_utils::ACTIVE_ONLY_DEVICES) }; + if (!display_data) { + BOOST_LOG(debug) << "test_no_access_to_ccd_api failed in query_display_config."; + return true; + } + + // Here we are supplying the retrieved display data back to SetDisplayConfig (with VALIDATE flag only, so that we make no actual changes). + // Unless something is really broken on Windows, this call should never fail under normal circumstances - the configuration is 100% correct, since it was + // provided by Windows. + const UINT32 flags { SDC_VALIDATE | SDC_USE_SUPPLIED_DISPLAY_CONFIG | SDC_VIRTUAL_MODE_AWARE }; + const LONG result { SetDisplayConfig(display_data->paths.size(), display_data->paths.data(), display_data->modes.size(), display_data->modes.data(), flags) }; + + BOOST_LOG(debug) << "test_no_access_to_ccd_api result: " << get_error_string(result); + return result == ERROR_ACCESS_DENIED; + } + +} // namespace display_device::w_utils diff --git a/src/platform/windows/display_device/windows_utils.h b/src/platform/windows/display_device/windows_utils.h new file mode 100644 index 00000000000..78843a8ad8c --- /dev/null +++ b/src/platform/windows/display_device/windows_utils.h @@ -0,0 +1,491 @@ +#pragma once + +// the most stupid windows include (because it needs to be first...) +#include + +// local includes +#include "src/display_device/display_device.h" + +namespace display_device::w_utils { + + constexpr bool ACTIVE_ONLY_DEVICES { true }; /**< The device path must be active. */ + constexpr bool ALL_DEVICES { false }; /**< The device path can be active or inactive. */ + + /** + * @brief Contains currently available paths and associated modes. + */ + struct path_and_mode_data_t { + std::vector paths; /**< Available display paths. */ + std::vector modes; /**< Display modes for ACTIVE displays. */ + }; + + /** + * @brief Contains the device path and the id for a VALID device. + * @see get_device_info_for_valid_path for what is considered a valid device. + * @see get_device_id for how we make the device id. + */ + struct device_info_t { + std::string device_path; /**< Unique device path string. */ + std::string device_id; /**< A device id (made up by us) that is identifies the device. */ + }; + + /** + * @brief Stringify the error code from Windows API. + * @param error_code Error code to stringify. + * @returns String containing the error code in a readable format + a system message describing the code. + * + * EXAMPLES: + * ```cpp + * const std::string error_message = get_error_string(ERROR_NOT_SUPPORTED); + * ``` + */ + std::string + get_error_string(LONG error_code); + + /** + * @brief Check if the display's source mode is primary - if the associated device is a primary display device. + * @param mode Mode to check. + * @returns True if the mode's origin point is at (0, 0) coordinate (primary), false otherwise. + * @note It is possible to have multiple primary source modes at the same time. + * @see get_source_mode on how to get the source mode. + * + * EXAMPLES: + * ```cpp + * DISPLAYCONFIG_SOURCE_MODE mode; + * const bool is_primary = is_primary(mode); + * ``` + */ + bool + is_primary(const DISPLAYCONFIG_SOURCE_MODE &mode); + + /** + * @brief Check if the source modes are duplicated (cloned). + * @param mode_a First mode to check. + * @param mode_b Second mode to check. + * @returns True if both mode have the same origin point, false otherwise. + * @note Windows enforces the behaviour that only the duplicate devices can + * have the same origin point as otherwise the configuration is considered invalid by the OS. + * @see get_source_mode on how to get the source mode. + * + * EXAMPLES: + * ```cpp + * DISPLAYCONFIG_SOURCE_MODE mode_a; + * DISPLAYCONFIG_SOURCE_MODE mode_b; + * const bool are_duplicated = are_modes_duplicated(mode_a, mode_b); + * ``` + */ + bool + are_modes_duplicated(const DISPLAYCONFIG_SOURCE_MODE &mode_a, const DISPLAYCONFIG_SOURCE_MODE &mode_b); + + /** + * @brief Check if the display device path's target is available. + * + * In most cases this this would mean physically connected to the system, + * but it also possible force the path to persist. It is not clear if it be + * counted as available or not. + * + * @param path Path to check. + * @returns True if path's target is marked as available, false otherwise. + * @see query_display_config on how to get paths from the system. + * + * EXAMPLES: + * ```cpp + * DISPLAYCONFIG_PATH_INFO path; + * const bool available = is_available(path); + * ``` + */ + bool + is_available(const DISPLAYCONFIG_PATH_INFO &path); + + /** + * @brief Check if the display device path is marked as active. + * @param path Path to check. + * @returns True if path is marked as active, false otherwise. + * @see query_display_config on how to get paths from the system. + * + * EXAMPLES: + * ```cpp + * DISPLAYCONFIG_PATH_INFO path; + * const bool active = is_active(path); + * ``` + */ + bool + is_active(const DISPLAYCONFIG_PATH_INFO &path); + + /** + * @brief Mark the display device path as active. + * @param path Path to mark. + * @see query_display_config on how to get paths from the system. + * + * EXAMPLES: + * ```cpp + * DISPLAYCONFIG_PATH_INFO path; + * if (!is_active(path)) { + * set_active(path); + * } + * ``` + */ + void + set_active(DISPLAYCONFIG_PATH_INFO &path); + + /** + * @brief Get a stable and persistent device id for the path. + * + * This function tries to generate a unique id for the path that + * is persistent between driver re-installs and physical unplugging and + * replugging of the device. + * + * The best candidate for it could have been a "ContainerID" from the + * registry, however it was found to be unstable for the virtual display + * (probably because it uses the EDID for the id generation and the current + * virtual displays have incomplete EDID information). The "ContainerID" + * also does not change if the physical device is plugged into a different + * port and seems to be very stable, however because of virtual displays + * other solution was used. + * + * The accepted solution was to use the "InstanceID" and EDID (just to be + * on the safe side). "InstanceID" is semi-stable, it has some parts that + * change between driver re-installs and it has a part that changes based + * on the GPU port that the display is connected to. It is most likely to + * be unique, but since the MS documentation is lacking we are also hashing + * EDID information (contains serial ids, timestamps and etc. that should + * guarantee that identical displays are differentiated like with the + * "ContainerID"). Most importantly this information is stable for the virtual + * displays. + * + * After we remove the unstable parts from the "InstanceID" and hash everything + * together, we get an id that changes only when you connect the display to + * a different GPU port which seems to be acceptable. + * + * As a fallback we are using a hashed device path, in case the "InstanceID" or + * EDID is not available. At least if you don't do driver re-installs often + * and change the GPU ports, it will be stable for a while. + * + * @param path Path to get the device id for. + * @returns Device id, or an empty string if it could not be generated. + * @see query_display_config on how to get paths from the system. + * + * EXAMPLES: + * ```cpp + * DISPLAYCONFIG_PATH_INFO path; + * const std::string device_path = get_device_id(path); + * ``` + */ + std::string + get_device_id(const DISPLAYCONFIG_PATH_INFO &path); + + /** + * @brief Get a string that represents a path from the adapter to the display target. + * @param path Path to get the string for. + * @returns String representation, or an empty string if it's not available. + * @see query_display_config on how to get paths from the system. + * @note In the rest of the code we refer to this string representation simply as the "device path". + * It is used as a simple way of grouping related path objects together and removing "bad" paths + * that don't have such string representation. + * + * EXAMPLES: + * ```cpp + * DISPLAYCONFIG_PATH_INFO path; + * const std::string device_path = get_monitor_device_path(path); + * ``` + */ + std::string + get_monitor_device_path(const DISPLAYCONFIG_PATH_INFO &path); + + /** + * @brief Get the user friendly name for the path. + * @param path Path to get user friendly name for. + * @returns User friendly name for the path if available, empty string otherwise. + * @see query_display_config on how to get paths from the system. + * @note This is usually a monitor name (like "ROG PG279Q") and is most likely take from EDID. + * + * EXAMPLES: + * ```cpp + * DISPLAYCONFIG_PATH_INFO path; + * const std::string friendly_name = get_friendly_name(path); + * ``` + */ + std::string + get_friendly_name(const DISPLAYCONFIG_PATH_INFO &path); + + /** + * @brief Get the logical display name for the path. + * + * These are the "\\\\.\\DISPLAY1", "\\\\.\\DISPLAY2" and etc. display names that can + * change whenever Windows wants to change them. + * + * @param path Path to get user display name for. + * @returns Display name for the path if available, empty string otherwise. + * @see query_display_config on how to get paths from the system. + * @note Inactive paths can have these names already assigned to them, even + * though they are not even in use! There can also be duplicates. + * + * EXAMPLES: + * ```cpp + * DISPLAYCONFIG_PATH_INFO path; + * const std::string display_name = get_display_name(path); + * ``` + */ + std::string + get_display_name(const DISPLAYCONFIG_PATH_INFO &path); + + /** + * @brief Get the HDR state the path. + * @param path Path to get HDR state for. + * @returns hdr_state_e::unknown if the state could not be retrieved, or other enum values describing the state otherwise. + * @see query_display_config on how to get paths from the system. + * @see hdr_state_e + * + * EXAMPLES: + * ```cpp + * DISPLAYCONFIG_PATH_INFO path; + * const auto hdr_state = get_hdr_state(path); + * ``` + */ + hdr_state_e + get_hdr_state(const DISPLAYCONFIG_PATH_INFO &path); + + /** + * @brief Set the HDR state for the path. + * @param path Path to set HDR state for. + * @param enable Specify whether to enable or disable HDR state. + * @returns True if new HDR state was set, false otherwise. + * @see query_display_config on how to get paths from the system. + * + * EXAMPLES: + * ```cpp + * DISPLAYCONFIG_PATH_INFO path; + * const bool success = set_hdr_state(path, false); + * ``` + */ + bool + set_hdr_state(const DISPLAYCONFIG_PATH_INFO &path, bool enable); + + /** + * @brief Get the source mode index from the path. + * + * It performs sanity checks on the modes list that the index is indeed correct. + * + * @param path Path to get the source mode index for. + * @param modes A list of various modes (source, target, desktop and probably more in the future). + * @returns Valid index value if it's found in the modes list and the mode at that index is of a type "source" mode, + * empty optional otherwise. + * @see query_display_config on how to get paths and modes from the system. + * + * EXAMPLES: + * ```cpp + * DISPLAYCONFIG_PATH_INFO path; + * std::vector modes; + * const auto source_index = get_source_index(path, modes); + * ``` + */ + boost::optional + get_source_index(const DISPLAYCONFIG_PATH_INFO &path, const std::vector &modes); + + /** + * @brief Set the source mode index in the path. + * @param path Path to modify. + * @param index Index value to set or empty optional to mark the index as invalid. + * @see query_display_config on how to get paths and modes from the system. + * + * EXAMPLES: + * ```cpp + * DISPLAYCONFIG_PATH_INFO path; + * set_source_index(path, 5); + * set_source_index(path, boost::none); + * ``` + */ + void + set_source_index(DISPLAYCONFIG_PATH_INFO &path, const boost::optional &index); + + /** + * @brief Set the target mode index in the path. + * @param path Path to modify. + * @param index Index value to set or empty optional to mark the index as invalid. + * @see query_display_config on how to get paths and modes from the system. + * + * EXAMPLES: + * ```cpp + * DISPLAYCONFIG_PATH_INFO path; + * set_target_index(path, 5); + * set_target_index(path, boost::none); + * ``` + */ + void + set_target_index(DISPLAYCONFIG_PATH_INFO &path, const boost::optional &index); + + /** + * @brief Set the desktop mode index in the path. + * @param path Path to modify. + * @param index Index value to set or empty optional to mark the index as invalid. + * @see query_display_config on how to get paths and modes from the system. + * + * EXAMPLES: + * ```cpp + * DISPLAYCONFIG_PATH_INFO path; + * set_desktop_index(path, 5); + * set_desktop_index(path, boost::none); + * ``` + */ + void + set_desktop_index(DISPLAYCONFIG_PATH_INFO &path, const boost::optional &index); + + /** + * @brief Set the clone group id in the path. + * @param path Path to modify. + * @param id Id value to set or empty optional to mark the id as invalid. + * @see query_display_config on how to get paths and modes from the system. + * + * EXAMPLES: + * ```cpp + * DISPLAYCONFIG_PATH_INFO path; + * set_clone_group_id(path, 5); + * set_clone_group_id(path, boost::none); + * ``` + */ + void + set_clone_group_id(DISPLAYCONFIG_PATH_INFO &path, const boost::optional &id); + + /** + * @brief Get the source mode from the list at the specified index. + * + * This function does additional sanity checks for the modes list and ensures + * that the mode at the specified index is indeed a source mode. + * + * @param index Index to get the mode for. It is of boost::optional type + * as the function is intended to be used with get_source_index function. + * @param modes List to get the mode from. + * @returns A pointer to a valid source mode from to list at the specified index, nullptr otherwise. + * @see query_display_config on how to get paths and modes from the system. + * @see get_source_index + * + * EXAMPLES: + * ```cpp + * DISPLAYCONFIG_PATH_INFO path; + * const std::vector modes; + * const DISPLAYCONFIG_SOURCE_MODE* source_mode = get_source_mode(get_source_index(path, modes), modes); + * ``` + */ + const DISPLAYCONFIG_SOURCE_MODE * + get_source_mode(const boost::optional &index, const std::vector &modes); + + /** + * @brief Get the source mode from the list at the specified index. + * + * This function does additional sanity checks for the modes list and ensures + * that the mode at the specified index is indeed a source mode. + * + * @param index Index to get the mode for. It is of boost::optional type + * as the function is intended to be used with get_source_index function. + * @param modes List to get the mode from. + * @returns A pointer to a valid source mode from to list at the specified index, nullptr otherwise. + * @see query_display_config on how to get paths and modes from the system. + * @see get_source_index + * + * EXAMPLES: + * ```cpp + * DISPLAYCONFIG_PATH_INFO path; + * std::vector modes; + * DISPLAYCONFIG_SOURCE_MODE* source_mode = get_source_mode(get_source_index(path, modes), modes); + * ``` + */ + DISPLAYCONFIG_SOURCE_MODE * + get_source_mode(const boost::optional &index, std::vector &modes); + + /** + * @brief Validate the path and get the commonly used information from it. + * + * This a convenience function to ensure that our concept of "valid path" remains the + * same throughout the code. + * + * Currently, for use, a valid path is: + * - a path with and available display target; + * - a path that is active (optional); + * - a path that has a non-empty device path; + * - a path that has a non-empty device id; + * - a path that has a non-empty device name assigned. + * + * @param path Path to validate and get info for. + * @param must_be_active Optionally request that the valid path must also be active. + * @returns Commonly used info for the path, or empty optional if the path is invalid. + * @see query_display_config on how to get paths and modes from the system. + * + * EXAMPLES: + * ```cpp + * DISPLAYCONFIG_PATH_INFO path; + * const auto device_info = get_device_info_for_valid_path(path, true); + * ``` + */ + boost::optional + get_device_info_for_valid_path(const DISPLAYCONFIG_PATH_INFO &path, bool must_be_active); + + /** + * @brief Query Windows for the device paths and associated modes. + * @param active_only Specify to query for active devices only. + * @returns Data containing paths and modes, empty optional if we have failed to query. + * + * EXAMPLES: + * ```cpp + * const auto display_data = query_display_config(true); + * ``` + */ + boost::optional + query_display_config(bool active_only); + + /** + * @brief Get the active path matching the device id. + * @param device_id Id to search for in the the list. + * @param paths List to be searched. + * @returns A pointer to an active path matching our id, nullptr otherwise. + * @see query_display_config on how to get paths and modes from the system. + * + * EXAMPLES: + * ```cpp + * const std::vector paths; + * const DISPLAYCONFIG_PATH_INFO* active_path = get_active_path("MY_DEVICE_ID", paths); + * ``` + */ + const DISPLAYCONFIG_PATH_INFO * + get_active_path(const std::string &device_id, const std::vector &paths); + + /** + * @brief Get the active path matching the device id. + * @param device_id Id to search for in the the list. + * @param paths List to be searched. + * @returns A pointer to an active path matching our id, nullptr otherwise. + * @see query_display_config on how to get paths and modes from the system. + * + * EXAMPLES: + * ```cpp + * std::vector paths; + * DISPLAYCONFIG_PATH_INFO* active_path = get_active_path("MY_DEVICE_ID", paths); + * ``` + */ + DISPLAYCONFIG_PATH_INFO * + get_active_path(const std::string &device_id, std::vector &paths); + + /** + * @brief Check whether the user session is locked. + * @returns True if it's definitely known that the session is locked, false otherwise. + * + * EXAMPLES: + * ```cpp + * const bool is_locked { is_user_session_locked() }; + * ``` + */ + bool + is_user_session_locked(); + + /** + * @brief Check whether it is already known that the CCD API will fail to set settings. + * @returns True if we already known we don't have access (for now), false otherwise. + * + * EXAMPLES: + * ```cpp + * const bool no_access { test_no_access_to_ccd_api() }; + * ``` + */ + bool + test_no_access_to_ccd_api(); + +} // namespace display_device::w_utils diff --git a/src/platform/windows/misc.cpp b/src/platform/windows/misc.cpp index a4190e01708..ad4593905f7 100644 --- a/src/platform/windows/misc.cpp +++ b/src/platform/windows/misc.cpp @@ -1075,7 +1075,7 @@ namespace platf { auto working_dir = boost::filesystem::path(); std::error_code ec; - auto child = run_command(false, false, url, working_dir, _env, nullptr, ec, nullptr); + auto child = run_command(false, true, "assets/gui/sunshine-gui.exe --url=" + url, working_dir, _env, nullptr, ec, nullptr); if (ec) { BOOST_LOG(warning) << "Couldn't open url ["sv << url << "]: System: "sv << ec.message(); } diff --git a/src/process.cpp b/src/process.cpp index 6ab6b31757b..ef28e627b32 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -23,6 +23,7 @@ #include "config.h" #include "crypto.h" +#include "display_device/session.h" #include "logging.h" #include "platform/common.h" #include "system_tray.h" @@ -98,7 +99,7 @@ namespace proc { } boost::filesystem::path - find_working_directory(const std::string &cmd, boost::process::environment &env) { + find_working_directory(const std::string &cmd) { // Parse the raw command string into parts to get the actual command portion #ifdef _WIN32 auto parts = boost::program_options::split_winmain(cmd); @@ -152,7 +153,8 @@ namespace proc { _app_prep_begin = std::begin(_app.prep_cmds); _app_prep_it = _app_prep_begin; - // Add Stream-specific environment variables + // Add Process-specific environment variables + _env = launch_session->env; _env["SUNSHINE_APP_ID"] = std::to_string(_app_id); _env["SUNSHINE_APP_NAME"] = _app.name; _env["SUNSHINE_CLIENT_WIDTH"] = std::to_string(launch_session->width); @@ -206,7 +208,7 @@ namespace proc { } boost::filesystem::path working_dir = _app.working_dir.empty() ? - find_working_directory(cmd.do_cmd, _env) : + find_working_directory(cmd.do_cmd) : boost::filesystem::path(_app.working_dir); BOOST_LOG(info) << "Executing Do Cmd: ["sv << cmd.do_cmd << ']'; auto child = platf::run_command(cmd.elevated, true, cmd.do_cmd, working_dir, _env, _pipe.get(), ec, nullptr); @@ -231,7 +233,7 @@ namespace proc { for (auto &cmd : _app.detached) { boost::filesystem::path working_dir = _app.working_dir.empty() ? - find_working_directory(cmd, _env) : + find_working_directory(cmd) : boost::filesystem::path(_app.working_dir); BOOST_LOG(info) << "Spawning ["sv << cmd << "] in ["sv << working_dir << ']'; auto child = platf::run_command(_app.elevated, true, cmd, working_dir, _env, _pipe.get(), ec, nullptr); @@ -249,7 +251,7 @@ namespace proc { } else { boost::filesystem::path working_dir = _app.working_dir.empty() ? - find_working_directory(_app.cmd, _env) : + find_working_directory(_app.cmd) : boost::filesystem::path(_app.working_dir); BOOST_LOG(info) << "Executing: ["sv << _app.cmd << "] in ["sv << working_dir << ']'; _process = platf::run_command(_app.elevated, true, _app.cmd, working_dir, _env, _pipe.get(), ec, &_process_group); @@ -312,7 +314,7 @@ namespace proc { } boost::filesystem::path working_dir = _app.working_dir.empty() ? - find_working_directory(cmd.undo_cmd, _env) : + find_working_directory(cmd.undo_cmd) : boost::filesystem::path(_app.working_dir); BOOST_LOG(info) << "Executing Undo Cmd: ["sv << cmd.undo_cmd << ']'; auto child = platf::run_command(cmd.elevated, true, cmd.undo_cmd, working_dir, _env, _pipe.get(), ec, nullptr); @@ -330,16 +332,19 @@ 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 + // Same applies when restoring display state + display_device::session_t::get().restore_state(); + } + _app_id = -1; } @@ -371,6 +376,30 @@ namespace proc { return _app.name; } + void + proc_t::run_menu_cmd(std::string cmd_id) { + auto iter = std::find_if(_app.menu_cmds.begin(), _app.menu_cmds.end(), [&cmd_id](const auto menu_cmd) { + return menu_cmd.id == cmd_id; + }); + + if (iter != _app.menu_cmds.end()) { + auto cmd = iter->do_cmd; + std::error_code ec; + + boost::filesystem::path working_dir = _app.working_dir.empty() ? + find_working_directory(cmd) : + boost::filesystem::path(_app.working_dir); + auto child = platf::run_command(iter->elevated, true, cmd, working_dir, _env, nullptr, ec, nullptr); + if (ec) { + BOOST_LOG(warning) << "Couldn't run cmd ["sv << cmd << "]: System: "sv << ec.message(); + } + else { + BOOST_LOG(info) << "Executing cmd ["sv << cmd << "]"sv; + child.detach(); + } + } + } + proc_t::~proc_t() { // It's not safe to call terminate() here because our proc_t is a static variable // that may be destroyed after the Boost loggers have been destroyed. Instead, @@ -592,6 +621,7 @@ namespace proc { proc::ctx_t ctx; auto prep_nodes_opt = app_node.get_child_optional("prep-cmd"s); + auto menu_nodes_opt = app_node.get_child_optional("menu-cmd"s); auto detached_nodes_opt = app_node.get_child_optional("detached"s); auto exclude_global_prep = app_node.get_optional("exclude-global-prep-cmd"s); auto output = app_node.get_optional("output"s); @@ -608,6 +638,10 @@ namespace proc { if (!exclude_global_prep.value_or(false)) { prep_cmds.reserve(config::sunshine.prep_cmds.size()); for (auto &prep_cmd : config::sunshine.prep_cmds) { + if (prep_cmd.on_session) { + continue; + } + auto do_cmd = parse_env_val(this_env, prep_cmd.do_cmd); auto undo_cmd = parse_env_val(this_env, prep_cmd.undo_cmd); @@ -634,6 +668,20 @@ namespace proc { } } + std::vector menu_cmds; + if (menu_nodes_opt) { + auto &menu_nodes = *menu_nodes_opt; + + menu_cmds.reserve(menu_nodes.size()); + for (auto &[_, menu_node] : menu_nodes) { + auto id = menu_node.get("id"s); + auto name = menu_node.get("name"s); + auto do_cmd = parse_env_val(this_env, menu_node.get("cmd"s)); + auto elevated = menu_node.get_optional("elevated"); + menu_cmds.emplace_back(std::move(id), std::move(name), std::move(do_cmd), std::move(elevated.value_or(false))); + } + } + std::vector detached; if (detached_nodes_opt) { auto &detached_nodes = *detached_nodes_opt; @@ -684,6 +732,7 @@ namespace proc { ctx.name = std::move(name); ctx.prep_cmds = std::move(prep_cmds); + ctx.menu_cmds = std::move(menu_cmds); ctx.detached = std::move(detached); apps.emplace_back(std::move(ctx)); diff --git a/src/process.h b/src/process.h index 29abaa88acc..a127808486e 100644 --- a/src/process.h +++ b/src/process.h @@ -22,6 +22,14 @@ namespace proc { using file_t = util::safe_ptr_v2; typedef config::prep_cmd_t cmd_t; + struct scmd_t { + scmd_t(std::string &&id, std::string &&name, std::string &&do_cmd, bool &&elevated): + id(std::move(id)), name(std::move(name)), do_cmd(std::move(do_cmd)), elevated(std::move(elevated)) {} + std::string id; + std::string name; + std::string do_cmd; + bool elevated; + }; /** * pre_cmds -- guaranteed to be executed unless any of the commands fail. * detached -- commands detached from Sunshine @@ -36,6 +44,7 @@ namespace proc { */ struct ctx_t { std::vector prep_cmds; + std::vector menu_cmds; /** * Some applications, such as Steam, either exit quickly, or keep running indefinitely. @@ -94,6 +103,8 @@ namespace proc { std::string get_last_run_app_name(); void + run_menu_cmd(std::string cmd_id); + void terminate(); private: diff --git a/src/rtsp.cpp b/src/rtsp.cpp index 3528c258115..a93d333b49e 100644 --- a/src/rtsp.cpp +++ b/src/rtsp.cpp @@ -530,6 +530,9 @@ namespace rtsp_stream { raised_timeout = now + config::stream.ping_timeout; --_slot_count; + + launch_session->env["SUNSHINE_CLIENT_SLOT"] = std::to_string(session_count()); + launch_event.raise(std::move(launch_session)); } diff --git a/src/rtsp.h b/src/rtsp.h index 910fc427eb4..d98bccf5a64 100644 --- a/src/rtsp.h +++ b/src/rtsp.h @@ -6,6 +6,8 @@ #include +#include + #include "crypto.h" #include "thread_safe.h" @@ -21,6 +23,8 @@ namespace rtsp_stream { std::string av_ping_payload; uint32_t control_connect_data; + boost::process::environment env; + bool host_audio; std::string unique_id; int width; diff --git a/src/stream.cpp b/src/stream.cpp index 0f6b946347f..b67c339932d 100644 --- a/src/stream.cpp +++ b/src/stream.cpp @@ -11,6 +11,9 @@ #include #include +#include +#include +#include extern "C" { // clang-format off @@ -20,6 +23,7 @@ extern "C" { } #include "config.h" +#include "display_device/session.h" #include "globals.h" #include "input.h" #include "logging.h" @@ -335,6 +339,8 @@ namespace stream { struct session_t { config_t config; + boost::process::environment env; + safe::mail_t mail; std::shared_ptr input; @@ -1901,6 +1907,7 @@ namespace stream { } namespace session { + typedef config::prep_cmd_t cmd_t; std::atomic_uint running_sessions; state_e @@ -1908,6 +1915,37 @@ namespace stream { return session.state.load(std::memory_order_relaxed); } + boost::filesystem::path + find_working_directory(const std::string &cmd) { + // Parse the raw command string into parts to get the actual command portion +#ifdef _WIN32 + auto parts = boost::program_options::split_winmain(cmd); +#else + auto parts = boost::program_options::split_unix(cmd); +#endif + if (parts.empty()) { + BOOST_LOG(error) << "Unable to parse command: "sv << cmd; + return boost::filesystem::path(); + } + + BOOST_LOG(debug) << "Parsed executable ["sv << parts.at(0) << "] from command ["sv << cmd << ']'; + + // If the cmd path is not an absolute path, resolve it using our PATH variable + boost::filesystem::path cmd_path(parts.at(0)); + if (!cmd_path.is_absolute()) { + cmd_path = boost::process::search_path(parts.at(0)); + if (cmd_path.empty()) { + BOOST_LOG(error) << "Unable to find executable ["sv << parts.at(0) << "]. Is it in your PATH?"sv; + return boost::filesystem::path(); + } + } + + BOOST_LOG(debug) << "Resolved executable ["sv << parts.at(0) << "] to path ["sv << cmd_path << ']'; + + // Now that we have a complete path, we can just use parent_path() + return cmd_path.parent_path(); + } + void stop(session_t &session) { while_starting_do_nothing(session.state); @@ -1917,6 +1955,32 @@ namespace stream { return; } + std::error_code ec; + std::vector::const_iterator prep_it = std::end(config::sunshine.prep_cmds); + for (; prep_it != std::begin(config::sunshine.prep_cmds); --prep_it) { + auto &cmd = *(prep_it - 1); + + // Skip empty and non session commands + if (cmd.undo_cmd.empty() || !cmd.on_session) { + continue; + } + + boost::filesystem::path working_dir = find_working_directory(cmd.do_cmd); + BOOST_LOG(info) << "Executing Undo Cmd: ["sv << cmd.undo_cmd << ']'; + auto child = platf::run_command(cmd.elevated, true, cmd.undo_cmd, working_dir, session.env, nullptr, ec, nullptr); + + if (ec) { + BOOST_LOG(warning) << "Couldn't run ["sv << cmd.undo_cmd << "]: "sv << ec.message(); + } + + child.wait(); + auto ret = child.exit_code(); + + if (ret != 0) { + BOOST_LOG(warning) << '[' << cmd.undo_cmd << "] failed with code ["sv << ret << ']'; + } + } + session.shutdown_event->raise(true); } @@ -1948,11 +2012,20 @@ namespace stream { // If this is the last session, invoke the platform callbacks if (--running_sessions == 0) { -#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 + bool restore_display_state { true }; if (proc::proc.running()) { +#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 system_tray::update_tray_pausing(proc::proc.get_last_run_app_name()); - } #endif + + // TODO: make this configurable per app + restore_display_state = false; + } + + if (restore_display_state) { + display_device::session_t::get().restore_state(); + } + platf::streaming_will_stop(); } @@ -1968,6 +2041,33 @@ namespace stream { return -1; } + std::error_code ec; + std::vector::const_iterator prep_it = std::begin(config::sunshine.prep_cmds); + for (; prep_it != std::end(config::sunshine.prep_cmds); ++prep_it) { + auto &cmd = *prep_it; + + // Skip empty and non session commands + if (cmd.do_cmd.empty() || !cmd.on_session) { + continue; + } + + boost::filesystem::path working_dir = find_working_directory(cmd.do_cmd); + BOOST_LOG(info) << "Executing Do Cmd: ["sv << cmd.do_cmd << ']'; + auto child = platf::run_command(cmd.elevated, true, cmd.do_cmd, working_dir, session.env, nullptr, ec, nullptr); + + if (ec) { + BOOST_LOG(error) << "Couldn't run ["sv << cmd.do_cmd << "]: System: "sv << ec.message(); + return -1; + } + + child.wait(); + auto ret = child.exit_code(); + if (ret != 0) { + BOOST_LOG(error) << '[' << cmd.do_cmd << "] failed with code ["sv << ret << ']'; + return -1; + } + } + session.control.expected_peer_address = addr_string; BOOST_LOG(debug) << "Expecting incoming session connections from "sv << addr_string; @@ -2012,6 +2112,7 @@ namespace stream { session->launch_session_id = launch_session.id; session->config = config; + session->env = launch_session.env; session->control.connect_data = launch_session.control_connect_data; session->control.feedback_queue = mail->queue(mail::gamepad_feedback); diff --git a/src/system_tray.cpp b/src/system_tray.cpp index e949ff57ac1..01160a61c2d 100644 --- a/src/system_tray.cpp +++ b/src/system_tray.cpp @@ -9,6 +9,7 @@ #define WIN32_LEAN_AND_MEAN #include #include + #include #define TRAY_ICON WEB_DIR "images/sunshine.ico" #define TRAY_ICON_PLAYING WEB_DIR "images/sunshine-playing.ico" #define TRAY_ICON_PAUSING WEB_DIR "images/sunshine-pausing.ico" @@ -87,11 +88,19 @@ namespace system_tray { BOOST_LOG(info) << "Quitting from system tray"sv; #ifdef _WIN32 - // If we're running in a service, return a special status to - // tell it to terminate too, otherwise it will just respawn us. - if (GetConsoleWindow() == NULL) { - lifetime::exit_sunshine(ERROR_SHUTDOWN_IN_PROGRESS, true); - return; + int msgboxID = MessageBoxW( + NULL, + L"你不能退出!\n那么想退吗? 真拿你没办法呢, 继续点一下吧~", + L"退你妹", + MB_ICONWARNING | MB_OKCANCEL); + + if (msgboxID == IDOK) { + // If we're running in a service, return a special status to + // tell it to terminate too, otherwise it will just respawn us. + if (GetConsoleWindow() == NULL) { + lifetime::exit_sunshine(ERROR_SHUTDOWN_IN_PROGRESS, true); + return; + } } #endif diff --git a/src/video.cpp b/src/video.cpp index 7758d68a9b5..22c59d64dae 100644 --- a/src/video.cpp +++ b/src/video.cpp @@ -18,6 +18,7 @@ extern "C" { #include "cbs.h" #include "config.h" +#include "display_device/display_device.h" #include "globals.h" #include "input.h" #include "logging.h" @@ -966,6 +967,8 @@ namespace video { */ void refresh_displays(platf::mem_type_e dev_type, std::vector &display_names, int ¤t_display_index) { + // It is possible that the output display name may be empty even if it wasn't before (device disconnected) + const auto output_display_name { display_device::get_display_name(config::video.output_name) }; std::string current_display_name; // If we have a current display index, let's start with that @@ -984,7 +987,7 @@ namespace video { return; } else if (display_names.empty()) { - display_names.emplace_back(config::video.output_name); + display_names.emplace_back(output_display_name); } // We now have a new display name list, so reset the index back to 0 @@ -1004,7 +1007,7 @@ namespace video { } else { for (int x = 0; x < display_names.size(); ++x) { - if (display_names[x] == config::video.output_name) { + if (display_names[x] == output_display_name) { current_display_index = x; return; } @@ -2307,7 +2310,8 @@ namespace video { config_t config_autoselect { 1920, 1080, 60, 1000, 1, 0, 1, 0, 0 }; // If the encoder isn't supported at all (not even H.264), bail early - reset_display(disp, encoder.platform_formats->dev_type, config::video.output_name, config_autoselect); + const auto output_display_name { display_device::get_display_name(config::video.output_name) }; + reset_display(disp, encoder.platform_formats->dev_type, output_display_name, config_autoselect); if (!disp) { return false; } @@ -2437,7 +2441,7 @@ namespace video { av1.videoFormat = 2; // Reset the display since we're switching from SDR to HDR - reset_display(disp, encoder.platform_formats->dev_type, config::video.output_name, config); + reset_display(disp, encoder.platform_formats->dev_type, output_display_name, config); if (!disp) { return false; } diff --git a/src_assets/common/assets/web/Locale.vue b/src_assets/common/assets/web/Locale.vue new file mode 100644 index 00000000000..7359d7d80c9 --- /dev/null +++ b/src_assets/common/assets/web/Locale.vue @@ -0,0 +1,56 @@ + + + diff --git a/src_assets/common/assets/web/Navbar.vue b/src_assets/common/assets/web/Navbar.vue index 838c630f45a..1dfbe1918b0 100644 --- a/src_assets/common/assets/web/Navbar.vue +++ b/src_assets/common/assets/web/Navbar.vue @@ -48,9 +48,6 @@ export default { mounted() { let el = document.querySelector("a[href='" + document.location.pathname + "']"); if (el) el.classList.add("active") - let discordWidget = document.createElement('script') - discordWidget.setAttribute('src', 'https://app.lizardbyte.dev/js/discord.js') - document.head.appendChild(discordWidget) } } @@ -86,4 +83,18 @@ export default { .form-control::placeholder { opacity: 0.5; } + +body { + background: url(https://raw.gitmirror.com/qiin2333/qiin.github.io/assets/img/sunshine-bg0.webp) no-repeat; + background-color: #5496dd; + background-size: cover; + background-attachment: fixed; +} + +[data-bs-theme=light] { + --bs-body-bg: rgba(255, 255, 255, .3); +} +[data-bs-theme=dark] { + --bs-body-bg: rgba(0, 0, 0, .65); +} diff --git a/src_assets/common/assets/web/ResourceCard.vue b/src_assets/common/assets/web/ResourceCard.vue index aee837689cf..645582a909a 100644 --- a/src_assets/common/assets/web/ResourceCard.vue +++ b/src_assets/common/assets/web/ResourceCard.vue @@ -1,33 +1,75 @@ diff --git a/src_assets/common/assets/web/apps.html b/src_assets/common/assets/web/apps.html index 9a8c65d16ae..7931a6eb7e4 100644 --- a/src_assets/common/assets/web/apps.html +++ b/src_assets/common/assets/web/apps.html @@ -171,6 +171,56 @@

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

+ + +
+ +
配置后在客户端返回菜单中可见,用于在不打断串流的情况下快速执行特定操作,例如调出辅助程序。
+
示例:展示名称-关闭你的颠佬;指令-shutdown -s -t 10
+
+ +
+ + + + + + + + + + + + + + + + + +
展示名称 指令 + {{ $t('_common.run_as') }} +
+ + + + +
+ + +
+
+ + +
+
+
@@ -359,6 +409,7 @@

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

import { initApp } from './init' import Navbar from './Navbar.vue' import { Dropdown } from 'bootstrap/dist/js/bootstrap' + import { nanoid } from 'nanoid' const app = createApp({ components: { @@ -401,6 +452,7 @@

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

"wait-all": true, "exit-timeout": 5, "prep-cmd": [], + "menu-cmd": [], detached: [], "image-path": "" }; @@ -410,12 +462,10 @@

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

editApp(id) { this.editForm = JSON.parse(JSON.stringify(this.apps[id])); this.editForm.index = id; - if (this.editForm["prep-cmd"] === undefined) - this.editForm["prep-cmd"] = []; - if (this.editForm["detached"] === undefined) - this.editForm["detached"] = []; - if (this.editForm["exclude-global-prep-cmd"] === undefined) - this.editForm["exclude-global-prep-cmd"] = []; + if (!this.editForm["prep-cmd"]) this.editForm["prep-cmd"] = []; + if (!this.editForm["menu-cmd"]) this.editForm["menu-cmd"] = []; + if (!this.editForm["detached"]) this.editForm["detached"] = []; + if (!this.editForm["exclude-global-prep-cmd"]) this.editForm["exclude-global-prep-cmd"] = []; if (this.editForm["elevated"] === undefined && this.platform === 'windows') { this.editForm["elevated"] = []; } @@ -452,6 +502,19 @@

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

this.editForm["prep-cmd"].push(template); }, + addMenuCmd() { + let template = { + id: nanoid(10), + name: "", + cmd: "" + }; + + if (this.platform === 'windows') { + template = { ...template, elevated: false }; + } + + this.editForm["menu-cmd"].push(template); + }, showCoverFinder($event) { this.coverCandidates = []; this.coverSearching = true; diff --git a/src_assets/common/assets/web/config.html b/src_assets/common/assets/web/config.html index 8c2bce2b32f..baedb8ad5f0 100644 --- a/src_assets/common/assets/web/config.html +++ b/src_assets/common/assets/web/config.html @@ -6,13 +6,20 @@ @@ -20,7 +27,7 @@

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

-
+