diff --git a/.clang-format b/.clang-format index 97bdef1e1ef..e72a1e27d11 100644 --- a/.clang-format +++ b/.clang-format @@ -52,7 +52,7 @@ NamespaceIndentation: All ObjCSpaceAfterProperty: true ObjCSpaceBeforeProtocolList: true PointerAlignment: Right -ReflowComments: false +ReflowComments: true SpaceAfterCStyleCast: true SpaceAfterLogicalNot: false SpaceAfterTemplateKeyword: true diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index d25c409123f..d701774ed37 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -102,7 +102,6 @@ body: - Linux - solus (Third Party) - macOS - dmg - macOS - Portfile - - macOS - pkg - Windows - Chocolatey (Third Party) - Windows - installer - Windows - portable @@ -111,6 +110,7 @@ body: - other (not listed) - other (self built) - other (fork of this repo) + - n/a validations: required: true - type: dropdown @@ -123,6 +123,7 @@ body: - Intel - Nvidia - none (software encoding) + - n/a validations: required: true - type: input diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 234fb791d39..a2d7f99fb44 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -474,7 +474,7 @@ jobs: build_mac: name: MacOS runs-on: macos-11 - needs: setup_release + needs: [check_changelog, setup_release] steps: - name: Checkout @@ -514,13 +514,10 @@ jobs: # package cpack -G DragNDrop - mv ./cpack_artifacts/Sunshine.dmg ../artifacts/sunshine-macos-experimental-dragndrop.dmg - - cpack -G Bundle - mv ./cpack_artifacts/Sunshine.dmg ../artifacts/sunshine-macos-experimental-bundle.dmg + mv ./cpack_artifacts/Sunshine.dmg ../artifacts/sunshine.dmg - cpack -G ZIP - mv ./cpack_artifacts/Sunshine.zip ../artifacts/sunshine-macos-experimental-archive.zip + # cpack -G Bundle + # mv ./cpack_artifacts/Sunshine.dmg ../artifacts/sunshine-bundle.dmg - name: Upload Artifacts uses: actions/upload-artifact@v3 @@ -528,33 +525,23 @@ jobs: name: sunshine-macos path: artifacts/ - # this step can be removed after packages are fixed - - name: Delete experimental packages - if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }} - working-directory: artifacts - run: | - rm -f ./sunshine-macos-experimental-dragndrop.dmg - rm -f ./sunshine-macos-experimental-bundle.dmg - rm -f ./sunshine-macos-experimental-archive.zip - -# # no artifacts to release currently -# - name: Create/Update GitHub Release -# if: ${{ needs.setup_release.outputs.create_release == 'true' }} -# uses: ncipollo/release-action@v1 -# with: -# name: ${{ needs.setup_release.outputs.release_name }} -# tag: ${{ needs.setup_release.outputs.release_tag }} -# commit: ${{ needs.setup_release.outputs.release_commit }} -# artifacts: "*artifacts/*" -# token: ${{ secrets.GH_BOT_TOKEN }} -# allowUpdates: true -# body: ${{ needs.setup_release.outputs.release_body }} -# discussionCategory: announcements -# prerelease: ${{ needs.setup_release.outputs.pre_release }} + - name: Create/Update GitHub Release + if: ${{ needs.setup_release.outputs.create_release == 'true' }} + uses: ncipollo/release-action@v1 + with: + name: ${{ needs.setup_release.outputs.release_name }} + tag: ${{ needs.setup_release.outputs.release_tag }} + commit: ${{ needs.setup_release.outputs.release_commit }} + artifacts: "*artifacts/*" + token: ${{ secrets.GH_BOT_TOKEN }} + allowUpdates: true + body: ${{ needs.setup_release.outputs.release_body }} + discussionCategory: announcements + prerelease: ${{ needs.setup_release.outputs.pre_release }} build_mac_port: name: Macports - needs: setup_release + needs: [check_changelog, setup_release] runs-on: macos-11 steps: @@ -728,25 +715,6 @@ jobs: done exit "$fail" - - name: Package - run: | - # create packages - sudo port pkg sunshine - sudo port dmg sunshine - - work=$(port work sunshine) - echo "Sunshine port work directory: ${work}" - - # move components out of port work directory - sudo mv ${work}/Sunshine*component.pkg /tmp/ - - # copy artifacts - sudo mv ${work}/Sunshine*.pkg ./artifacts/sunshine.pkg - sudo mv ${work}/Sunshine*.dmg ./artifacts/sunshine.dmg - - # move components back - # sudo mv /tmp/Sunshine*component.pkg ${work}/ - - name: Upload Artifacts uses: actions/upload-artifact@v3 with: @@ -770,7 +738,7 @@ jobs: build_win: name: Windows runs-on: windows-2019 - needs: setup_release + needs: [check_changelog, setup_release] steps: - name: Checkout @@ -813,7 +781,7 @@ jobs: run: | mkdir build cd build - cmake -DCMAKE_BUILD_TYPE=Release \ + cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo \ -DSUNSHINE_ASSETS_DIR=assets \ -G "MinGW Makefiles" \ .. @@ -833,6 +801,15 @@ jobs: mv ./cpack_artifacts/Sunshine.exe ../artifacts/sunshine-windows-installer.exe mv ./cpack_artifacts/Sunshine.zip ../artifacts/sunshine-windows-portable.zip + - name: Package Windows Debug Info + working-directory: build + run: | + # save the original binaries with debug info + 7z -r ` + "-xr!CMakeFiles" ` + "-xr!cpack_artifacts" ` + a "../artifacts/sunshine-debuginfo-win32.zip" "*.exe" + - name: Upload Artifacts uses: actions/upload-artifact@v3 with: diff --git a/.github/workflows/localize.yml b/.github/workflows/localize.yml index 2c57c114e76..d2a236c54e5 100644 --- a/.github/workflows/localize.yml +++ b/.github/workflows/localize.yml @@ -77,7 +77,7 @@ jobs: run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT - name: Create/Update Pull Request - uses: peter-evans/create-pull-request@v4 + uses: peter-evans/create-pull-request@v5 with: add-paths: | locale/*.po diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 75ec50a72e3..3a0f243378b 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -8,26 +8,16 @@ version: 2 # Set the version of Python build: - os: ubuntu-20.04 + os: ubuntu-22.04 tools: - python: "3.10" - -## apt packages required packages to run cmake on sunshine, note that additional packages are required -# apt_packages: -# - cmake -# - libboost-filesystem-dev -# - libboost-log-dev -# - libboost-thread-dev - -## run cmake -# jobs: -# pre_build: -# - cmake . - -## Include the submodules, required for cmake -# submodules: -# include: all -# recursive: true + python: "3.11" + apt_packages: + - graphviz + +# submodules required for include statements +submodules: + include: all + recursive: true # Build documentation in the docs/ directory with Sphinx sphinx: diff --git a/CHANGELOG.md b/CHANGELOG.md index c074a75e274..419e16025e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,85 @@ # Changelog +## [0.20.0] - 2023-05-28 +**Breaking** +- (Windows) The Windows installer version of Sunshine is now always launched by the Sunshine Service. Manually launching Sunshine.exe from Program Files is no longer supported. This was necessary to address security issues caused by non-admin users having access to Sunshine's config data. If you have set up Task Scheduler or other mechanisms to launch Sunshine automatically, remove those from your system before updating. +- (Windows) The Start Menu shortcut has been redesigned for use with the Sunshine Service. It now launches Sunshine in the background (if not already running) and opens the Web UI, avoiding the persistent Command Prompt window present in prior versions. The Start Menu shortcut is now the recommended method for opening the Web UI and launching Sunshine. +- (Network/UPnP) If the Moonlight Internet Hosting Tool is installed alongside Sunshine, you must remove it or upgrade to v5.6 or later to prevent conflicts with Sunshine's UPnP support. As a reminder, the Moonlight Internet Hosting Tool is not required to stream over the Internet with Sunshine. Instead, simply enable UPnP in the Sunshine Web UI. +- (Windows) If Steam is installed, the Steam Streaming Speakers driver will be automatically installed when starting a stream for the first time. This behavior can be disabled in the Audio/Video tab of the Web UI. This Steam driver enables support for surround sound and muting host audio without requiring any manual configuration. +- (Input) The Back Button Timeout option has been renamed to Guide Button Emulation Timeout and has been disabled by default to ensure long presses on the Back button work by default. The previous behavior can be restored by setting the Guide Button Emulation Timeout to 2000. +- (Windows) The service name of SunshineSvc has been changed to SunshineService to address a false positive in MalwareBytes. If you have any scripts or other logic on your system that is using the service name, you will need to update that for the new name. +- (Windows) To support new installer features, install-service.bat no longer sets the service to auto-start by default. Users executing install-service.bat manually on the Sunshine portable build must also execute autostart-service.bat to start Sunshine on boot. However, installing the service manually like this is not recommended. Instead, use the Sunshine installer which handles service installation and configuration automatically. +- (Linux/Fedora) Fedora 36 builds are removed due to upstream end of support + +**Added** +- (Windows) Added an option to launch apps and prep/undo commands as administrator +- (Installer/Windows) Added an option to choose whether Sunshine launches on boot. If not configured to launch on boot, use the Start Menu shortcut to start Sunshine when desired. +- (Input/Windows) Added option to send VK codes instead of scancodes for compatibility with iOS/Android devices using non-English keyboard layouts +- (UI) The Apply/Restart option is now available in the Web UI for all platforms +- (Systray) Added a Restart option to the system tray context menu +- (Video/Windows) Added host latency data to video frames. This requires future updates to Moonlight to display in the on-screen overlay. +- (Audio) Added support for matching Audio Sink and Virtual Sink values on device names +- (Client) Added friendly error messages for clients when streaming fails to start +- (Video/Windows) Added warning log messages when Windows is hiding DRM-protected content from display capture +- (Interop/Windows) Added warning log messages when GeForce Experience is currently using the same ports as Sunshine +- (Linux/Fedora) Fedora 38 builds are now available + +**Changed** +- (Video) Encoder selection now happens at each stream start for more reliable GPU detection +- (Video/Windows) The host display now stays on while clients are actively streaming +- (Audio) Streaming will no longer fail if audio capture is unavailable +- (Audio/Windows) Sunshine will automatically switch back to the Virtual Sink if the default audio device is changed while streaming +- (Audio) Changes to the host audio playback option will now take effect when resuming a session from Moonlight +- (Audio/Windows) Sunshine will switch to a different default audio device if Steam Streaming Speakers are the default when Sunshine starts. This handles cases where Sunshine terminates unexpectedly without restoring the default audio device. +- (Apps) The Connection Terminated dialog will no longer appear in Moonlight when the app on the host exits normally +- (Systray/Windows) Quitting Sunshine via the system tray will now stop the Sunshine Service rather than leaving it running and allowing it to restart Sunshine +- (UI) The 100.64.0.0/10 CGN IP address range is now treated as a LAN address range +- (Video) Removed a workaround for some versions of Moonlight prior to mid-2022 +- (UI) The PIN field is now cleared after successfully pairing +- (UI) User names are now treated as case-insensitive +- (Linux) Changed udev rule to automatically grant access to virtual input devices +- (UI) Several item descriptions were adjusted to reflect newer configuration recommendations +- (UI) Placeholder text opacity has been reduced to improve contrast with non-placeholder text +- (Video/Windows) Minor capture performance improvements + +**Fixed** +- (Video) VRAM usage while streaming is significantly reduced, particularly with higher display resolutions and HDR +- (Network/UPnP) UPnP support was rewritten to fix several major issues handling router restarts, IP address changes, and port forwarding expiration +- (Input) Fixed modifier keys from the software keyboard on Android clients +- (Audio) Fixed handling of default audio device changes while streaming +- (Windows) Fixed streaming after Microsoft Remote Desktop or Fast User Switching has been used +- (Input) Fixed some games not recognizing emulated Guide button presses +- (Video/Windows) Fixed incorrect gamma when using an Advanced Color SDR display on the host +- (Installer/Windows) The installer is no longer blurry on High DPI systems +- (Systray/Windows) Fixed multiple system tray icons appearing if Sunshine exits unexpectedly +- (Systray/Windows) Fixed the system tray icon not appearing in several situations +- (Windows) Fixed hang on shutdown/restart if mDNS registration fails +- (UI) Fixed missing response headers +- (Stability) Fixed several possible crashes in cases where the client did not successfully connect +- (Stability) Fixed several memory leaks +- (Input/Windows) Back/Select input now correctly generates the Share button when emulating DS4 controllers +- (Audio/Windows) Fixed various bugs in audio-info.exe that led to inaccurate output on some systems +- (Video/Windows) Fixed incorrect resolution values from dxgi-info.exe on High DPI systems +- (Video/Linux) Fixed poor quality encoding from H.264 on Intel encoders +- (Config) Fixed a couple of typos in predefined resolutions + +**Dependencies** +- Bump sphinx-copybutton from 0.5.1 to 0.5.2 +- Bump sphinx from 6.13 to 7.0.1 +- Bump third-party/nv-codec-headers from 2055784 to 2cd175b +- Bump furo from 2023.3.27 to 2023.5.20 + +**Misc** +- (Build/Linux) Add X11 to PLATFORM_LIBARIES when found +- (Build/macOS) Fix compilation with Clang 14 +- (Docs) Fix nvlax URL +- (Docs) Add diagrams using graphviz +- (Docs) Improvements to source code documentation +- (Build) Unpin docker dependencies +- (Build/Linux) Honor install prefix for tray icon +- (Build/Windows) Unstripped binaries are now provided as a debuginfo package to support crash dump debugging +- (Config) Config directories are now created recursively + ## [0.19.1] - 2023-03-30 **Fixed** - (Audio) Fixed no audio issue introduced in v0.19.0 @@ -409,3 +489,4 @@ settings. In v0.17.0, games now run under your user account without elevated pri [0.18.4]: https://github.com/LizardByte/Sunshine/releases/tag/v0.18.4 [0.19.0]: https://github.com/LizardByte/Sunshine/releases/tag/v0.19.0 [0.19.1]: https://github.com/LizardByte/Sunshine/releases/tag/v0.19.1 +[0.20.0]: https://github.com/LizardByte/Sunshine/releases/tag/v0.20.0 diff --git a/CMakeLists.txt b/CMakeLists.txt index 641b2712b6f..ccca6fcec60 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.18) # `CMAKE_CUDA_ARCHITECTURES` requires 3.18 # todo - set version to 0.0.0 once confident in automated versioning -project(Sunshine VERSION 0.19.1 +project(Sunshine VERSION 0.20.0 DESCRIPTION "Sunshine is a self-hosted game stream host for Moonlight." HOMEPAGE_URL "https://app.lizardbyte.dev") @@ -125,7 +125,7 @@ set(UPNPC_BUILD_TESTS OFF CACHE BOOL "Don't build tests for miniupnpc") set(UPNPC_BUILD_SAMPLE OFF CACHE BOOL "Don't build samples for miniupnpc") set(UPNPC_NO_INSTALL ON CACHE BOOL "Don't install any libraries build for miniupnpc") add_subdirectory(third-party/miniupnp/miniupnpc) -include_directories(third-party/miniupnp/miniupnpc/include) +include_directories(SYSTEM third-party/miniupnp/miniupnpc/include) find_package(Threads REQUIRED) find_package(OpenSSL REQUIRED) @@ -134,13 +134,11 @@ pkg_check_modules(CURL REQUIRED libcurl) if(WIN32) set(Boost_USE_STATIC_LIBS ON) # cmake-lint: disable=C0103 - # workaround to prevent link errors against icudata, icui18n - set(Boost_NO_BOOST_CMAKE ON) # cmake-lint: disable=C0103 endif() find_package(Boost COMPONENTS locale log filesystem program_options REQUIRED) -list(APPEND SUNSHINE_COMPILE_OPTIONS -Wall -Wno-missing-braces -Wno-maybe-uninitialized -Wno-sign-compare) +list(APPEND SUNSHINE_COMPILE_OPTIONS -Wall -Wno-sign-compare) # enable system tray, we will disable this later if we cannot find the required package config on linux set(SUNSHINE_TRAY 1) @@ -151,13 +149,13 @@ if(WIN32) set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static") add_definitions(-DCURL_STATICLIB) - include_directories(${CURL_STATIC_INCLUDE_DIRS}) + include_directories(SYSTEM ${CURL_STATIC_INCLUDE_DIRS}) link_directories(${CURL_STATIC_LIBRARY_DIRS}) add_compile_definitions(SUNSHINE_PLATFORM="windows") add_subdirectory(tools) # This is temporary, only tools for Windows are needed, for now - include_directories(third-party/ViGEmClient/include) + include_directories(SYSTEM third-party/ViGEmClient/include) if(NOT DEFINED SUNSHINE_ICON_PATH) set(SUNSHINE_ICON_PATH "${CMAKE_CURRENT_SOURCE_DIR}/sunshine.ico") @@ -353,12 +351,13 @@ else() if(X11_FOUND) add_compile_definitions(SUNSHINE_BUILD_X11) - include_directories(${X11_INCLUDE_DIR}) + include_directories(SYSTEM ${X11_INCLUDE_DIR}) + list(APPEND PLATFORM_LIBRARIES ${X11_LIBRARIES}) list(APPEND PLATFORM_TARGET_FILES src/platform/linux/x11grab.cpp) endif() if(CUDA_FOUND) - include_directories(third-party/nvfbc) + include_directories(SYSTEM third-party/nvfbc) list(APPEND PLATFORM_TARGET_FILES src/platform/linux/cuda.cu src/platform/linux/cuda.cpp @@ -369,7 +368,7 @@ else() if(LIBDRM_FOUND AND LIBCAP_FOUND) add_compile_definitions(SUNSHINE_BUILD_DRM) - include_directories(${LIBDRM_INCLUDE_DIRS} ${LIBCAP_INCLUDE_DIRS}) + include_directories(SYSTEM ${LIBDRM_INCLUDE_DIRS} ${LIBCAP_INCLUDE_DIRS}) list(APPEND PLATFORM_LIBRARIES ${LIBDRM_LIBRARIES} ${LIBCAP_LIBRARIES}) list(APPEND PLATFORM_TARGET_FILES src/platform/linux/kmsgrab.cpp) list(APPEND SUNSHINE_DEFINITIONS EGL_NO_X11=1) @@ -415,6 +414,7 @@ ${CMAKE_BINARY_DIR}/generated-src/${filename}.h") GEN_WAYLAND(wlr-export-dmabuf-unstable-v1) include_directories( + SYSTEM ${WAYLAND_INCLUDE_DIRS} ${CMAKE_BINARY_DIR}/generated-src ) @@ -435,7 +435,7 @@ ${CMAKE_BINARY_DIR}/generated-src/${filename}.h") message(WARNING "Couldn't find appindicator, disabling tray icon") set(SUNSHINE_TRAY 0) else() - include_directories(${APPINDICATOR_INCLUDE_DIRS}) + include_directories(SYSTEM ${APPINDICATOR_INCLUDE_DIRS}) link_directories(${APPINDICATOR_LIBRARY_DIRS}) list(APPEND PLATFORM_TARGET_FILES third-party/tray/tray_linux.c) @@ -474,6 +474,7 @@ ${CMAKE_BINARY_DIR}/generated-src/${filename}.h") pulse-simple) include_directories( + SYSTEM /usr/include/libevdev-1.0 third-party/nv-codec-headers/include third-party/glad/include) @@ -535,6 +536,8 @@ set(SUNSHINE_TARGET_FILES src/thread_safe.h src/sync.h src/round_robin.h + src/stat_trackers.h + src/stat_trackers.cpp ${PLATFORM_TARGET_FILES}) set_source_files_properties(src/upnp.cpp PROPERTIES COMPILE_FLAGS -Wno-pedantic) @@ -582,8 +585,10 @@ set(FFMPEG_LIBRARIES ${HDR10_PLUS_LIBRARY} ${FFMPEG_PLATFORM_LIBRARIES}) +include_directories(${CMAKE_CURRENT_SOURCE_DIR}) + include_directories( - ${CMAKE_CURRENT_SOURCE_DIR} + SYSTEM ${CMAKE_CURRENT_SOURCE_DIR}/third-party ${CMAKE_CURRENT_SOURCE_DIR}/third-party/moonlight-common-c/enet/include ${CMAKE_CURRENT_SOURCE_DIR}/third-party/nanors @@ -638,6 +643,8 @@ if(WIN32) set_target_properties(sunshine PROPERTIES LINK_SEARCH_START_STATIC 1) set(CMAKE_FIND_LIBRARY_SUFFIXES ".dll") find_library(ZLIB ZLIB1) + list(APPEND SUNSHINE_EXTERNAL_LIBRARIES + Wtsapi32.lib) endif() target_link_libraries(sunshine ${SUNSHINE_EXTERNAL_LIBRARIES} ${EXTRA_LIBS}) @@ -692,25 +699,31 @@ if(WIN32) # see options at: https://cmake.org/cmake/help/latest/cpack_gen/nsis.h # Adding tools install(TARGETS dxgi-info RUNTIME DESTINATION "tools" COMPONENT dxgi) install(TARGETS audio-info RUNTIME DESTINATION "tools" COMPONENT audio) - install(TARGETS sunshinesvc RUNTIME DESTINATION "tools" COMPONENT sunshinesvc) - install(TARGETS elevator RUNTIME DESTINATION "tools" COMPONENT elevator) # Mandatory tools install(TARGETS ddprobe RUNTIME DESTINATION "tools" COMPONENT application) + install(TARGETS sunshinesvc RUNTIME DESTINATION "tools" COMPONENT application) + + # Mandatory scripts + install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/windows/misc/service/" + DESTINATION "scripts" + COMPONENT assets) + install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/windows/misc/migration/" + DESTINATION "scripts" + COMPONENT assets) + + # Configurable options for the service + install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/windows/misc/autostart/" + DESTINATION "scripts" + COMPONENT autostart) # scripts install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/windows/misc/firewall/" DESTINATION "scripts" COMPONENT firewall) - install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/windows/misc/service/" - DESTINATION "scripts" - COMPONENT service) install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/windows/misc/vigembus/" DESTINATION "scripts" COMPONENT vigembus) - install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/windows/misc/migration/" - DESTINATION "scripts" - COMPONENT assets) # Sunshine assets install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/common/assets/" @@ -729,7 +742,6 @@ if(WIN32) # see options at: https://cmake.org/cmake/help/latest/cpack_gen/nsis.h # Extra install commands # Restores permissions on the install directory # Migrates config files from the root into the new config folder - # Sets permissions on the config folder so that we can write in it # Install service SET(CPACK_NSIS_EXTRA_INSTALL_COMMANDS "${CPACK_NSIS_EXTRA_INSTALL_COMMANDS} @@ -737,10 +749,10 @@ if(WIN32) # see options at: https://cmake.org/cmake/help/latest/cpack_gen/nsis.h ExecShell 'open' 'https://sunshinestream.readthedocs.io/' nsExec::ExecToLog 'icacls \\\"$INSTDIR\\\" /reset' nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\migrate-config.bat\\\"' - nsExec::ExecToLog 'icacls \\\"$INSTDIR\\\\config\\\" /grant:r Users:\\\(OI\\\)\\\(CI\\\)\\\(F\\\)' nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\add-firewall-rule.bat\\\"' nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\install-service.bat\\\"' nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\install-vigembus.bat\\\"' + nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\autostart-service.bat\\\"' NoController: ") @@ -762,20 +774,19 @@ if(WIN32) # see options at: https://cmake.org/cmake/help/latest/cpack_gen/nsis.h NoDelete: ") - # Adding an option for the start menu and PATH - # TODO: it asks to add it to the PATH but is not working https://gitlab.kitware.com/cmake/cmake/-/issues/15635 + # Adding an option for the start menu set(CPACK_NSIS_MODIFY_PATH "OFF") set(CPACK_NSIS_EXECUTABLES_DIRECTORY ".") # This will be shown on the installed apps Windows settings set(CPACK_NSIS_INSTALLED_ICON_NAME "${CMAKE_PROJECT_NAME}.exe") set(CPACK_NSIS_CREATE_ICONS_EXTRA "${CPACK_NSIS_CREATE_ICONS_EXTRA} - CreateShortCut '\$SMPROGRAMS\\\\$STARTMENU_FOLDER\\\\${CMAKE_PROJECT_NAME} (Foreground Mode).lnk' \ - '\$INSTDIR\\\\${CMAKE_PROJECT_NAME}.exe' + CreateShortCut '\$SMPROGRAMS\\\\$STARTMENU_FOLDER\\\\${CMAKE_PROJECT_NAME}.lnk' \ + '\$INSTDIR\\\\${CMAKE_PROJECT_NAME}.exe' '--shortcut' ") set(CPACK_NSIS_DELETE_ICONS_EXTRA "${CPACK_NSIS_DELETE_ICONS_EXTRA} - Delete '\$SMPROGRAMS\\\\$MUI_TEMP\\\\${CMAKE_PROJECT_NAME} (Foreground Mode).lnk' + Delete '\$SMPROGRAMS\\\\$MUI_TEMP\\\\${CMAKE_PROJECT_NAME}.lnk' ") # Checking for previous installed versions @@ -789,57 +800,48 @@ if(WIN32) # see options at: https://cmake.org/cmake/help/latest/cpack_gen/nsis.h "https://sunshinestream.readthedocs.io" "Sunshine documentation" "https://app.lizardbyte.dev" "LizardByte Web Site" "https://app.lizardbyte.dev/support" "LizardByte Support") + set(CPACK_NSIS_MANIFEST_DPI_AWARE true) # Setting components groups and dependencies + set(CPACK_COMPONENT_GROUP_CORE_EXPANDED true) + # sunshine binary set(CPACK_COMPONENT_APPLICATION_DISPLAY_NAME "${CMAKE_PROJECT_NAME}") - set(CPACK_COMPONENT_APPLICATION_DESCRIPTION "${CMAKE_PROJECT_NAME} main application.") - set(CPACK_COMPONENT_APPLICATION_GROUP "core") + set(CPACK_COMPONENT_APPLICATION_DESCRIPTION "${CMAKE_PROJECT_NAME} main application and required components.") + set(CPACK_COMPONENT_APPLICATION_GROUP "Core") set(CPACK_COMPONENT_APPLICATION_REQUIRED true) set(CPACK_COMPONENT_APPLICATION_DEPENDS assets) + # service auto-start script + set(CPACK_COMPONENT_AUTOSTART_DISPLAY_NAME "Launch on Startup") + set(CPACK_COMPONENT_AUTOSTART_DESCRIPTION "If enabled, launches Sunshine automatically on system startup.") + set(CPACK_COMPONENT_AUTOSTART_GROUP "Core") + # assets - set(CPACK_COMPONENT_ASSETS_DISPLAY_NAME "assets") - set(CPACK_COMPONENT_ASSETS_DESCRIPTION "Shaders, default box art, and web ui.") - set(CPACK_COMPONENT_ASSETS_GROUP "core") + set(CPACK_COMPONENT_ASSETS_DISPLAY_NAME "Required Assets") + set(CPACK_COMPONENT_ASSETS_DESCRIPTION "Shaders, default box art, and web UI.") + set(CPACK_COMPONENT_ASSETS_GROUP "Core") set(CPACK_COMPONENT_ASSETS_REQUIRED true) # audio tool set(CPACK_COMPONENT_AUDIO_DISPLAY_NAME "audio-info") set(CPACK_COMPONENT_AUDIO_DESCRIPTION "CLI tool providing information about sound devices.") - set(CPACK_COMPONENT_AUDIO_GROUP "tools") - - # elevation tool - set(CPACK_COMPONENT_ELEVATOR_DISPLAY_NAME "elevator") - set(CPACK_COMPONENT_ELEVATOR_DESCRIPTION "CLI tool that assists with elevating \ - commands when permissions have been denied.") - set(CPACK_COMPONENT_ELEVATOR_GROUP "tools") + set(CPACK_COMPONENT_AUDIO_GROUP "Tools") # display tool set(CPACK_COMPONENT_DXGI_DISPLAY_NAME "dxgi-info") set(CPACK_COMPONENT_DXGI_DESCRIPTION "CLI tool providing information about graphics cards and displays.") - set(CPACK_COMPONENT_DXGI_GROUP "tools") - - # service - set(CPACK_COMPONENT_SUNSHINESVC_DISPLAY_NAME "sunshinesvc") - set(CPACK_COMPONENT_SUNSHINESVC_DESCRIPTION "CLI tool providing ability to enable/disable the Sunshine service.") - set(CPACK_COMPONENT_SUNSHINESVC_GROUP "tools") - - # service scripts - set(CPACK_COMPONENT_SERVICE_DISPLAY_NAME "service-scripts") - set(CPACK_COMPONENT_SERVICE_DESCRIPTION "Scripts to enable/disable the service.") - set(CPACK_COMPONENT_SERVICE_GROUP "scripts") - set(CPACK_COMPONENT_SERVICE_DEPENDS sunshinesvc) + set(CPACK_COMPONENT_DXGI_GROUP "Tools") # firewall scripts - set(CPACK_COMPONENT_FIREWALL_DISPLAY_NAME "firewall-scripts") + set(CPACK_COMPONENT_FIREWALL_DISPLAY_NAME "Add Firewall Exclusions") set(CPACK_COMPONENT_FIREWALL_DESCRIPTION "Scripts to enable or disable firewall rules.") - set(CPACK_COMPONENT_FIREWALL_GROUP "scripts") + set(CPACK_COMPONENT_FIREWALL_GROUP "Scripts") # vigembus scripts - set(CPACK_COMPONENT_VIGEMBUS_DISPLAY_NAME "vigembus-scripts") + set(CPACK_COMPONENT_VIGEMBUS_DISPLAY_NAME "Virtual Gamepad Support") set(CPACK_COMPONENT_VIGEMBUS_DESCRIPTION "Scripts to install and uninstall ViGEmBus for virtual gamepad support.") - set(CPACK_COMPONENT_VIGEMBUS_GROUP "scripts") + set(CPACK_COMPONENT_VIGEMBUS_GROUP "Scripts") endif() if(APPLE) # TODO: bundle doesn't produce a valid .app use cpack -G DragNDrop @@ -942,7 +944,7 @@ elseif(UNIX) if(${SUNSHINE_TRAY} STREQUAL 1) install(FILES "${CMAKE_SOURCE_DIR}/sunshine.svg" - DESTINATION "/usr/share/icons") + DESTINATION "${CMAKE_INSTALL_PREFIX}/share/icons") set(CPACK_DEBIAN_PACKAGE_DEPENDS "\ ${CPACK_DEBIAN_PACKAGE_DEPENDS}, \ diff --git a/docker/archlinux.dockerfile b/docker/archlinux.dockerfile index 0cbfcf7b7f8..5ff4a4ab0b8 100644 --- a/docker/archlinux.dockerfile +++ b/docker/archlinux.dockerfile @@ -131,7 +131,7 @@ userdel -r builder # then create the lizard user groupadd -f -g "${PGID}" "${UNAME}" -useradd -lm -d ${HOME} -s /bin/bash -g "${PGID}" -G input -u "${PUID}" "${UNAME}" +useradd -lm -d ${HOME} -s /bin/bash -g "${PGID}" -u "${PUID}" "${UNAME}" mkdir -p ${HOME}/.config/sunshine ln -s ${HOME}/.config/sunshine /config chown -R ${UNAME} ${HOME} diff --git a/docker/debian-bullseye.dockerfile b/docker/debian-bullseye.dockerfile index 1d2d0fcf6e4..7c4396184ae 100644 --- a/docker/debian-bullseye.dockerfile +++ b/docker/debian-bullseye.dockerfile @@ -30,39 +30,39 @@ RUN <<_DEPS set -e apt-get update -y apt-get install -y --no-install-recommends \ - build-essential=12.9* \ - cmake=3.18.4* \ - git=1:2.30.2* \ - libavdevice-dev=7:4.3.* \ - libboost-filesystem-dev=1.74.0* \ - libboost-locale-dev=1.74.0* \ - libboost-log-dev=1.74.0* \ - libboost-program-options-dev=1.74.0* \ - libboost-thread-dev=1.74.0* \ - libcap-dev=1:2.44* \ - libcurl4-openssl-dev=7.74.0* \ - libdrm-dev=2.4.104* \ - libevdev-dev=1.11.0* \ - libnuma-dev=2.0.12* \ - libopus-dev=1.3.1* \ - libpulse-dev=14.2* \ - libssl-dev=1.1.1* \ - libva-dev=2.10.0* \ - libvdpau-dev=1.4* \ - libwayland-dev=1.18.0* \ - libx11-dev=2:1.7.2* \ - libxcb-shm0-dev=1.14* \ - libxcb-xfixes0-dev=1.14* \ - libxcb1-dev=1.14* \ - libxfixes-dev=1:5.0.3* \ - libxrandr-dev=2:1.5.1* \ - libxtst-dev=2:1.2.3* \ - nodejs=12.22* \ - npm=7.5.2* \ - wget=1.21* + build-essential \ + cmake=3.18.* \ + git \ + libavdevice-dev \ + libboost-filesystem-dev=1.74.* \ + libboost-locale-dev=1.74.* \ + libboost-log-dev=1.74.* \ + libboost-program-options-dev=1.74.* \ + libboost-thread-dev=1.74.* \ + libcap-dev \ + libcurl4-openssl-dev \ + libdrm-dev \ + libevdev-dev \ + libnuma-dev \ + libopus-dev \ + libpulse-dev \ + libssl-dev \ + libva-dev \ + libvdpau-dev \ + libwayland-dev \ + libx11-dev \ + libxcb-shm0-dev \ + libxcb-xfixes0-dev \ + libxcb1-dev \ + libxfixes-dev \ + libxrandr-dev \ + libxtst-dev \ + nodejs \ + npm \ + wget if [[ "${TARGETPLATFORM}" == 'linux/amd64' ]]; then apt-get install -y --no-install-recommends \ - libmfx-dev=21.1.0* + libmfx-dev fi apt-get clean rm -rf /var/lib/apt/lists/* @@ -161,7 +161,7 @@ RUN <<_SETUP_USER #!/bin/bash set -e groupadd -f -g "${PGID}" "${UNAME}" -useradd -lm -d ${HOME} -s /bin/bash -g "${PGID}" -G input -u "${PUID}" "${UNAME}" +useradd -lm -d ${HOME} -s /bin/bash -g "${PGID}" -u "${PUID}" "${UNAME}" mkdir -p ${HOME}/.config/sunshine ln -s ${HOME}/.config/sunshine /config chown -R ${UNAME} ${HOME} diff --git a/docker/fedora-37.dockerfile b/docker/fedora-37.dockerfile index 2e1be80c368..b05f8cb1c21 100644 --- a/docker/fedora-37.dockerfile +++ b/docker/fedora-37.dockerfile @@ -30,37 +30,37 @@ set -e dnf -y update dnf -y group install "Development Tools" dnf -y install \ - boost-devel-1.78.0* \ - cmake-3.24.1* \ - gcc-12.2.1* \ - gcc-c++-12.2.1* \ - git-2.39.2* \ - libappindicator-gtk3-devel-12.10.1* \ - libcap-devel-2.48* \ - libcurl-devel-7.85.0* \ - libdrm-devel-2.4.112* \ - libevdev-devel-1.13.0* \ - libva-devel-2.15.0* \ - libvdpau-devel-1.5* \ - libX11-devel-1.8.1* \ - libxcb-devel-1.13.1* \ - libXcursor-devel-1.2.1* \ - libXfixes-devel-6.0.0* \ - libXi-devel-1.8* \ - libXinerama-devel-1.1.4* \ - libXrandr-devel-1.5.2* \ - libXtst-devel-1.2.3* \ - mesa-libGL-devel-22.2.2* \ - npm-8.15.0* \ - numactl-devel-2.0.14* \ - openssl-devel-3.0.5* \ - opus-devel-1.3.1* \ - pulseaudio-libs-devel-16.1* \ - rpm-build-4.18.0* \ - wget-1.21.3* \ - which-2.21* + boost-devel-1.78.* \ + cmake-3.26.* \ + gcc-12.2.* \ + gcc-c++-12.2.* \ + git \ + libappindicator-gtk3-devel \ + libcap-devel \ + libcurl-devel \ + libdrm-devel \ + libevdev-devel \ + libva-devel \ + libvdpau-devel \ + libX11-devel \ + libxcb-devel \ + libXcursor-devel \ + libXfixes-devel \ + libXi-devel \ + libXinerama-devel \ + libXrandr-devel \ + libXtst-devel \ + mesa-libGL-devel \ + nodejs-npm \ + numactl-devel \ + openssl-devel \ + opus-devel \ + pulseaudio-libs-devel \ + rpm-build \ + wget \ + which if [[ "${TARGETPLATFORM}" == 'linux/amd64' ]]; then - dnf -y install intel-mediasdk-devel-22.4.4* + dnf -y install intel-mediasdk-devel fi dnf clean all rm -rf /var/cache/yum @@ -159,7 +159,7 @@ RUN <<_SETUP_USER #!/bin/bash set -e groupadd -f -g "${PGID}" "${UNAME}" -useradd -lm -d ${HOME} -s /bin/bash -g "${PGID}" -G input -u "${PUID}" "${UNAME}" +useradd -lm -d ${HOME} -s /bin/bash -g "${PGID}" -u "${PUID}" "${UNAME}" mkdir -p ${HOME}/.config/sunshine ln -s ${HOME}/.config/sunshine /config chown -R ${UNAME} ${HOME} diff --git a/docker/fedora-36.dockerfile b/docker/fedora-38.dockerfile similarity index 57% rename from docker/fedora-36.dockerfile rename to docker/fedora-38.dockerfile index 33adf2250b4..9635f192cc7 100644 --- a/docker/fedora-36.dockerfile +++ b/docker/fedora-38.dockerfile @@ -4,7 +4,7 @@ # platforms_pr: linux/amd64 # no-cache-filters: sunshine-base,artifacts,sunshine ARG BASE=fedora -ARG TAG=36 +ARG TAG=38 FROM ${BASE}:${TAG} AS sunshine-base FROM sunshine-base as sunshine-build @@ -30,63 +30,64 @@ set -e dnf -y update dnf -y group install "Development Tools" dnf -y install \ - boost-devel-1.76.0* \ - cmake-3.22.2* \ - gcc-12.0.1* \ - gcc-c++-12.0.1* \ - git-2.39.2* \ - libappindicator-gtk3-devel-12.10.0* \ - libcap-devel-2.48* \ - libcurl-devel-7.82.0* \ - libdrm-devel-2.4.110* \ - libevdev-devel-1.12.0* \ - libva-devel-2.14.0* \ - libvdpau-devel-1.5* \ - libX11-devel-1.7.3* \ - libxcb-devel-1.13.1* \ - libXcursor-devel-1.2.0* \ - libXfixes-devel-6.0.0* \ - libXi-devel-1.8* \ - libXinerama-devel-1.1.4* \ - libXrandr-devel-1.5.2* \ - libXtst-devel-1.2.3* \ - mesa-libGL-devel-22.0.1* \ - npm-8.3.1* \ - numactl-devel-2.0.14* \ - openssl-devel-3.0.2* \ - opus-devel-1.3.1* \ - pulseaudio-libs-devel-15.0* \ - rpm-build-4.17.0* \ - wget-1.21.3* \ - which-2.21* + boost-devel-1.78.0* \ + cmake-3.26.* \ + gcc-13.0.* \ + gcc-c++-13.0.* \ + git \ + libappindicator-gtk3-devel \ + libcap-devel \ + libcurl-devel \ + libdrm-devel \ + libevdev-devel \ + libva-devel \ + libvdpau-devel \ + libX11-devel \ + libxcb-devel \ + libXcursor-devel \ + libXfixes-devel \ + libXi-devel \ + libXinerama-devel \ + libXrandr-devel \ + libXtst-devel \ + mesa-libGL-devel \ + nodejs-npm \ + numactl-devel \ + openssl-devel \ + opus-devel \ + pulseaudio-libs-devel \ + rpm-build \ + wget \ + which if [[ "${TARGETPLATFORM}" == 'linux/amd64' ]]; then - dnf -y install intel-mediasdk-devel-22.3.0* + dnf -y install intel-mediasdk-devel fi dnf clean all rm -rf /var/cache/yum _DEPS -# install cuda -WORKDIR /build/cuda -# versions: https://developer.nvidia.com/cuda-toolkit-archive -ENV CUDA_VERSION="12.0.0" -ENV CUDA_BUILD="525.60.13" -# hadolint ignore=SC3010 -RUN <<_INSTALL_CUDA -#!/bin/bash -set -e -cuda_prefix="https://developer.download.nvidia.com/compute/cuda/" -cuda_suffix="" -if [[ "${TARGETPLATFORM}" == 'linux/arm64' ]]; then - cuda_suffix="_sbsa" -fi -url="${cuda_prefix}${CUDA_VERSION}/local_installers/cuda_${CUDA_VERSION}_${CUDA_BUILD}_linux${cuda_suffix}.run" -echo "cuda url: ${url}" -wget "$url" --progress=bar:force:noscroll -q --show-progress -O ./cuda.run -chmod a+x ./cuda.run -./cuda.run --silent --toolkit --toolkitpath=/build/cuda --no-opengl-libs --no-man-page --no-drm -rm ./cuda.run -_INSTALL_CUDA +# todo - enable cuda once it's supported for gcc 13 and fedora 38 +## install cuda +#WORKDIR /build/cuda +## versions: https://developer.nvidia.com/cuda-toolkit-archive +#ENV CUDA_VERSION="12.0.0" +#ENV CUDA_BUILD="525.60.13" +## hadolint ignore=SC3010 +#RUN <<_INSTALL_CUDA +##!/bin/bash +#set -e +#cuda_prefix="https://developer.download.nvidia.com/compute/cuda/" +#cuda_suffix="" +#if [[ "${TARGETPLATFORM}" == 'linux/arm64' ]]; then +# cuda_suffix="_sbsa" +#fi +#url="${cuda_prefix}${CUDA_VERSION}/local_installers/cuda_${CUDA_VERSION}_${CUDA_BUILD}_linux${cuda_suffix}.run" +#echo "cuda url: ${url}" +#wget "$url" --progress=bar:force:noscroll -q --show-progress -O ./cuda.run +#chmod a+x ./cuda.run +#./cuda.run --silent --toolkit --toolkitpath=/build/cuda --no-opengl-libs --no-man-page --no-drm +#rm ./cuda.run +#_INSTALL_CUDA # copy repository WORKDIR /build/sunshine/ @@ -99,11 +100,12 @@ RUN npm install WORKDIR /build/sunshine/build # cmake and cpack +# todo - add cmake argument back in for cuda support "-DCMAKE_CUDA_COMPILER:PATH=/build/cuda/bin/nvcc \" +# todo - re-enable "DSUNSHINE_ENABLE_CUDA" RUN <<_MAKE #!/bin/bash set -e cmake \ - -DCMAKE_CUDA_COMPILER:PATH=/build/cuda/bin/nvcc \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_INSTALL_PREFIX=/usr \ -DSUNSHINE_ASSETS_DIR=share/sunshine \ @@ -111,7 +113,7 @@ cmake \ -DSUNSHINE_ENABLE_WAYLAND=ON \ -DSUNSHINE_ENABLE_X11=ON \ -DSUNSHINE_ENABLE_DRM=ON \ - -DSUNSHINE_ENABLE_CUDA=ON \ + -DSUNSHINE_ENABLE_CUDA=OFF \ /build/sunshine make -j "$(nproc)" cpack -G RPM @@ -159,7 +161,7 @@ RUN <<_SETUP_USER #!/bin/bash set -e groupadd -f -g "${PGID}" "${UNAME}" -useradd -lm -d ${HOME} -s /bin/bash -g "${PGID}" -G input -u "${PUID}" "${UNAME}" +useradd -lm -d ${HOME} -s /bin/bash -g "${PGID}" -u "${PUID}" "${UNAME}" mkdir -p ${HOME}/.config/sunshine ln -s ${HOME}/.config/sunshine /config chown -R ${UNAME} ${HOME} diff --git a/docker/ubuntu-20.04.dockerfile b/docker/ubuntu-20.04.dockerfile index 4de2c02ca2d..5689ba7134b 100644 --- a/docker/ubuntu-20.04.dockerfile +++ b/docker/ubuntu-20.04.dockerfile @@ -30,41 +30,41 @@ RUN <<_DEPS set -e apt-get update -y apt-get install -y --no-install-recommends \ - build-essential=12.8* \ - gcc-10=10.3.0* \ - g++-10=10.3.0* \ - git=1:2.25.1* \ - libappindicator3-dev=12.10.1* \ - libavdevice-dev=7:4.2.* \ - libboost-filesystem-dev=1.71.0* \ - libboost-locale-dev=1.71.0* \ - libboost-log-dev=1.71.0* \ - libboost-program-options-dev=1.71.0* \ - libboost-thread-dev=1.71.0* \ - libcap-dev=1:2.32* \ - libcurl4-openssl-dev=7.68.0* \ - libdrm-dev=2.4.107* \ - libevdev-dev=1.9.0* \ - libnuma-dev=2.0.12* \ - libopus-dev=1.3.1* \ - libpulse-dev=1:13.99.1* \ - libssl-dev=1.1.1* \ - libva-dev=2.7.0* \ - libvdpau-dev=1.3* \ - libwayland-dev=1.18.0* \ - libx11-dev=2:1.6.9* \ - libxcb-shm0-dev=1.14* \ - libxcb-xfixes0-dev=1.14* \ - libxcb1-dev=1.14* \ - libxfixes-dev=1:5.0.3* \ - libxrandr-dev=2:1.5.2* \ - libxtst-dev=2:1.2.3* \ - nodejs=10.19.0* \ - npm=6.14.4* \ - wget=1.20.3* + build-essential \ + gcc-10=10.3.* \ + g++-10=10.3.* \ + git \ + libappindicator3-dev \ + libavdevice-dev \ + libboost-filesystem-dev=1.71.* \ + libboost-locale-dev=1.71.* \ + libboost-log-dev=1.71.* \ + libboost-program-options-dev=1.71.* \ + libboost-thread-dev=1.71.* \ + libcap-dev \ + libcurl4-openssl-dev \ + libdrm-dev \ + libevdev-dev \ + libnuma-dev \ + libopus-dev \ + libpulse-dev \ + libssl-dev \ + libva-dev \ + libvdpau-dev \ + libwayland-dev \ + libx11-dev \ + libxcb-shm0-dev \ + libxcb-xfixes0-dev \ + libxcb1-dev \ + libxfixes-dev \ + libxrandr-dev \ + libxtst-dev \ + nodejs \ + npm \ + wget if [[ "${TARGETPLATFORM}" == 'linux/amd64' ]]; then apt-get install -y --no-install-recommends \ - libmfx-dev=20.1.0* + libmfx-dev fi apt-get clean rm -rf /var/lib/apt/lists/* @@ -198,7 +198,7 @@ RUN <<_SETUP_USER #!/bin/bash set -e groupadd -f -g "${PGID}" "${UNAME}" -useradd -lm -d ${HOME} -s /bin/bash -g "${PGID}" -G input -u "${PUID}" "${UNAME}" +useradd -lm -d ${HOME} -s /bin/bash -g "${PGID}" -u "${PUID}" "${UNAME}" mkdir -p ${HOME}/.config/sunshine ln -s ${HOME}/.config/sunshine /config chown -R ${UNAME} ${HOME} diff --git a/docker/ubuntu-22.04.dockerfile b/docker/ubuntu-22.04.dockerfile index e8a69361894..143fc1e4b24 100644 --- a/docker/ubuntu-22.04.dockerfile +++ b/docker/ubuntu-22.04.dockerfile @@ -30,40 +30,40 @@ RUN <<_DEPS set -e apt-get update -y apt-get install -y --no-install-recommends \ - build-essential=12.9* \ - cmake=3.22.1* \ - git=1:2.34.1* \ - libappindicator3-dev=12.10.1* \ - libavdevice-dev=7:4.4.* \ - libboost-filesystem-dev=1.74.0* \ - libboost-locale-dev=1.74.0* \ - libboost-log-dev=1.74.0* \ - libboost-program-options-dev=1.74.0* \ - libboost-thread-dev=1.74.0* \ - libcap-dev=1:2.44* \ - libcurl4-openssl-dev=7.81.0* \ - libdrm-dev=2.4.113* \ - libevdev-dev=1.12.1* \ - libnuma-dev=2.0.14* \ - libopus-dev=1.3.1* \ - libpulse-dev=1:15.99.1* \ - libssl-dev=3.0.2* \ - libva-dev=2.14.0* \ - libvdpau-dev=1.4* \ - libwayland-dev=1.20.0* \ - libx11-dev=2:1.7.5* \ - libxcb-shm0-dev=1.14* \ - libxcb-xfixes0-dev=1.14* \ - libxcb1-dev=1.14* \ - libxfixes-dev=1:6.0.0* \ - libxrandr-dev=2:1.5.2* \ - libxtst-dev=2:1.2.3* \ - nodejs=12.22.9* \ - npm=8.5.1* \ - wget=1.21.2* + build-essential \ + cmake=3.22.* \ + git \ + libappindicator3-dev \ + libavdevice-dev \ + libboost-filesystem-dev=1.74.* \ + libboost-locale-dev=1.74.* \ + libboost-log-dev=1.74.* \ + libboost-program-options-dev=1.74.* \ + libboost-thread-dev=1.74.* \ + libcap-dev \ + libcurl4-openssl-dev \ + libdrm-dev \ + libevdev-dev \ + libnuma-dev \ + libopus-dev \ + libpulse-dev \ + libssl-dev \ + libva-dev \ + libvdpau-dev \ + libwayland-dev \ + libx11-dev \ + libxcb-shm0-dev \ + libxcb-xfixes0-dev \ + libxcb1-dev \ + libxfixes-dev \ + libxrandr-dev \ + libxtst-dev \ + nodejs \ + npm \ + wget if [[ "${TARGETPLATFORM}" == 'linux/amd64' ]]; then apt-get install -y --no-install-recommends \ - libmfx-dev=22.3.0* + libmfx-dev fi apt-get clean rm -rf /var/lib/apt/lists/* @@ -162,7 +162,7 @@ RUN <<_SETUP_USER #!/bin/bash set -e groupadd -f -g "${PGID}" "${UNAME}" -useradd -lm -d ${HOME} -s /bin/bash -g "${PGID}" -G input -u "${PUID}" "${UNAME}" +useradd -lm -d ${HOME} -s /bin/bash -g "${PGID}" -u "${PUID}" "${UNAME}" mkdir -p ${HOME}/.config/sunshine ln -s ${HOME}/.config/sunshine /config chown -R ${UNAME} ${HOME} diff --git a/docs/Doxyfile b/docs/Doxyfile index cac6c8f8e9b..5d909b5335e 100644 --- a/docs/Doxyfile +++ b/docs/Doxyfile @@ -184,7 +184,7 @@ FULL_PATH_NAMES = YES # will be relative from the directory where doxygen is started. # This tag requires that the tag FULL_PATH_NAMES is set to YES. -STRIP_FROM_PATH = +STRIP_FROM_PATH = ../ # The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of the # path mentioned in the documentation of a class, which tells the reader which @@ -193,7 +193,7 @@ STRIP_FROM_PATH = # specify the list of include paths that are normally passed to the compiler # using the -I flag. -STRIP_FROM_INC_PATH = +STRIP_FROM_INC_PATH = ../ # If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter (but # less readable) file names. This can be useful is your file systems doesn't @@ -878,6 +878,7 @@ WARN_IF_UNDOC_ENUM_VAL = NO # The default value is: NO. WARN_AS_ERROR = NO +# todo - ideally this will eventually become FAIL_ON_WARNINGS # The WARN_FORMAT tag determines the format of the warning messages that doxygen # can produce. The string should contain the $file, $line, and $text tags, which @@ -2331,7 +2332,7 @@ ENABLE_PREPROCESSING = YES # The default value is: NO. # This tag requires that the tag ENABLE_PREPROCESSING is set to YES. -MACRO_EXPANSION = NO +MACRO_EXPANSION = YES # If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES then # the macro expansion is limited to the macros specified with the PREDEFINED and @@ -2354,7 +2355,7 @@ SEARCH_INCLUDES = YES # RECURSIVE has no effect here. # This tag requires that the tag SEARCH_INCLUDES is set to YES. -INCLUDE_PATH = +INCLUDE_PATH = ../third-party/ffmpeg-linux-x86_64/include/ # You can use the INCLUDE_FILE_PATTERNS tag to specify one or more wildcard # patterns (like *.h and *.hpp) to filter out the header-files in the @@ -2463,7 +2464,7 @@ HIDE_UNDOC_RELATIONS = YES # set to NO # The default value is: NO. -HAVE_DOT = NO +HAVE_DOT = YES # The DOT_NUM_THREADS specifies the number of dot invocations doxygen is allowed # to run in parallel. When set to 0 doxygen will base this on the number of diff --git a/docs/requirements.txt b/docs/requirements.txt index 6160650a168..7ce0be80b83 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,5 +1,5 @@ breathe==4.35.0 -furo==2023.3.27 +furo==2023.5.20 m2r2==0.3.3.post2 -Sphinx==6.1.3 -sphinx-copybutton==0.5.1 +Sphinx==7.0.1 +sphinx-copybutton==0.5.2 diff --git a/docs/source/about/advanced_usage.rst b/docs/source/about/advanced_usage.rst index 19fe237791c..088d9045c29 100644 --- a/docs/source/about/advanced_usage.rst +++ b/docs/source/about/advanced_usage.rst @@ -156,14 +156,12 @@ back_button_timeout ^^^^^^^^^^^^^^^^^^^ **Description** - If, after the timeout, the back/select button is still pressed down, Home/Guide button press is emulated. - - On Nvidia Shield, the home and power button are not passed to Moonlight. + If the Back/Select button is held down for the specified number of milliseconds, a Home/Guide button press is emulated. .. Tip:: If back_button_timeout < 0, then the Home/Guide button will not be emulated. **Default** - ``2000`` + ``-1`` **Example** .. code-block:: text @@ -200,6 +198,27 @@ key_repeat_frequency key_repeat_frequency = 24.9 +always_send_scancodes +^^^^^^^^^^^^^^^^^^^^^ + +**Description** + Sending scancodes enhances compatibility with games and apps but may result in incorrect keyboard input + from certain clients that aren't using a US English keyboard layout. + + Enable if keyboard input is not working at all in certain applications. + + Disable if keys on the client are generating the wrong input on the host. + + .. Caution:: Applies to Windows only. + +**Default** + ``enabled`` + +**Example** + .. code-block:: text + + always_send_scancodes = enabled + keybindings ^^^^^^^^^^^ @@ -373,7 +392,7 @@ resolutions 2560x1080, 3440x1440, 1920x1200, - 3860x2160, + 3840x2160, 3840x1600, ] @@ -389,7 +408,7 @@ resolutions 2560x1080, 3440x1440, 1920x1200, - 3860x2160, + 3840x2160, 3840x1600, ] @@ -447,6 +466,8 @@ audio_sink tools\audio-info.exe + .. Tip:: If you have multiple audio devices with identical names, use the Device ID instead. + .. Tip:: If you want to mute the host speakers, use `virtual_sink`_ instead. **Default** @@ -466,7 +487,7 @@ audio_sink **Windows** .. code-block:: text - audio_sink = {0.0.0.00000000}.{FD47D9CC-4218-4135-9CE2-0C195C87405B} + audio_sink = Speakers (High Definition Audio Device) virtual_sink ^^^^^^^^^^^^ @@ -481,14 +502,31 @@ virtual_sink - Stream Streaming Speakers (Linux, macOS, Windows) - - To use this option, you must have Steam installed and have used Stream remote play at least once. + - Steam must be installed. + - Enable `install_steam_audio_drivers`_ or use Steam Remote Play at least once to install the drivers. - `Virtual Audio Cable `_ (macOS, Windows) **Example** .. code-block:: text - virtual_sink = {0.0.0.00000000}.{8edba70c-1125-467c-b89c-15da389bc1d4} + virtual_sink = Steam Streaming Speakers + +install_steam_audio_drivers +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Description** + Installs the Steam Streaming Speakers driver (if Steam is installed) to support surround sound and muting host audio. + + .. Tip:: This option is only supported on Windows. + +**Default** + ``enabled`` + +**Example** + .. code-block:: text + + install_steam_audio_drivers = enabled Network ------- @@ -790,7 +828,7 @@ capture nvfbc Use NVIDIA Frame Buffer Capture to capture direct to GPU memory. This is usually the fastest method for NVIDIA cards. For GeForce cards it will only work with drivers patched with `nvidia-patch `_ - or `nvlax `_. + or `nvlax `_. wlr Capture for wlroots based Wayland compositors via DMA-BUF. kms DRM/KMS screen capture from the kernel. This requires that sunshine has cap_sys_admin capability. See :ref:`Linux Setup `. diff --git a/docs/source/about/app_examples.rst b/docs/source/about/app_examples.rst index dbeac1ac91d..0e39029797b 100644 --- a/docs/source/about/app_examples.rst +++ b/docs/source/about/app_examples.rst @@ -187,3 +187,32 @@ Changing Resolution and Refresh Rate (Windows) .. Tip:: You can change your host resolution to match the client resolution automatically using the `Nonary/ResolutionAutomation `_ project. + + +Elevating Commands (Windows) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If you've installed Sunshine as a service (default), you can now specify if a command should be elevated with adminsitrative privileges. +Simply enable the elevated option in the WEB UI, or add it to the JSON configuration. +This is an option for both prep-cmd and regular commands and will launch the process with the current user without a UAC prompt. + +.. Note:: It's important to write the values "true" and "false" as string values, not as the typical true/false values in most JSON. + +**Example** + .. code-block:: json + + { + "name": "Game With AntiCheat that Requires Admin", + "output": "", + "cmd": "ping 127.0.0.1", + "exclude-global-prep-cmd": "false", + "elevated": "true", + "prep-cmd": [ + { + "do": "powershell.exe -command \"Start-Streaming\"", + "undo": "powershell.exe -command \"Stop-Streaming\"", + "elevated": "false" + } + ], + "image-path": "" + } diff --git a/docs/source/about/installation.rst b/docs/source/about/installation.rst index dede1f22d0c..323d78f4610 100644 --- a/docs/source/about/installation.rst +++ b/docs/source/about/installation.rst @@ -40,8 +40,8 @@ CUDA is used for NVFBC capture. sunshine.pkg.tar.zst 11.8.0 450.80.02 50;52;60;61;62;70;75;80;86;90;35 sunshine_{arch}.flatpak 12.0.0 525.60.13 50;52;60;61;62;70;75;80;86;90 sunshine-debian-bullseye-{arch}.deb 11.8.0 450.80.02 50;52;60;61;62;70;75;80;86;90;35 - sunshine-fedora-36-{arch}.rpm 12.0.0 525.60.13 50;52;60;61;62;70;75;80;86;90 sunshine-fedora-37-{arch}.rpm 12.0.0 525.60.13 50;52;60;61;62;70;75;80;86;90 + sunshine-fedora-38-{arch}.rpm unavailable unavailable none sunshine-ubuntu-20.04-{arch}.deb 11.8.0 450.80.02 50;52;60;61;62;70;75;80;86;90;35 sunshine-ubuntu-22.04-{arch}.deb 11.8.0 450.80.02 50;52;60;61;62;70;75;80;86;90;35 =========================================== ============== ============== ================================ @@ -190,11 +190,11 @@ macOS ----- Sunshine on macOS is experimental. Gamepads do not work. Other features may not work as expected. -pkg +dmg ^^^ -.. Warning:: The `pkg` does not include runtime dependencies. +.. Warning:: The `dmg` does not include runtime dependencies. -#. Download the ``sunshine.pkg`` file and install it as normal. +#. Download the ``sunshine.dmg`` file and install it. Uninstall: .. code-block:: bash diff --git a/docs/source/about/usage.rst b/docs/source/about/usage.rst index db7af09ea7e..fa680ba6c2d 100644 --- a/docs/source/about/usage.rst +++ b/docs/source/about/usage.rst @@ -68,16 +68,11 @@ The `deb`, `rpm`, `Flatpak` and `AppImage` packages handle these steps automatic Sunshine needs access to `uinput` to create mouse and gamepad events. -#. Add user to group `input`, if this is the first time installing. - .. code-block:: bash - - sudo usermod -a -G input $USER - #. Create `udev` rules. - .. code-block:: + .. code-block:: bash - echo 'KERNEL=="uinput", GROUP="input", MODE="0660", OPTIONS+="static_node=uinput"' | \ - sudo tee /etc/udev/rules.d/85-sunshine-input.rules + echo 'KERNEL=="uinput", SUBSYSTEM=="misc", OPTIONS+="static_node=uinput", TAG+="uaccess"' | \ + sudo tee /etc/udev/rules.d/85-sunshine.rules #. Optionally, configure autostart service diff --git a/docs/source/conf.py b/docs/source/conf.py index 58bff26c731..e581783865f 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -23,7 +23,7 @@ # -- Project information ----------------------------------------------------- project = 'Sunshine' -copyright = f'{datetime.now ().year}, {project}' +project_copyright = f'{datetime.now ().year}, {project}' author = 'ReenigneArcher' # The full version, including alpha/beta/rc tags @@ -95,8 +95,16 @@ ) todo_include_todos = True -subprocess.run('doxygen', shell=True, cwd=source_dir) - # disable epub mimetype warnings # https://github.com/readthedocs/readthedocs.org/blob/eadf6ac6dc6abc760a91e1cb147cc3c5f37d1ea8/docs/conf.py#L235-L236 suppress_warnings = ["epub.unknown_project_files"] + +# get doxygen version +doxy_proc = subprocess.run('doxygen --version', shell=True, cwd=source_dir, capture_output=True) +doxy_version = doxy_proc.stdout.decode('utf-8').strip() +print('doxygen version: ' + doxy_version) + +# run doxygen +doxy_proc = subprocess.run('doxygen Doxyfile', shell=True, cwd=source_dir) +if doxy_proc.returncode != 0: + raise RuntimeError('doxygen failed with return code ' + str(doxy_proc.returncode)) diff --git a/docs/source/gamestream/gamestream.rst b/docs/source/gamestream/gamestream.rst index 59ba6e1d0e7..aed014b287c 100644 --- a/docs/source/gamestream/gamestream.rst +++ b/docs/source/gamestream/gamestream.rst @@ -12,6 +12,14 @@ migration option. At the time of writing this GSMS offers the ability to migrate working directory, command, and image are all set in Sunshine's ``apps.json`` file. The box-art image is also copied to a specified directory. +Internet Streaming +------------------ +If you are using the Moonlight Internet Hosting Tool, you can remove it from your system when you migrate to Sunshine. +To stream over the Internet with Sunshine and a UPnP-capable router, enable the UPnP option in the Sunshine Web UI. + +.. note:: Running Sunshine together with versions of the Moonlight Internet Hosting Tool prior to v5.6 will cause UPnP + port forwarding to become unreliable. Either uninstall the tool entirely or update it to v5.6 or later. + Limitations ----------- Sunshine does have some limitations, as compared to Nvidia GameStream. diff --git a/docs/source/source/src/audio.rst b/docs/source/source/src/audio.rst index a8fb3c9ca45..08665fe6780 100644 --- a/docs/source/source/src/audio.rst +++ b/docs/source/source/src/audio.rst @@ -2,3 +2,4 @@ audio ===== .. doxygenfile:: audio.h + :allow-dot-graphs: diff --git a/docs/source/source/src/cbs.rst b/docs/source/source/src/cbs.rst index 2abe9915772..6547c495821 100644 --- a/docs/source/source/src/cbs.rst +++ b/docs/source/source/src/cbs.rst @@ -2,3 +2,4 @@ cbs === .. doxygenfile:: cbs.h + :allow-dot-graphs: diff --git a/docs/source/source/src/config.rst b/docs/source/source/src/config.rst index 44e10565493..34bccff3ce2 100644 --- a/docs/source/source/src/config.rst +++ b/docs/source/source/src/config.rst @@ -2,3 +2,4 @@ config ====== .. doxygenfile:: config.h + :allow-dot-graphs: diff --git a/docs/source/source/src/confighttp.rst b/docs/source/source/src/confighttp.rst index 3b46ba5edf7..348f6521ba6 100644 --- a/docs/source/source/src/confighttp.rst +++ b/docs/source/source/src/confighttp.rst @@ -2,3 +2,4 @@ confighttp ========== .. doxygenfile:: confighttp.h + :allow-dot-graphs: diff --git a/docs/source/source/src/crypto.rst b/docs/source/source/src/crypto.rst index 5abf24693db..aced9d35c68 100644 --- a/docs/source/source/src/crypto.rst +++ b/docs/source/source/src/crypto.rst @@ -2,3 +2,4 @@ crypto ====== .. doxygenfile:: crypto.h + :allow-dot-graphs: diff --git a/docs/source/source/src/httpcommon.rst b/docs/source/source/src/httpcommon.rst index 8afbdeb215f..67557f86860 100644 --- a/docs/source/source/src/httpcommon.rst +++ b/docs/source/source/src/httpcommon.rst @@ -2,3 +2,4 @@ httpcommon ========== .. doxygenfile:: httpcommon.h + :allow-dot-graphs: diff --git a/docs/source/source/src/input.rst b/docs/source/source/src/input.rst index e1988b2406f..4c8db082dc6 100644 --- a/docs/source/source/src/input.rst +++ b/docs/source/source/src/input.rst @@ -2,3 +2,4 @@ input ===== .. doxygenfile:: input.h + :allow-dot-graphs: diff --git a/docs/source/source/src/main.rst b/docs/source/source/src/main.rst index cd6716faa5c..5102a3e9448 100644 --- a/docs/source/source/src/main.rst +++ b/docs/source/source/src/main.rst @@ -2,3 +2,4 @@ main ==== .. doxygenfile:: main.h + :allow-dot-graphs: diff --git a/docs/source/source/src/move_by_copy.rst b/docs/source/source/src/move_by_copy.rst index 034c3aa240a..5ec37716a9a 100644 --- a/docs/source/source/src/move_by_copy.rst +++ b/docs/source/source/src/move_by_copy.rst @@ -2,3 +2,4 @@ move_by_copy ============ .. doxygenfile:: move_by_copy.h + :allow-dot-graphs: diff --git a/docs/source/source/src/network.rst b/docs/source/source/src/network.rst index 00df0e16a5f..a9121e16008 100644 --- a/docs/source/source/src/network.rst +++ b/docs/source/source/src/network.rst @@ -2,3 +2,4 @@ network ======= .. doxygenfile:: network.h + :allow-dot-graphs: diff --git a/docs/source/source/src/nvhttp.rst b/docs/source/source/src/nvhttp.rst index ea4daa01507..8a3bbd008a5 100644 --- a/docs/source/source/src/nvhttp.rst +++ b/docs/source/source/src/nvhttp.rst @@ -2,3 +2,4 @@ nvhttp ====== .. doxygenfile:: nvhttp.h + :allow-dot-graphs: diff --git a/docs/source/source/src/platform/linux/cuda.rst b/docs/source/source/src/platform/linux/cuda.rst index cb975aa918d..5b6dffe770b 100644 --- a/docs/source/source/src/platform/linux/cuda.rst +++ b/docs/source/source/src/platform/linux/cuda.rst @@ -2,3 +2,4 @@ cuda ==== .. doxygenfile:: platform/linux/cuda.h + :allow-dot-graphs: diff --git a/docs/source/source/src/platform/linux/vaapi.rst b/docs/source/source/src/platform/linux/vaapi.rst index 8880e079eeb..973e78523b4 100644 --- a/docs/source/source/src/platform/linux/vaapi.rst +++ b/docs/source/source/src/platform/linux/vaapi.rst @@ -2,3 +2,4 @@ vaapi ===== .. doxygenfile:: platform/linux/vaapi.h + :allow-dot-graphs: diff --git a/docs/source/source/src/platform/linux/wayland.rst b/docs/source/source/src/platform/linux/wayland.rst index 72f74fe2b83..670e4340ab0 100644 --- a/docs/source/source/src/platform/linux/wayland.rst +++ b/docs/source/source/src/platform/linux/wayland.rst @@ -2,3 +2,4 @@ wayland ======= .. doxygenfile:: platform/linux/wayland.h + :allow-dot-graphs: diff --git a/docs/source/source/src/platform/macos/misc.rst b/docs/source/source/src/platform/macos/misc.rst index f000da0813f..26cbc1874c6 100644 --- a/docs/source/source/src/platform/macos/misc.rst +++ b/docs/source/source/src/platform/macos/misc.rst @@ -2,3 +2,4 @@ misc ==== .. doxygenfile:: platform/macos/misc.h + :allow-dot-graphs: diff --git a/docs/source/source/src/platform/windows/misc.rst b/docs/source/source/src/platform/windows/misc.rst index cca4b82fdd4..88c1620002a 100644 --- a/docs/source/source/src/platform/windows/misc.rst +++ b/docs/source/source/src/platform/windows/misc.rst @@ -2,3 +2,4 @@ misc ==== .. doxygenfile:: platform/windows/misc.h + :allow-dot-graphs: diff --git a/docs/source/source/src/process.rst b/docs/source/source/src/process.rst index fd4110a09a0..ad8f9764114 100644 --- a/docs/source/source/src/process.rst +++ b/docs/source/source/src/process.rst @@ -2,3 +2,4 @@ process ======= .. doxygenfile:: process.h + :allow-dot-graphs: diff --git a/docs/source/source/src/round_robin.rst b/docs/source/source/src/round_robin.rst index 2a838b88f50..89aea6f5904 100644 --- a/docs/source/source/src/round_robin.rst +++ b/docs/source/source/src/round_robin.rst @@ -2,3 +2,4 @@ round_robin =========== .. doxygenfile:: round_robin.h + :allow-dot-graphs: diff --git a/docs/source/source/src/rtsp.rst b/docs/source/source/src/rtsp.rst index 419f7d7fe2c..7aee0baf056 100644 --- a/docs/source/source/src/rtsp.rst +++ b/docs/source/source/src/rtsp.rst @@ -2,3 +2,4 @@ rtsp ==== .. doxygenfile:: rtsp.h + :allow-dot-graphs: diff --git a/docs/source/source/src/stream.rst b/docs/source/source/src/stream.rst index b9bcc4b4675..c771414016a 100644 --- a/docs/source/source/src/stream.rst +++ b/docs/source/source/src/stream.rst @@ -2,3 +2,4 @@ stream ====== .. doxygenfile:: stream.h + :allow-dot-graphs: diff --git a/docs/source/source/src/sync.rst b/docs/source/source/src/sync.rst index 43cea778eca..6f3d95929e2 100644 --- a/docs/source/source/src/sync.rst +++ b/docs/source/source/src/sync.rst @@ -2,3 +2,4 @@ sync ==== .. doxygenfile:: sync.h + :allow-dot-graphs: diff --git a/docs/source/source/src/system_tray.rst b/docs/source/source/src/system_tray.rst index 3b69c246977..7c1009e1c8c 100644 --- a/docs/source/source/src/system_tray.rst +++ b/docs/source/source/src/system_tray.rst @@ -2,3 +2,4 @@ system_tray =========== .. doxygenfile:: system_tray.h + :allow-dot-graphs: diff --git a/docs/source/source/src/task_pool.rst b/docs/source/source/src/task_pool.rst index 8c37231452d..a556141aa4c 100644 --- a/docs/source/source/src/task_pool.rst +++ b/docs/source/source/src/task_pool.rst @@ -1,4 +1,5 @@ -tasl_pool +task_pool ========= .. doxygenfile:: task_pool.h + :allow-dot-graphs: diff --git a/docs/source/source/src/thread_pool.rst b/docs/source/source/src/thread_pool.rst index 3d563bd3486..32787512811 100644 --- a/docs/source/source/src/thread_pool.rst +++ b/docs/source/source/src/thread_pool.rst @@ -2,3 +2,4 @@ thread_pool =========== .. doxygenfile:: thread_pool.h + :allow-dot-graphs: diff --git a/docs/source/source/src/thread_safe.rst b/docs/source/source/src/thread_safe.rst index 00f394c4df3..d7ecdda3f43 100644 --- a/docs/source/source/src/thread_safe.rst +++ b/docs/source/source/src/thread_safe.rst @@ -2,3 +2,4 @@ thread_safe =========== .. doxygenfile:: thread_safe.h + :allow-dot-graphs: diff --git a/docs/source/source/src/upnp.rst b/docs/source/source/src/upnp.rst index b38e8d63dc0..987692353b7 100644 --- a/docs/source/source/src/upnp.rst +++ b/docs/source/source/src/upnp.rst @@ -2,3 +2,4 @@ upnp ==== .. doxygenfile:: upnp.h + :allow-dot-graphs: diff --git a/docs/source/source/src/uuid.rst b/docs/source/source/src/uuid.rst index 803f0e1c904..d21e0a6ceaf 100644 --- a/docs/source/source/src/uuid.rst +++ b/docs/source/source/src/uuid.rst @@ -2,3 +2,4 @@ uuid ==== .. doxygenfile:: uuid.h + :allow-dot-graphs: diff --git a/docs/source/source/src/video.rst b/docs/source/source/src/video.rst index 17bca35e8ba..2e45883f293 100644 --- a/docs/source/source/src/video.rst +++ b/docs/source/source/src/video.rst @@ -2,3 +2,4 @@ video ===== .. doxygenfile:: video.h + :allow-dot-graphs: diff --git a/packaging/linux/AppImage/AppRun b/packaging/linux/AppImage/AppRun index e9f9f3f733c..ddc5fd38455 100644 --- a/packaging/linux/AppImage/AppRun +++ b/packaging/linux/AppImage/AppRun @@ -45,9 +45,8 @@ echo " function install() { # user input rules - sudo usermod -a -G input $USER # shellcheck disable=SC2002 - cat "$SUNSHINE_SHARE_HERE/udev/rules.d/85-sunshine.rules" | sudo tee /etc/udev/85-sunshine.rules + cat "$SUNSHINE_SHARE_HERE/udev/rules.d/85-sunshine.rules" | sudo tee /etc/udev/rules.d/85-sunshine.rules # sunshine service mkdir -p ~/.config/systemd/user diff --git a/packaging/linux/flatpak/scripts/additional-install.sh b/packaging/linux/flatpak/scripts/additional-install.sh index 43615144494..8a905b53810 100644 --- a/packaging/linux/flatpak/scripts/additional-install.sh +++ b/packaging/linux/flatpak/scripts/additional-install.sh @@ -6,8 +6,8 @@ cp /app/share/sunshine/systemd/user/sunshine.service $HOME/.config/systemd/user/ echo Sunshine User Service has been installed. echo Use [systemctl --user enable sunshine] once to autostart Sunshine on login. -# Udev rule and input group +# Udev rule UDEV=$(cat /app/share/sunshine/udev/rules.d/85-sunshine.rules) echo Configuring mouse permission. -flatpak-spawn --host pkexec sh -c "usermod -a -G input $USER && echo '$UDEV' > /etc/udev/rules.d/85-sunshine.rules" +flatpak-spawn --host pkexec sh -c "echo '$UDEV' > /etc/udev/rules.d/85-sunshine.rules" echo Restart computer for mouse permission to take effect. diff --git a/packaging/linux/flatpak/scripts/remove-additional-install.sh b/packaging/linux/flatpak/scripts/remove-additional-install.sh index 74cb95054ed..6148f62ea1e 100644 --- a/packaging/linux/flatpak/scripts/remove-additional-install.sh +++ b/packaging/linux/flatpak/scripts/remove-additional-install.sh @@ -6,6 +6,6 @@ rm $HOME/.config/systemd/user/sunshine.service systemctl --user daemon-reload echo Sunshine User Service has been removed. -# Udev rule and input group -flatpak-spawn --host pkexec sh -c "gpasswd -d $USER input && rm /etc/udev/rules.d/85-sunshine.rules" +# Udev rule +flatpak-spawn --host pkexec sh -c "rm /etc/udev/rules.d/85-sunshine.rules" echo Mouse permission removed. Restart computer to take effect. diff --git a/src/audio.cpp b/src/audio.cpp index e926e01324b..b2c8f458f7f 100644 --- a/src/audio.cpp +++ b/src/audio.cpp @@ -1,3 +1,7 @@ +/** + * @file src/audio.cpp + * @brief todo + */ #include #include @@ -135,22 +139,30 @@ namespace audio { return; } - auto &control = ref->control; - if (!control) { + auto init_failure_fg = util::fail_guard([&shutdown_event]() { + BOOST_LOG(error) << "Unable to initialize audio capture. The stream will not have audio."sv; + + // Wait for shutdown to be signalled if we fail init. + // This allows streaming to continue without audio. shutdown_event->view(); + }); + auto &control = ref->control; + if (!control) { return; } // Order of priority: - // 1. Config - // 2. Virtual if available + // 1. Virtual sink + // 2. Audio sink // 3. Host std::string *sink = &ref->sink.host; if (!config::audio.sink.empty()) { sink = &config::audio.sink; } - else if (ref->sink.null) { + + // Prefer the virtual sink if host playback is disabled or there's no other sink + if (ref->sink.null && (!config.flags[config_t::HOST_AUDIO] || sink->empty())) { auto &null = *ref->sink.null; switch (stream->channelCount) { case 2: @@ -167,20 +179,24 @@ namespace audio { // Only the first to start a session may change the default sink if (!ref->sink_flag->exchange(true, std::memory_order_acquire)) { - ref->restore_sink = !config.flags[config_t::HOST_AUDIO]; - - // If the sink is empty (Host has no sink!), definately switch to the virtual. - if (ref->sink.host.empty()) { + // If the selected sink is different than the current one, change sinks. + ref->restore_sink = ref->sink.host != *sink; + if (ref->restore_sink) { if (control->set_sink(*sink)) { return; } } - // If the client requests audio on the host, don't change the default sink - else if (!config.flags[config_t::HOST_AUDIO] && control->set_sink(*sink)) { - return; - } } + auto frame_size = config.packetDuration * stream->sampleRate / 1000; + auto mic = control->microphone(stream->mapping, stream->channelCount, stream->sampleRate, frame_size); + if (!mic) { + return; + } + + // Audio is initialized, so we don't want to print the failure message + init_failure_fg.disable(); + // Capture takes place on this thread platf::adjust_thread_priority(platf::thread_priority_e::critical); @@ -194,16 +210,8 @@ namespace audio { shutdown_event->view(); }); - auto frame_size = config.packetDuration * stream->sampleRate / 1000; int samples_per_frame = frame_size * stream->channelCount; - auto mic = control->microphone(stream->mapping, stream->channelCount, stream->sampleRate, frame_size); - if (!mic) { - BOOST_LOG(error) << "Couldn't create audio input"sv; - - return; - } - while (!shutdown_event->peek()) { std::vector sample_buffer; sample_buffer.resize(samples_per_frame); @@ -215,14 +223,15 @@ namespace audio { case platf::capture_e::timeout: continue; case platf::capture_e::reinit: + BOOST_LOG(info) << "Reinitializing audio capture"sv; mic.reset(); - mic = control->microphone(stream->mapping, stream->channelCount, stream->sampleRate, frame_size); - if (!mic) { - BOOST_LOG(error) << "Couldn't re-initialize audio input"sv; - - return; - } - return; + do { + mic = control->microphone(stream->mapping, stream->channelCount, stream->sampleRate, frame_size); + if (!mic) { + BOOST_LOG(warning) << "Couldn't re-initialize audio input"sv; + } + } while (!mic && !shutdown_event->view(5s)); + continue; default: return; } @@ -280,7 +289,8 @@ namespace audio { return; } - const std::string &sink = config::audio.sink.empty() ? ctx.sink.host : config::audio.sink; + // Change back to the host sink, unless there was none + const std::string &sink = ctx.sink.host.empty() ? config::audio.sink : ctx.sink.host; if (!sink.empty()) { // Best effort, it's allowed to fail ctx.control->set_sink(sink); diff --git a/src/audio.h b/src/audio.h index 09a99b03321..fe22c94611d 100644 --- a/src/audio.h +++ b/src/audio.h @@ -1,5 +1,8 @@ -#ifndef SUNSHINE_AUDIO_H -#define SUNSHINE_AUDIO_H +/** + * @file src/audio.h + * @brief todo + */ +#pragma once #include "thread_safe.h" #include "utility.h" @@ -44,5 +47,3 @@ namespace audio { void capture(safe::mail_t mail, config_t config, void *channel_data); } // namespace audio - -#endif diff --git a/src/cbs.cpp b/src/cbs.cpp index eedaaff0877..52a3ed93f5e 100644 --- a/src/cbs.cpp +++ b/src/cbs.cpp @@ -1,3 +1,7 @@ +/** + * @file src/cbs.cpp + * @brief todo + */ extern "C" { #include #include @@ -50,7 +54,7 @@ namespace cbs { }; util::buffer_t - write(const cbs::ctx_t &cbs_ctx, std::uint8_t nal, void *uh, AVCodecID codec_id) { + write(cbs::ctx_t &cbs_ctx, std::uint8_t nal, void *uh, AVCodecID codec_id) { cbs::frag_t frag; auto err = ff_cbs_insert_unit_content(&frag, -1, nal, uh, nullptr); if (err < 0) { @@ -87,9 +91,9 @@ namespace cbs { make_sps_h264(const AVCodecContext *ctx) { H264RawSPS sps {}; - /* b_per_p == ctx->max_b_frames for h264 */ - /* desired_b_depth == avoption("b_depth") == 1 */ - /* max_b_depth == std::min(av_log2(ctx->b_per_p) + 1, desired_b_depth) ==> 1 */ + // b_per_p == ctx->max_b_frames for h264 + // desired_b_depth == avoption("b_depth") == 1 + // max_b_depth == std::min(av_log2(ctx->b_per_p) + 1, desired_b_depth) ==> 1 auto max_b_depth = 1; auto dpb_frame = ctx->gop_size == 1 ? 0 : 1 + max_b_depth; auto mb_width = (FFALIGN(ctx->width, 16) / 16) * 16; diff --git a/src/cbs.h b/src/cbs.h index fe532b985c8..575f1e40720 100644 --- a/src/cbs.h +++ b/src/cbs.h @@ -1,5 +1,8 @@ -#ifndef SUNSHINE_CBS_H -#define SUNSHINE_CBS_H +/** + * @file src/cbs.h + * @brief todo + */ +#pragma once #include "utility.h" @@ -28,10 +31,8 @@ namespace cbs { make_sps_h264(const AVCodecContext *ctx, const AVPacket *packet); /** - * Check if SPS->VUI is present - */ + * Check if SPS->VUI is present + */ bool validate_sps(const AVPacket *packet, int codec_id); } // namespace cbs - -#endif diff --git a/src/config.cpp b/src/config.cpp index 2f16cbffce8..6e4c93e37f5 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -1,3 +1,7 @@ +/** + * @file src/config.cpp + * @brief todo + */ #include #include #include @@ -16,6 +20,10 @@ #include "platform/common.h" +#ifdef _WIN32 + #include +#endif + namespace fs = std::filesystem; using namespace std::literals; @@ -374,7 +382,11 @@ namespace config { true // dwmflush }; - audio_t audio {}; + audio_t audio { + {}, // audio_sink + {}, // virtual_sink + true, // install_steam_drivers + }; stream_t stream { 10s, // ping_timeout @@ -402,9 +414,9 @@ namespace config { "1280x720"s, "1920x1080"s, "2560x1080"s, - "3440x1440"s + "3440x1440"s, "1920x1200"s, - "3860x2160"s, + "3840x2160"s, "3840x1600"s, }, // supported resolutions @@ -417,7 +429,7 @@ namespace config { { 0x11, 0xA2 }, { 0x12, 0xA4 }, }, - 2s, // back_button_timeout + -1ms, // back_button_timeout 500ms, // key_repeat_delay std::chrono::duration { 1 / 24.9 }, // key_repeat_period @@ -429,6 +441,7 @@ namespace config { true, // keyboard enabled true, // mouse enabled true, // controller enabled + true, // always send scancodes }; sunshine_t sunshine { @@ -816,12 +829,11 @@ namespace config { boost::property_tree::read_json(jsonStream, jsonTree); for (auto &[_, prep_cmd] : jsonTree.get_child("prep_cmd"s)) { - auto do_cmd = prep_cmd.get("do"s); - auto undo_cmd = prep_cmd.get("undo"s); + 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); - input.emplace_back( - std::move(do_cmd), - std::move(undo_cmd)); + input.emplace_back(do_cmd.value_or(""), undo_cmd.value_or(""), elevated.value_or(false)); } } @@ -975,6 +987,7 @@ namespace config { string_f(vars, "audio_sink", audio.sink); string_f(vars, "virtual_sink", audio.virtual_sink); + bool_f(vars, "install_steam_audio_drivers", audio.install_steam_drivers); string_restricted_f(vars, "origin_pin_allowed", nvhttp.origin_pin_allowed, { "pc"sv, "lan"sv, "wan"sv }); string_restricted_f(vars, "origin_web_ui_allowed", nvhttp.origin_web_ui_allowed, { "pc"sv, "lan"sv, "wan"sv }); @@ -1027,6 +1040,8 @@ namespace config { bool_f(vars, "keyboard", input.keyboard); bool_f(vars, "controller", input.controller); + bool_f(vars, "always_send_scancodes", input.always_send_scancodes); + int port = sunshine.port; int_f(vars, "port"s, port); sunshine.port = (std::uint16_t) port; @@ -1089,6 +1104,10 @@ namespace config { int parse(int argc, char *argv[]) { std::unordered_map cmd_vars; +#ifdef _WIN32 + bool shortcut_launch = false; + bool service_admin_launch = false; +#endif for (auto x = 1; x < argc; ++x) { auto line = argv[x]; @@ -1097,6 +1116,14 @@ namespace config { print_help(*argv); return 1; } +#ifdef _WIN32 + else if (line == "--shortcut"sv) { + shortcut_launch = true; + } + else if (line == "--shortcut-admin"sv) { + service_admin_launch = true; + } +#endif else if (*line == '-') { if (*(line + 1) == '-') { sunshine.cmd.name = line + 2; @@ -1136,23 +1163,90 @@ namespace config { } } - // create appdata folder if it does not exist - if (!boost::filesystem::exists(platf::appdata().string())) { - boost::filesystem::create_directory(platf::appdata().string()); + bool config_loaded = false; + try { + // Create appdata folder if it does not exist + if (!boost::filesystem::exists(platf::appdata().string())) { + boost::filesystem::create_directories(platf::appdata().string()); + } + + // Create empty config file if it does not exist + if (!fs::exists(sunshine.config_file)) { + std::ofstream { sunshine.config_file }; + } + + // Read config file + auto vars = parse_config(read_file(sunshine.config_file.c_str())); + + for (auto &[name, value] : cmd_vars) { + vars.insert_or_assign(std::move(name), std::move(value)); + } + + // Apply the config. Note: This will try to create any paths + // referenced in the config, so we may receive exceptions if + // the path is incorrect or inaccessible. + apply_config(std::move(vars)); + config_loaded = true; + } + catch (const std::filesystem::filesystem_error &err) { + BOOST_LOG(fatal) << "Failed to apply config: "sv << err.what(); + } + catch (const boost::filesystem::filesystem_error &err) { + BOOST_LOG(fatal) << "Failed to apply config: "sv << err.what(); } - // create config file if it does not exist - if (!fs::exists(sunshine.config_file)) { - std::ofstream { sunshine.config_file }; // create empty config file + if (!config_loaded) { +#ifdef _WIN32 + BOOST_LOG(fatal) << "To relaunch Sunshine successfully, use the shortcut in the Start Menu. Do not run Sunshine.exe manually."sv; + std::this_thread::sleep_for(10s); +#endif + return -1; } - auto vars = parse_config(read_file(sunshine.config_file.c_str())); +#ifdef _WIN32 + // We have to wait until the config is loaded to handle these launches, + // because we need to have the correct base port loaded in our config. + if (service_admin_launch) { + // This is a relaunch as admin to start the service + service_ctrl::start_service(); - for (auto &[name, value] : cmd_vars) { - vars.insert_or_assign(std::move(name), std::move(value)); + // Always return 1 to ensure Sunshine doesn't start normally + return 1; } + else if (shortcut_launch) { + if (!service_ctrl::is_service_running()) { + // If the service isn't running, relaunch ourselves as admin to start it + WCHAR executable[MAX_PATH]; + GetModuleFileNameW(NULL, executable, ARRAYSIZE(executable)); + + SHELLEXECUTEINFOW shell_exec_info {}; + shell_exec_info.cbSize = sizeof(shell_exec_info); + shell_exec_info.fMask = SEE_MASK_NOASYNC | SEE_MASK_NO_CONSOLE | SEE_MASK_NOCLOSEPROCESS; + shell_exec_info.lpVerb = L"runas"; + shell_exec_info.lpFile = executable; + shell_exec_info.lpParameters = L"--shortcut-admin"; + shell_exec_info.nShow = SW_NORMAL; + if (!ShellExecuteExW(&shell_exec_info)) { + auto winerr = GetLastError(); + std::cout << "Error: ShellExecuteEx() failed:"sv << winerr << std::endl; + return 1; + } - apply_config(std::move(vars)); + // Wait for the elevated process to finish starting the service + WaitForSingleObject(shell_exec_info.hProcess, INFINITE); + CloseHandle(shell_exec_info.hProcess); + + // Wait for the UI to be ready for connections + service_ctrl::wait_for_ui_ready(); + } + + // Launch the web UI + launch_ui(); + + // Always return 1 to ensure Sunshine doesn't start normally + return 1; + } +#endif return 0; } diff --git a/src/config.h b/src/config.h index fc77748bdde..2c32e7afd1b 100644 --- a/src/config.h +++ b/src/config.h @@ -1,5 +1,8 @@ -#ifndef SUNSHINE_CONFIG_H -#define SUNSHINE_CONFIG_H +/** + * @file src/config.h + * @brief todo + */ +#pragma once #include #include @@ -62,6 +65,7 @@ namespace config { struct audio_t { std::string sink; std::string virtual_sink; + bool install_steam_drivers; }; struct stream_t { @@ -105,6 +109,8 @@ namespace config { bool keyboard; bool mouse; bool controller; + + bool always_send_scancodes; }; namespace flag { @@ -119,14 +125,14 @@ namespace config { } struct prep_cmd_t { - prep_cmd_t(std::string &&do_cmd, std::string &&undo_cmd): - do_cmd(std::move(do_cmd)), undo_cmd(std::move(undo_cmd)) {} - explicit prep_cmd_t(std::string &&do_cmd): - do_cmd(std::move(do_cmd)) {} + 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; }; - struct sunshine_t { int min_log_level; std::bitset flags; @@ -162,4 +168,3 @@ namespace config { std::unordered_map parse_config(const std::string_view &file_content); } // namespace config -#endif diff --git a/src/confighttp.cpp b/src/confighttp.cpp index c7a2a8b3480..6e8b2393730 100644 --- a/src/confighttp.cpp +++ b/src/confighttp.cpp @@ -1,5 +1,9 @@ -// Created by TheElixZammuto on 2021-05-09. -// TODO: Authentication, better handling of routes common to nvhttp, cleanup +/** + * @file src/confighttp.cpp + * @brief todo + * + * @todo Authentication, better handling of routes common to nvhttp, cleanup + */ #define BOOST_BIND_GLOBAL_PLACEHOLDERS @@ -128,7 +132,7 @@ namespace confighttp { auto password = authData.substr(index + 1); auto hash = util::hex(crypto::hash(password + config::sunshine.salt)).to_string(); - if (username != config::sunshine.username || hash != config::sunshine.password) { + if (!boost::iequals(username, config::sunshine.username) || hash != config::sunshine.password) { return false; } @@ -159,7 +163,9 @@ namespace confighttp { std::string header = read_file(WEB_DIR "header.html"); std::string content = read_file(WEB_DIR "index.html"); - response->write(header + content); + SimpleWeb::CaseInsensitiveMultimap headers; + headers.emplace("Content-Type", "text/html; charset=utf-8"); + response->write(header + content, headers); } void @@ -170,7 +176,9 @@ namespace confighttp { std::string header = read_file(WEB_DIR "header.html"); std::string content = read_file(WEB_DIR "pin.html"); - response->write(header + content); + SimpleWeb::CaseInsensitiveMultimap headers; + headers.emplace("Content-Type", "text/html; charset=utf-8"); + response->write(header + content, headers); } void @@ -179,11 +187,11 @@ namespace confighttp { print_req(request); - SimpleWeb::CaseInsensitiveMultimap headers; - headers.emplace("Access-Control-Allow-Origin", "https://images.igdb.com/"); - std::string header = read_file(WEB_DIR "header.html"); std::string content = read_file(WEB_DIR "apps.html"); + SimpleWeb::CaseInsensitiveMultimap headers; + headers.emplace("Content-Type", "text/html; charset=utf-8"); + headers.emplace("Access-Control-Allow-Origin", "https://images.igdb.com/"); response->write(header + content, headers); } @@ -195,7 +203,9 @@ namespace confighttp { std::string header = read_file(WEB_DIR "header.html"); std::string content = read_file(WEB_DIR "clients.html"); - response->write(header + content); + SimpleWeb::CaseInsensitiveMultimap headers; + headers.emplace("Content-Type", "text/html; charset=utf-8"); + response->write(header + content, headers); } void @@ -206,7 +216,9 @@ namespace confighttp { std::string header = read_file(WEB_DIR "header.html"); std::string content = read_file(WEB_DIR "config.html"); - response->write(header + content); + SimpleWeb::CaseInsensitiveMultimap headers; + headers.emplace("Content-Type", "text/html; charset=utf-8"); + response->write(header + content, headers); } void @@ -217,7 +229,9 @@ namespace confighttp { std::string header = read_file(WEB_DIR "header.html"); std::string content = read_file(WEB_DIR "password.html"); - response->write(header + content); + SimpleWeb::CaseInsensitiveMultimap headers; + headers.emplace("Content-Type", "text/html; charset=utf-8"); + response->write(header + content, headers); } void @@ -229,7 +243,9 @@ namespace confighttp { } std::string header = read_file(WEB_DIR "header-no-nav.html"); std::string content = read_file(WEB_DIR "welcome.html"); - response->write(header + content); + SimpleWeb::CaseInsensitiveMultimap headers; + headers.emplace("Content-Type", "text/html; charset=utf-8"); + response->write(header + content, headers); } void @@ -240,7 +256,9 @@ namespace confighttp { std::string header = read_file(WEB_DIR "header.html"); std::string content = read_file(WEB_DIR "troubleshooting.html"); - response->write(header + content); + SimpleWeb::CaseInsensitiveMultimap headers; + headers.emplace("Content-Type", "text/html; charset=utf-8"); + response->write(header + content, headers); } void @@ -314,7 +332,9 @@ namespace confighttp { print_req(request); std::string content = read_file(config::stream.file_apps.c_str()); - response->write(content); + SimpleWeb::CaseInsensitiveMultimap headers; + headers.emplace("Content-Type", "application/json"); + response->write(content, headers); } void @@ -488,7 +508,7 @@ namespace confighttp { const std::string coverdir = platf::appdata().string() + "/covers/"; if (!boost::filesystem::exists(coverdir)) { - boost::filesystem::create_directory(coverdir); + boost::filesystem::create_directories(coverdir); } std::basic_string path = coverdir + http::url_escape(key) + ".png"; @@ -528,7 +548,6 @@ namespace confighttp { outputTree.put("status", "true"); outputTree.put("platform", SUNSHINE_PLATFORM); outputTree.put("version", PROJECT_VER); - outputTree.put("restart_supported", platf::restart_supported()); auto vars = config::parse_config(read_file(config::sunshine.config_file.c_str())); @@ -579,30 +598,8 @@ namespace confighttp { print_req(request); - std::stringstream ss; - std::stringstream configStream; - ss << request->content.rdbuf(); - pt::ptree outputTree; - auto g = util::fail_guard([&]() { - std::ostringstream data; - - pt::write_json(data, outputTree); - response->write(data.str()); - }); - - if (!platf::restart_supported()) { - outputTree.put("status", false); - outputTree.put("error", "Restart is not currently supported on this platform"); - return; - } - - if (!platf::restart()) { - outputTree.put("status", false); - outputTree.put("error", "Restart failed"); - return; - } - - outputTree.put("status", true); + // We may not return from this call + platf::restart(); } void @@ -638,7 +635,7 @@ namespace confighttp { } else { auto hash = util::hex(crypto::hash(password + config::sunshine.salt)).to_string(); - if (config::sunshine.username.empty() || (username == config::sunshine.username && hash == config::sunshine.password)) { + if (config::sunshine.username.empty() || (boost::iequals(username, config::sunshine.username) && hash == config::sunshine.password)) { if (newPassword.empty() || newPassword != confirmPassword) { outputTree.put("status", false); outputTree.put("error", "Password Mismatch"); diff --git a/src/confighttp.h b/src/confighttp.h index f5fe158af19..471ed9eba07 100644 --- a/src/confighttp.h +++ b/src/confighttp.h @@ -1,7 +1,8 @@ -// Created by loki on 6/3/19. - -#ifndef SUNSHINE_CONFIGHTTP_H -#define SUNSHINE_CONFIGHTTP_H +/** + * @file src/confighttp.h + * @brief todo + */ +#pragma once #include #include @@ -34,5 +35,3 @@ const std::map mime_types = { { "woff2", "font/woff2" }, { "xml", "text/xml" }, }; - -#endif // SUNSHINE_CONFIGHTTP_H diff --git a/src/crypto.cpp b/src/crypto.cpp index f50c536cbab..5dec0f8dd57 100644 --- a/src/crypto.cpp +++ b/src/crypto.cpp @@ -1,5 +1,7 @@ -// Created by loki on 5/31/19. - +/** + * @file src/crypto.cpp + * @brief todo + */ #include "crypto.h" #include @@ -35,13 +37,13 @@ namespace crypto { } } - /* - * When certificates from two or more instances of Moonlight have been added to x509_store_t, - * only one of them will be verified by X509_verify_cert, resulting in only a single instance of - * Moonlight to be able to use Sunshine - * - * To circumvent this, x509_store_t instance will be created for each instance of the certificates. - */ + /** + * When certificates from two or more instances of Moonlight have been added to x509_store_t, + * only one of them will be verified by X509_verify_cert, resulting in only a single instance of + * Moonlight to be able to use Sunshine + * + * To circumvent this, x509_store_t instance will be created for each instance of the certificates. + */ const char * cert_chain_t::verify(x509_t::element_type *cert) { int err_code = 0; @@ -399,7 +401,7 @@ namespace crypto { sign(const pkey_t &pkey, const std::string_view &data, const EVP_MD *md) { md_ctx_t ctx { EVP_MD_CTX_create() }; - if (EVP_DigestSignInit(ctx.get(), nullptr, md, nullptr, pkey.get()) != 1) { + if (EVP_DigestSignInit(ctx.get(), nullptr, md, nullptr, (EVP_PKEY *) pkey.get()) != 1) { return {}; } @@ -472,7 +474,7 @@ namespace crypto { bool verify(const x509_t &x509, const std::string_view &data, const std::string_view &signature, const EVP_MD *md) { - auto pkey = X509_get_pubkey(x509.get()); + auto pkey = X509_get0_pubkey(x509.get()); md_ctx_t ctx { EVP_MD_CTX_create() }; diff --git a/src/crypto.h b/src/crypto.h index 2de5bafaf4a..d8d0a35a607 100644 --- a/src/crypto.h +++ b/src/crypto.h @@ -1,7 +1,8 @@ -// Created by loki on 6/1/19. - -#ifndef SUNSHINE_CRYPTO_H -#define SUNSHINE_CRYPTO_H +/** + * @file src/crypto.h + * @brief todo + */ +#pragma once #include #include @@ -123,11 +124,11 @@ namespace crypto { gcm_t(const crypto::aes_t &key, bool padding = true); /** - * length of cipher must be at least: round_to_pkcs7_padded(plaintext.size()) + crypto::cipher::tag_size - * - * return -1 on error - * return bytes written on success - */ + * length of cipher must be at least: round_to_pkcs7_padded(plaintext.size()) + crypto::cipher::tag_size + * + * return -1 on error + * return bytes written on success + */ int encrypt(const std::string_view &plaintext, std::uint8_t *tagged_cipher, aes_t *iv); @@ -145,15 +146,13 @@ namespace crypto { cbc_t(const crypto::aes_t &key, bool padding = true); /** - * length of cipher must be at least: round_to_pkcs7_padded(plaintext.size()) - * - * return -1 on error - * return bytes written on success - */ + * length of cipher must be at least: round_to_pkcs7_padded(plaintext.size()) + * + * return -1 on error + * return bytes written on success + */ int encrypt(const std::string_view &plaintext, std::uint8_t *cipher, aes_t *iv); }; } // namespace cipher } // namespace crypto - -#endif //SUNSHINE_CRYPTO_H diff --git a/src/httpcommon.cpp b/src/httpcommon.cpp index 24810f51663..2cfbfe0dd42 100644 --- a/src/httpcommon.cpp +++ b/src/httpcommon.cpp @@ -1,3 +1,7 @@ +/** + * @file src/httpcommon.cpp + * @brief todo + */ #define BOOST_BIND_GLOBAL_PLACEHOLDERS #include "process.h" @@ -89,7 +93,7 @@ namespace http { pt::write_json(file, outputTree); } catch (std::exception &e) { - BOOST_LOG(error) << "generating user credentials: "sv << e.what(); + BOOST_LOG(error) << "error writing to the credentials file, perhaps try this again as an administrator? Details: "sv << e.what(); return -1; } diff --git a/src/httpcommon.h b/src/httpcommon.h index 4deecd143f6..02d42d265fa 100644 --- a/src/httpcommon.h +++ b/src/httpcommon.h @@ -1,3 +1,9 @@ +/** + * @file src/httpcommon.h + * @brief todo + */ +#pragma once + #include "network.h" #include "thread_safe.h" diff --git a/src/input.cpp b/src/input.cpp index b0fc2791fa9..a9a38e2d91c 100644 --- a/src/input.cpp +++ b/src/input.cpp @@ -1,5 +1,7 @@ -// Created by loki on 6/20/19. - +/** + * @file src/input.cpp + * @brief todo + */ // define uint32_t for #include extern "C" { @@ -17,6 +19,9 @@ extern "C" { #include "thread_pool.h" #include "utility.h" +#include +#include + using namespace std::literals; namespace input { @@ -59,8 +64,22 @@ namespace input { gamepad_mask[id] = false; } + typedef uint32_t key_press_id_t; + key_press_id_t + make_kpid(uint16_t vk, uint8_t flags) { + return (key_press_id_t) vk << 8 | flags; + } + uint16_t + vk_from_kpid(key_press_id_t kpid) { + return kpid >> 8; + } + uint8_t + flags_from_kpid(key_press_id_t kpid) { + return kpid & 0xFF; + } + static task_pool_util::TaskPool::task_id_t key_press_repeat_id {}; - static std::unordered_map key_press {}; + static std::unordered_map key_press {}; static std::array mouse_press {}; static platf::input_t platf_input; @@ -116,7 +135,7 @@ namespace input { touch_port_event { std::move(touch_port_event) }, rumble_queue { std::move(rumble_queue) }, mouse_left_button_timeout {}, - touch_port { 0, 0, 0, 0, 0, 0, 1.0f } {} + touch_port { { 0, 0, 0, 0 }, 0, 0, 1.0f } {} // Keep track of alt+ctrl+shift key combo int shortcutFlags; @@ -133,12 +152,12 @@ namespace input { }; /** - * Apply shortcut based on VKEY - * On success - * return > 0 - * On nothing - * return 0 - */ + * Apply shortcut based on VKEY + * On success + * return > 0 + * On nothing + * return 0 + */ inline int apply_shortcut(short keyCode) { constexpr auto VK_F1 = 0x70; @@ -352,21 +371,20 @@ namespace input { mouse_press[button] = !release; } - /////////////////////////////////// - /*/ - * When Moonlight sends mouse input through absolute coordinates, - * it's possible that BUTTON_RIGHT is pressed down immediately after releasing BUTTON_LEFT. - * As a result, Sunshine will left-click on hyperlinks in the browser before right-clicking - * - * This can be solved by delaying BUTTON_LEFT, however, any delay on input is undesirable during gaming - * As a compromise, Sunshine will only put delays on BUTTON_LEFT when - * absolute mouse coordinates have been sent. - * - * Try to make sure BUTTON_RIGHT gets called before BUTTON_LEFT is released. - * - * input->mouse_left_button_timeout can only be nullptr - * when the last mouse coordinates were absolute - /*/ + /** + * When Moonlight sends mouse input through absolute coordinates, + * it's possible that BUTTON_RIGHT is pressed down immediately after releasing BUTTON_LEFT. + * As a result, Sunshine will left-click on hyperlinks in the browser before right-clicking + * + * This can be solved by delaying BUTTON_LEFT, however, any delay on input is undesirable during gaming + * As a compromise, Sunshine will only put delays on BUTTON_LEFT when + * absolute mouse coordinates have been sent. + * + * Try to make sure BUTTON_RIGHT gets called before BUTTON_LEFT is released. + * + * input->mouse_left_button_timeout can only be nullptr + * when the last mouse coordinates were absolute + */ if (button == BUTTON_LEFT && release && !input->mouse_left_button_timeout) { auto f = [=]() { auto left_released = mouse_press[BUTTON_LEFT]; @@ -394,7 +412,6 @@ namespace input { return; } - /////////////////////////////////// platf::button_mouse(platf_input, button, release); } @@ -410,8 +427,8 @@ namespace input { } /** - * Update flags for keyboard shortcut combo's - */ + * Update flags for keyboard shortcut combo's + */ inline void update_shortcutFlags(int *flags, short keyCode, bool release) { switch (keyCode) { @@ -448,17 +465,66 @@ namespace input { } } + bool + is_modifier(uint16_t keyCode) { + switch (keyCode) { + case VKEY_SHIFT: + case VKEY_LSHIFT: + case VKEY_RSHIFT: + case VKEY_CONTROL: + case VKEY_LCONTROL: + case VKEY_RCONTROL: + case VKEY_MENU: + case VKEY_LMENU: + case VKEY_RMENU: + return true; + default: + return false; + } + } + void - repeat_key(short key_code) { + send_key_and_modifiers(uint16_t key_code, bool release, uint8_t flags, uint8_t synthetic_modifiers) { + if (!release) { + // Press any synthetic modifiers required for this key + if (synthetic_modifiers & MODIFIER_SHIFT) { + platf::keyboard(platf_input, VKEY_SHIFT, false, flags); + } + if (synthetic_modifiers & MODIFIER_CTRL) { + platf::keyboard(platf_input, VKEY_CONTROL, false, flags); + } + if (synthetic_modifiers & MODIFIER_ALT) { + platf::keyboard(platf_input, VKEY_MENU, false, flags); + } + } + + platf::keyboard(platf_input, map_keycode(key_code), release, flags); + + if (!release) { + // Raise any synthetic modifier keys we pressed + if (synthetic_modifiers & MODIFIER_SHIFT) { + platf::keyboard(platf_input, VKEY_SHIFT, true, flags); + } + if (synthetic_modifiers & MODIFIER_CTRL) { + platf::keyboard(platf_input, VKEY_CONTROL, true, flags); + } + if (synthetic_modifiers & MODIFIER_ALT) { + platf::keyboard(platf_input, VKEY_MENU, true, flags); + } + } + } + + void + repeat_key(uint16_t key_code, uint8_t flags, uint8_t synthetic_modifiers) { // If key no longer pressed, stop repeating - if (!key_press[key_code]) { + if (!key_press[make_kpid(key_code, flags)]) { key_press_repeat_id = nullptr; return; } - platf::keyboard(platf_input, map_keycode(key_code), false); + send_key_and_modifiers(key_code, false, flags, synthetic_modifiers); - key_press_repeat_id = task_pool.pushDelayed(repeat_key, config::input.key_repeat_period, key_code).task_id; + key_press_repeat_id = task_pool.pushDelayed(repeat_key, config::input.key_repeat_period, key_code, flags, synthetic_modifiers).task_id; } void @@ -470,7 +536,22 @@ namespace input { auto release = util::endian::little(packet->header.magic) == KEY_UP_EVENT_MAGIC; auto keyCode = packet->keyCode & 0x00FF; - auto &pressed = key_press[keyCode]; + // Set synthetic modifier flags if the keyboard packet is requesting modifier + // keys that are not current pressed. + uint8_t synthetic_modifiers = 0; + if (!release && !is_modifier(keyCode)) { + if (!(input->shortcutFlags & input_t::SHIFT) && (packet->modifiers & MODIFIER_SHIFT)) { + synthetic_modifiers |= MODIFIER_SHIFT; + } + if (!(input->shortcutFlags & input_t::CTRL) && (packet->modifiers & MODIFIER_CTRL)) { + synthetic_modifiers |= MODIFIER_CTRL; + } + if (!(input->shortcutFlags & input_t::ALT) && (packet->modifiers & MODIFIER_ALT)) { + synthetic_modifiers |= MODIFIER_ALT; + } + } + + auto &pressed = key_press[make_kpid(keyCode, packet->flags)]; if (!pressed) { if (!release) { // A new key has been pressed down, we need to check for key combo's @@ -484,7 +565,7 @@ namespace input { } if (config::input.key_repeat_delay.count() > 0) { - key_press_repeat_id = task_pool.pushDelayed(repeat_key, config::input.key_repeat_delay, keyCode).task_id; + key_press_repeat_id = task_pool.pushDelayed(repeat_key, config::input.key_repeat_delay, keyCode, packet->flags, synthetic_modifiers).task_id; } } else { @@ -499,8 +580,9 @@ namespace input { pressed = !release; + send_key_and_modifiers(keyCode, release, packet->flags, synthetic_modifiers); + update_shortcutFlags(&input->shortcutFlags, map_keycode(keyCode), release); - platf::keyboard(platf_input, map_keycode(keyCode), release); } void @@ -655,6 +737,9 @@ namespace input { state.buttonFlags |= platf::HOME; platf::gamepad(platf_input, gamepad.id, state); + // Sleep for a short time to allow the input to be detected + boost::this_thread::sleep_for(boost::chrono::milliseconds(100)); + // Release Home button state.buttonFlags &= ~platf::HOME; platf::gamepad(platf_input, gamepad.id, state); @@ -731,7 +816,7 @@ namespace input { } for (auto &kp : key_press) { - platf::keyboard(platf_input, kp.first & 0x00FF, true); + platf::keyboard(platf_input, vk_from_kpid(kp.first) & 0x00FF, true, flags_from_kpid(kp.first)); key_press[kp.first] = false; } }); diff --git a/src/input.h b/src/input.h index 2001fb9e21f..095c20ee717 100644 --- a/src/input.h +++ b/src/input.h @@ -1,7 +1,8 @@ -// Created by loki on 6/20/19. - -#ifndef SUNSHINE_INPUT_H -#define SUNSHINE_INPUT_H +/** + * @file src/input.h + * @brief todo + */ +#pragma once #include @@ -33,5 +34,3 @@ namespace input { float scalar_inv; }; } // namespace input - -#endif // SUNSHINE_INPUT_H diff --git a/src/main.cpp b/src/main.cpp index 941dcfc194c..b347cc0ae5a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,5 +1,6 @@ /** - * @file main.cpp + * @file src/main.cpp + * @brief Main entry point for Sunshine. */ // standard includes @@ -34,6 +35,10 @@ extern "C" { #include #include + +#ifdef _WIN32 + #include +#endif } safe::mail_t mail::man; @@ -107,6 +112,263 @@ namespace version { } } // namespace version +namespace lifetime { + static char **argv; + static std::atomic_int desired_exit_code; + + /** + * @brief Terminates Sunshine gracefully with the provided exit code. + * @param exit_code The exit code to return from main(). + * @param async Specifies whether our termination will be non-blocking. + */ + void + exit_sunshine(int exit_code, bool async) { + // Store the exit code of the first exit_sunshine() call + int zero = 0; + desired_exit_code.compare_exchange_strong(zero, exit_code); + + // Raise SIGINT to start termination + std::raise(SIGINT); + + // Termination will happen asynchronously, but the caller may + // have wanted synchronous behavior. + while (!async) { + std::this_thread::sleep_for(1s); + } + } + + /** + * @brief Gets the argv array passed to main(). + */ + char ** + get_argv() { + return argv; + } +} // namespace lifetime + +#ifdef _WIN32 +namespace service_ctrl { + class service_controller { + public: + /** + * @brief Constructor for service_controller class. + * @param service_desired_access SERVICE_* desired access flags. + */ + service_controller(DWORD service_desired_access) { + scm_handle = OpenSCManagerA(nullptr, nullptr, SC_MANAGER_CONNECT); + if (!scm_handle) { + auto winerr = GetLastError(); + BOOST_LOG(error) << "OpenSCManager() failed: "sv << winerr; + return; + } + + service_handle = OpenServiceA(scm_handle, "SunshineService", service_desired_access); + if (!service_handle) { + auto winerr = GetLastError(); + BOOST_LOG(error) << "OpenService() failed: "sv << winerr; + return; + } + } + + ~service_controller() { + if (service_handle) { + CloseServiceHandle(service_handle); + } + + if (scm_handle) { + CloseServiceHandle(scm_handle); + } + } + + /** + * @brief Asynchronously starts the Sunshine service. + */ + bool + start_service() { + if (!service_handle) { + return false; + } + + if (!StartServiceA(service_handle, 0, nullptr)) { + auto winerr = GetLastError(); + if (winerr != ERROR_SERVICE_ALREADY_RUNNING) { + BOOST_LOG(error) << "StartService() failed: "sv << winerr; + return false; + } + } + + return true; + } + + /** + * @brief Query the service status. + * @param status The SERVICE_STATUS struct to populate. + */ + bool + query_service_status(SERVICE_STATUS &status) { + if (!service_handle) { + return false; + } + + if (!QueryServiceStatus(service_handle, &status)) { + auto winerr = GetLastError(); + BOOST_LOG(error) << "QueryServiceStatus() failed: "sv << winerr; + return false; + } + + return true; + } + + private: + SC_HANDLE scm_handle = NULL; + SC_HANDLE service_handle = NULL; + }; + + /** + * @brief Check if the service is running. + * + * EXAMPLES: + * ```cpp + * is_service_running(); + * ``` + */ + bool + is_service_running() { + service_controller sc { SERVICE_QUERY_STATUS }; + + SERVICE_STATUS status; + if (!sc.query_service_status(status)) { + return false; + } + + return status.dwCurrentState == SERVICE_RUNNING; + } + + /** + * @brief Start the service and wait for startup to complete. + * + * EXAMPLES: + * ```cpp + * start_service(); + * ``` + */ + bool + start_service() { + service_controller sc { SERVICE_QUERY_STATUS | SERVICE_START }; + + std::cout << "Starting Sunshine..."sv; + + // This operation is asynchronous, so we must wait for it to complete + if (!sc.start_service()) { + return false; + } + + SERVICE_STATUS status; + do { + Sleep(1000); + std::cout << '.'; + } while (sc.query_service_status(status) && status.dwCurrentState == SERVICE_START_PENDING); + + if (status.dwCurrentState != SERVICE_RUNNING) { + BOOST_LOG(error) << SERVICE_NAME " failed to start: "sv << status.dwWin32ExitCode; + return false; + } + + std::cout << std::endl; + return true; + } + + /** + * @brief Wait for the UI to be ready after Sunshine startup. + * + * EXAMPLES: + * ```cpp + * wait_for_ui_ready(); + * ``` + */ + bool + wait_for_ui_ready() { + std::cout << "Waiting for Web UI to be ready..."; + + // Wait up to 30 seconds for the web UI to start + for (int i = 0; i < 30; i++) { + PMIB_TCPTABLE tcp_table = nullptr; + ULONG table_size = 0; + ULONG err; + + auto fg = util::fail_guard([&tcp_table]() { + free(tcp_table); + }); + + do { + // Query all open TCP sockets to look for our web UI port + err = GetTcpTable(tcp_table, &table_size, false); + if (err == ERROR_INSUFFICIENT_BUFFER) { + free(tcp_table); + tcp_table = (PMIB_TCPTABLE) malloc(table_size); + } + } while (err == ERROR_INSUFFICIENT_BUFFER); + + if (err != NO_ERROR) { + BOOST_LOG(error) << "Failed to query TCP table: "sv << err; + return false; + } + + uint16_t port_nbo = htons(map_port(confighttp::PORT_HTTPS)); + for (DWORD i = 0; i < tcp_table->dwNumEntries; i++) { + auto &entry = tcp_table->table[i]; + + // Look for our port in the listening state + if (entry.dwLocalPort == port_nbo && entry.dwState == MIB_TCP_STATE_LISTEN) { + std::cout << std::endl; + return true; + } + } + + Sleep(1000); + std::cout << '.'; + } + + std::cout << "timed out"sv << std::endl; + return false; + } +} // namespace service_ctrl + +/** + * @brief Checks if NVIDIA's GameStream software is running. + * @return `true` if GameStream is enabled. + */ +bool +is_gamestream_enabled() { + DWORD enabled; + DWORD size = sizeof(enabled); + return RegGetValueW( + HKEY_LOCAL_MACHINE, + L"SOFTWARE\\NVIDIA Corporation\\NvStream", + L"EnableStreaming", + RRF_RT_REG_DWORD, + nullptr, + &enabled, + &size) == ERROR_SUCCESS && + enabled != 0; +} + +#endif + +/** + * @brief Launch the Web UI. + * + * EXAMPLES: + * ```cpp + * launch_ui(); + * ``` + */ +void +launch_ui() { + std::string url = "https://localhost:" + std::to_string(map_port(confighttp::PORT_HTTPS)); + platf::open_url(url); +} + /** * @brief Flush the log. * @@ -159,13 +421,9 @@ LRESULT CALLBACK SessionMonitorWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { switch (uMsg) { case WM_ENDSESSION: { - // Raise a SIGINT to trigger our cleanup logic and terminate ourselves + // Terminate ourselves with a blocking exit call std::cout << "Received WM_ENDSESSION"sv << std::endl; - std::raise(SIGINT); - - // The signal handling is asynchronous, so we will wait here to be terminated. - // If for some reason we don't terminate in a few seconds, Windows will kill us. - SuspendThread(GetCurrentThread()); + lifetime::exit_sunshine(0, false); return 0; } default: @@ -186,6 +444,8 @@ SessionMonitorWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { */ int main(int argc, char *argv[]) { + lifetime::argv = argv; + task_pool_util::TaskPool::task_id_t force_shutdown = nullptr; #ifdef _WIN32 @@ -354,14 +614,26 @@ main(int argc, char *argv[]) { BOOST_LOG(error) << "Platform failed to initialize"sv; } + auto proc_deinit_guard = proc::init(); + if (!proc_deinit_guard) { + BOOST_LOG(error) << "Proc failed to initialize"sv; + } + reed_solomon_init(); auto input_deinit_guard = input::init(); - if (video::init()) { - BOOST_LOG(error) << "Video failed to initialize"sv; + if (video::probe_encoders()) { + BOOST_LOG(error) << "Video failed to find working encoder"sv; } if (http::init()) { - BOOST_LOG(error) << "http failed to initialize"sv; + BOOST_LOG(fatal) << "HTTP interface failed to initialize"sv; + +#ifdef _WIN32 + BOOST_LOG(fatal) << "To relaunch Sunshine successfully, use the shortcut in the Start Menu. Do not run Sunshine.exe manually."sv; + std::this_thread::sleep_for(10s); +#endif + + return -1; } std::unique_ptr mDNS; @@ -376,12 +648,20 @@ main(int argc, char *argv[]) { // FIXME: Temporary workaround: Simple-Web_server needs to be updated or replaced if (shutdown_event->peek()) { - return 0; + return lifetime::desired_exit_code; } std::thread httpThread { nvhttp::start }; std::thread configThread { confighttp::start }; +#ifdef _WIN32 + // If we're using the default port and GameStream is enabled, warn the user + if (config::sunshine.port == 47989 && is_gamestream_enabled()) { + BOOST_LOG(fatal) << "GameStream is still enabled in GeForce Experience! This *will* cause streaming problems with Sunshine!"sv; + BOOST_LOG(fatal) << "Disable GameStream on the SHIELD tab in GeForce Experience or change the Port setting on the Advanced tab in the Sunshine Web UI."sv; + } +#endif + rtsp_stream::rtpThread(); httpThread.join(); @@ -395,7 +675,7 @@ main(int argc, char *argv[]) { system_tray::end_tray(); #endif - return 0; + return lifetime::desired_exit_code; } /** diff --git a/src/main.h b/src/main.h index aff1cfdd6bb..e98206f3838 100644 --- a/src/main.h +++ b/src/main.h @@ -1,10 +1,10 @@ /** - * @file main.h + * @file src/main.h + * @brief Main header file for the Sunshine application. */ // macros -#ifndef SUNSHINE_MAIN_H -#define SUNSHINE_MAIN_H +#pragma once // standard includes #include @@ -33,20 +33,6 @@ main(int argc, char *argv[]); void log_flush(); void -open_url(const std::string &url); -void -tray_open_ui_cb(struct tray_menu *item); -void -tray_donate_github_cb(struct tray_menu *item); -void -tray_donate_mee6_cb(struct tray_menu *item); -void -tray_donate_patreon_cb(struct tray_menu *item); -void -tray_donate_paypal_cb(struct tray_menu *item); -void -tray_quit_cb(struct tray_menu *item); -void print_help(const char *name); std::string read_file(const char *path); @@ -54,6 +40,8 @@ int write_file(const char *path, const std::string_view &contents); std::uint16_t map_port(int port); +void +launch_ui(); // namespaces namespace mail { @@ -79,4 +67,23 @@ namespace mail { #undef MAIL } // namespace mail -#endif // SUNSHINE_MAIN_H + +namespace lifetime { + void + exit_sunshine(int exit_code, bool async); + char ** + get_argv(); +} // namespace lifetime + +#ifdef _WIN32 +namespace service_ctrl { + bool + is_service_running(); + + bool + start_service(); + + bool + wait_for_ui_ready(); +} // namespace service_ctrl +#endif diff --git a/src/move_by_copy.h b/src/move_by_copy.h index 3c2bda1208e..bac1faf98be 100644 --- a/src/move_by_copy.h +++ b/src/move_by_copy.h @@ -1,12 +1,15 @@ -#ifndef DOSSIER_MOVE_BY_COPY_H -#define DOSSIER_MOVE_BY_COPY_H +/** + * @file src/move_by_copy.h + * @brief todo + */ +#pragma once #include namespace move_by_copy_util { - /* - * When a copy is made, it moves the object - * This allows you to move an object when a move can't be done. - */ + /** + * When a copy is made, it moves the object + * This allows you to move an object when a move can't be done. + */ template class MoveByCopy { public: @@ -53,4 +56,3 @@ namespace move_by_copy_util { return MoveByCopy(std::move(const_cast(movable))); } } // namespace move_by_copy_util -#endif diff --git a/src/network.cpp b/src/network.cpp index 6549127c524..f65bdfaae89 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -1,5 +1,7 @@ -// Created by loki on 12/27/19. - +/** + * @file src/network.cpp + * @brief todo + */ #include "network.h" #include "utility.h" #include @@ -16,7 +18,8 @@ namespace net { std::vector> lan_ips { ip_block("192.168.0.0/16"sv), ip_block("172.16.0.0/12"sv), - ip_block("10.0.0.0/8"sv) + ip_block("10.0.0.0/8"sv), + ip_block("100.64.0.0/10"sv) }; std::uint32_t diff --git a/src/network.h b/src/network.h index ffed2fe692b..e1ca36c7531 100644 --- a/src/network.h +++ b/src/network.h @@ -1,7 +1,8 @@ -// Created by loki on 12/27/19. - -#ifndef SUNSHINE_NETWORK_H -#define SUNSHINE_NETWORK_H +/** + * @file src/network.h + * @brief todo + */ +#pragma once #include @@ -34,5 +35,3 @@ namespace net { host_t host_create(ENetAddress &addr, std::size_t peers, std::uint16_t port); } // namespace net - -#endif // SUNSHINE_NETWORK_H diff --git a/src/nvhttp.cpp b/src/nvhttp.cpp index 15fc1f349a6..dfa0b9fef76 100644 --- a/src/nvhttp.cpp +++ b/src/nvhttp.cpp @@ -1,6 +1,7 @@ /** -* @file nvhttp.h -*/ + * @file src/nvhttp.h + * @brief todo + */ // macros #define BOOST_BIND_GLOBAL_PLACEHOLDERS @@ -29,6 +30,7 @@ #include "rtsp.h" #include "utility.h" #include "uuid.h" +#include "video.h" using namespace std::literals; namespace nvhttp { @@ -279,6 +281,7 @@ namespace nvhttp { if (sess.async_insert_pin.salt.size() < 32) { tree.put("root.paired", 0); tree.put("root..status_code", 400); + tree.put("root..status_message", "Salt too short"); return; } @@ -374,14 +377,7 @@ namespace nvhttp { auto hash = crypto::hash(data); // if hash not correct, probably MITM - if (std::memcmp(hash.data(), sess.clienthash.data(), hash.size())) { - // TODO: log - - map_id_sess.erase(client.uniqueID); - tree.put("root.paired", 0); - } - - if (crypto::verify256(crypto::x509(client.cert), secret, sign)) { + if (!std::memcmp(hash.data(), sess.clienthash.data(), hash.size()) && crypto::verify256(crypto::x509(client.cert), secret, sign)) { tree.put("root.paired", 1); add_cert->raise(crypto::x509(client.cert)); @@ -470,11 +466,12 @@ namespace nvhttp { auto args = request->parse_query_string(); if (args.find("uniqueid"s) == std::end(args)) { tree.put("root..status_code", 400); + tree.put("root..status_message", "Missing uniqueid parameter"); return; } - auto uniqID { std::move(get_arg(args, "uniqueid")) }; + auto uniqID { get_arg(args, "uniqueid") }; auto sess_it = map_id_sess.find(uniqID); args_t::const_iterator it; @@ -521,19 +518,20 @@ namespace nvhttp { } else { tree.put("root..status_code", 404); + tree.put("root..status_message", "Invalid pairing request"); } } /** - * @brief Compare the user supplied pin to the Moonlight pin. - * @param pin The user supplied pin. - * @return `true` if the pin is correct, `false` otherwise. - * - * EXAMPLES: - * ```cpp - * bool pin_status = nvhttp::pin("1234"); - * ``` - */ + * @brief Compare the user supplied pin to the Moonlight pin. + * @param pin The user supplied pin. + * @return `true` if the pin is correct, `false` otherwise. + * + * EXAMPLES: + * ```cpp + * bool pin_status = nvhttp::pin("1234"); + * ``` + */ bool pin(std::string pin) { pt::ptree tree; @@ -621,23 +619,19 @@ namespace nvhttp { tree.put("root.HttpsPort", map_port(PORT_HTTPS)); tree.put("root.ExternalPort", map_port(PORT_HTTP)); tree.put("root.mac", platf::get_mac_address(local_endpoint.address().to_string())); - tree.put("root.MaxLumaPixelsHEVC", config::video.hevc_mode > 1 ? "1869449984" : "0"); + tree.put("root.MaxLumaPixelsHEVC", video::active_hevc_mode > 1 ? "1869449984" : "0"); tree.put("root.LocalIP", local_endpoint.address().to_string()); - if (config::video.hevc_mode == 3) { + if (video::active_hevc_mode == 3) { tree.put("root.ServerCodecModeSupport", "3843"); } - else if (config::video.hevc_mode == 2) { + else if (video::active_hevc_mode == 2) { tree.put("root.ServerCodecModeSupport", "259"); } else { tree.put("root.ServerCodecModeSupport", "3"); } - if (!config::nvhttp.external_ip.empty()) { - tree.put("root.ExternalIP", config::nvhttp.external_ip); - } - pt::ptree display_nodes; for (auto &resolution : config::nvhttp.resolutions) { auto pred = [](auto ch) { return ch == ' ' || ch == '\t' || ch == 'x'; }; @@ -689,22 +683,6 @@ namespace nvhttp { response->close_connection_after_response = true; }); - auto args = request->parse_query_string(); - if (args.find("uniqueid"s) == std::end(args)) { - tree.put("root..status_code", 400); - - return; - } - - auto clientID = get_arg(args, "uniqueid"); - - auto client = map_id_client.find(clientID); - if (client == std::end(map_id_client)) { - tree.put("root..status_code", 501); - - return; - } - auto &apps = tree.add_child("root", pt::ptree {}); apps.put(".status_code", 200); @@ -712,7 +690,7 @@ namespace nvhttp { for (auto &proc : proc::proc.get_apps()) { pt::ptree app; - app.put("IsHdrSupported"s, config::video.hevc_mode == 3 ? 1 : 0); + app.put("IsHdrSupported"s, video::active_hevc_mode == 3 ? 1 : 0); app.put("AppTitle"s, proc.name); app.put("ID", proc.id); @@ -736,6 +714,7 @@ namespace nvhttp { if (rtsp_stream::session_count() == config::stream.channels) { tree.put("root.resume", 0); tree.put("root..status_code", 503); + tree.put("root..status_message", "The host's concurrent stream limit has been reached. Stop an existing stream or increase the 'Channels' value in the Sunshine Web UI."); return; } @@ -748,6 +727,7 @@ namespace nvhttp { args.find("appid"s) == std::end(args)) { tree.put("root.resume", 0); tree.put("root..status_code", 400); + tree.put("root..status_message", "Missing a required launch parameter"); return; } @@ -758,14 +738,30 @@ namespace nvhttp { if (current_appid > 0) { tree.put("root.resume", 0); tree.put("root..status_code", 400); + tree.put("root..status_message", "An app is already running on this host"); 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). + if (rtsp_stream::session_count() == 0) { + 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?"); + tree.put("root.gamesession", 0); + + return; + } + } + if (appid > 0) { auto err = proc::proc.execute(appid); if (err) { tree.put("root..status_code", err); + tree.put("root..status_message", "Failed to start the specified application"); tree.put("root.gamesession", 0); return; @@ -798,6 +794,7 @@ namespace nvhttp { if (rtsp_stream::session_count() == config::stream.channels) { tree.put("root.resume", 0); tree.put("root..status_code", 503); + tree.put("root..status_message", "The host's concurrent stream limit has been reached. Stop an existing stream or increase the 'Channels' value in the Sunshine Web UI."); return; } @@ -806,6 +803,7 @@ namespace nvhttp { if (current_appid == 0) { tree.put("root.resume", 0); tree.put("root..status_code", 503); + tree.put("root..status_message", "No running app to resume"); return; } @@ -816,10 +814,32 @@ namespace nvhttp { args.find("rikeyid"s) == std::end(args)) { tree.put("root.resume", 0); tree.put("root..status_code", 400); + tree.put("root..status_message", "Missing a required resume parameter"); return; } + if (rtsp_stream::session_count() == 0) { + // 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.resume", 0); + tree.put("root..status_code", 503); + tree.put("root..status_message", "Failed to initialize video capture/encoding. Is a display connected and turned on?"); + + 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")); + } + } + rtsp_stream::launch_session_raise(make_launch_session(host_audio, args)); tree.put("root..status_code", 200); @@ -845,6 +865,7 @@ namespace nvhttp { if (rtsp_stream::session_count() != 0) { tree.put("root.resume", 0); tree.put("root..status_code", 503); + tree.put("root..status_message", "All sessions must be disconnected before quitting"); return; } @@ -872,13 +893,13 @@ namespace nvhttp { } /** - * @brief Start the nvhttp server. - * - * EXAMPLES: - * ```cpp - * nvhttp::start(); - * ``` - */ + * @brief Start the nvhttp server. + * + * EXAMPLES: + * ```cpp + * nvhttp::start(); + * ``` + */ void start() { auto shutdown_event = mail::man->event(mail::shutdown); @@ -904,7 +925,7 @@ namespace nvhttp { auto add_cert = std::make_shared>(30); - // /resume doesn't get the parameter "localAudioPlayMode" + // /resume doesn't always get the parameter "localAudioPlayMode" // /launch will store it in host_audio bool host_audio {}; @@ -913,7 +934,7 @@ namespace nvhttp { // Verify certificates after establishing connection https_server.verify = [&cert_chain, add_cert](SSL *ssl) { - auto x509 = SSL_get_peer_certificate(ssl); + crypto::x509_t x509 { SSL_get_peer_certificate(ssl) }; if (!x509) { BOOST_LOG(info) << "unknown -- denied"sv; return 0; @@ -924,7 +945,7 @@ namespace nvhttp { auto fg = util::fail_guard([&]() { char subject_name[256]; - X509_NAME_oneline(X509_get_subject_name(x509), subject_name, sizeof(subject_name)); + X509_NAME_oneline(X509_get_subject_name(x509.get()), subject_name, sizeof(subject_name)); BOOST_LOG(debug) << subject_name << " -- "sv << (verified ? "verified"sv : "denied"sv); }); @@ -939,7 +960,7 @@ namespace nvhttp { cert_chain.add(std::move(cert)); } - auto err_str = cert_chain.verify(x509); + auto err_str = cert_chain.verify(x509.get()); if (err_str) { BOOST_LOG(warning) << "SSL Verification error :: "sv << err_str; @@ -1018,13 +1039,13 @@ namespace nvhttp { } /** - * @brief Remove all paired clients. - * - * EXAMPLES: - * ```cpp - * nvhttp::erase_all_clients(); - * ``` - */ + * @brief Remove all paired clients. + * + * EXAMPLES: + * ```cpp + * nvhttp::erase_all_clients(); + * ``` + */ void erase_all_clients() { map_id_client.clear(); diff --git a/src/nvhttp.h b/src/nvhttp.h index eccf9d349e9..3be24b3de06 100644 --- a/src/nvhttp.h +++ b/src/nvhttp.h @@ -1,10 +1,10 @@ /** -* @file nvhttp.h -*/ + * @file src/nvhttp.h + * @brief todo + */ // macros -#ifndef SUNSHINE_NVHTTP_H -#define SUNSHINE_NVHTTP_H +#pragma once // standard includes #include @@ -18,24 +18,25 @@ namespace nvhttp { /** - * @brief The protocol version. - */ + * @brief The protocol version. + * @details The version of the GameStream protocol we are mocking. + * @note The negative 4th number indicates to Moonlight that this is Sunshine. + */ constexpr auto VERSION = "7.1.431.-1"; - // The negative 4th version number tells Moonlight that this is Sunshine /** - * @brief The GFE version we are replicating. - */ + * @brief The GFE version we are replicating. + */ constexpr auto GFE_VERSION = "3.23.0.74"; /** - * @brief The HTTP port, as a difference from the config port. - */ + * @brief The HTTP port, as a difference from the config port. + */ constexpr auto PORT_HTTP = 0; /** - * @brief The HTTPS port, as a difference from the config port. - */ + * @brief The HTTPS port, as a difference from the config port. + */ constexpr auto PORT_HTTPS = -5; // functions @@ -46,5 +47,3 @@ namespace nvhttp { void erase_all_clients(); } // namespace nvhttp - -#endif // SUNSHINE_NVHTTP_H diff --git a/src/platform/common.h b/src/platform/common.h index 1175f889934..cdb5785e076 100644 --- a/src/platform/common.h +++ b/src/platform/common.h @@ -1,9 +1,8 @@ -// -// Created by loki on 6/21/19. -// - -#ifndef SUNSHINE_COMMON_H -#define SUNSHINE_COMMON_H +/** + * @file src/platform/common.h + * @brief todo + */ +#pragma once #include #include @@ -169,7 +168,7 @@ namespace platf { virtual ~deinit_t() = default; }; - struct img_t { + struct img_t: std::enable_shared_from_this { public: img_t() = default; @@ -186,6 +185,8 @@ namespace platf { std::int32_t pixel_pitch {}; std::int32_t row_pitch {}; + std::optional frame_timestamp; + virtual ~img_t() = default; }; @@ -213,8 +214,8 @@ namespace platf { } /** - * implementations must take ownership of 'frame' - */ + * implementations must take ownership of 'frame' + */ virtual int set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) { BOOST_LOG(error) << "Illegal call to hwdevice_t::set_frame(). Did you forget to override it?"; @@ -225,14 +226,14 @@ namespace platf { set_colorspace(std::uint32_t colorspace, std::uint32_t color_range) {}; /** - * Implementations may set parameters during initialization of the hwframes context - */ + * Implementations may set parameters during initialization of the hwframes context + */ virtual void init_hwframes(AVHWFramesContext *frames) {}; /** - * Implementations may make modifications required before context derivation - */ + * Implementations may make modifications required before context derivation + */ virtual int prepare_to_derive_context(int hw_device_type) { return 0; @@ -245,39 +246,53 @@ namespace platf { ok, reinit, timeout, + interrupted, error }; class display_t { public: /** - * When display has a new image ready or a timeout occurs, this callback will be called with the image. - * If a frame was captured, frame_captured will be true. If a timeout occurred, it will be false. - * - * On Break Request --> - * Returns nullptr - * - * On Success --> - * Returns the image object that should be filled next. - * This may or may not be the image send with the callback - */ - using snapshot_cb_t = std::function(std::shared_ptr &img, bool frame_captured)>; + * When display has a new image ready or a timeout occurs, this callback will be called with the image. + * If a frame was captured, frame_captured will be true. If a timeout occurred, it will be false. + * + * On Break Request --> + * Returns false + * + * On Success --> + * Returns true + */ + using push_captured_image_cb_t = std::function &&img, bool frame_captured)>; + + /** + * Use to get free image from the pool. Calls must be synchronized. + * Blocks until there is free image in the pool or capture is interrupted. + * + * Returns: + * 'true' on success, img_out contains free image + * 'false' when capture has been interrupted, img_out contains nullptr + */ + using pull_free_image_cb_t = std::function &img_out)>; display_t() noexcept: offset_x { 0 }, offset_y { 0 } {} /** - * snapshot_cb --> the callback - * std::shared_ptr img --> The first image to use - * bool *cursor --> A pointer to the flag that indicates wether the cursor should be captured as well - * - * Returns either: - * capture_e::ok when stopping - * capture_e::error on error - * capture_e::reinit when need of reinitialization - */ + * push_captured_image_cb --> The callback that is called with captured image, + * must be called from the same thread as capture() + * pull_free_image_cb --> Capture backends call this callback to get empty image + * from the pool. If backend uses multiple threads, calls to this + * callback must be synchronized. Calls to this callback and + * push_captured_image_cb must be synchronized as well. + * bool *cursor --> A pointer to the flag that indicates wether the cursor should be captured as well + * + * Returns either: + * capture_e::ok when stopping + * capture_e::error on error + * capture_e::reinit when need of reinitialization + */ virtual capture_e - capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr img, bool *cursor) = 0; + capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) = 0; virtual std::shared_ptr alloc_img() = 0; @@ -352,14 +367,14 @@ namespace platf { audio_control(); /** - * display_name --> The name of the monitor that SHOULD be displayed - * If display_name is empty --> Use the first monitor that's compatible you can find - * If you require to use this parameter in a seperate thread --> make a copy of it. - * - * config --> Stream configuration - * - * Returns display_t based on hwdevice_type - */ + * display_name --> The name of the monitor that SHOULD be displayed + * If display_name is empty --> Use the first monitor that's compatible you can find + * If you require to use this parameter in a seperate thread --> make a copy of it. + * + * config --> Stream configuration + * + * Returns display_t based on hwdevice_type + */ std::shared_ptr display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config); @@ -368,7 +383,7 @@ namespace platf { display_names(mem_type_e hwdevice_type); boost::process::child - run_unprivileged(const std::string &cmd, boost::filesystem::path &working_dir, boost::process::environment &env, FILE *file, std::error_code &ec, boost::process::group *group); + run_command(bool elevated, bool interactive, const std::string &cmd, boost::filesystem::path &working_dir, boost::process::environment &env, FILE *file, std::error_code &ec, boost::process::group *group); enum class thread_priority_e : int { low, @@ -385,9 +400,7 @@ namespace platf { void streaming_will_stop(); - bool - restart_supported(); - bool + void restart(); struct batched_send_info_t { @@ -409,6 +422,13 @@ namespace platf { std::unique_ptr enable_socket_qos(uintptr_t native_socket, boost::asio::ip::address &address, uint16_t port, qos_data_type_e data_type); + /** + * @brief Open a url in the default web browser. + * @param url The url to open. + */ + void + open_url(const std::string &url); + input_t input(); void @@ -422,7 +442,7 @@ namespace platf { void hscroll(input_t &input, int distance); void - keyboard(input_t &input, uint16_t modcode, bool release); + keyboard(input_t &input, uint16_t modcode, bool release, uint8_t flags); void gamepad(input_t &input, int nr, const gamepad_state_t &gamepad_state); void @@ -447,5 +467,3 @@ namespace platf { std::vector & supported_gamepads(); } // namespace platf - -#endif //SUNSHINE_COMMON_H diff --git a/src/platform/linux/audio.cpp b/src/platform/linux/audio.cpp index 0b68fc01a33..e31f539f615 100644 --- a/src/platform/linux/audio.cpp +++ b/src/platform/linux/audio.cpp @@ -1,6 +1,7 @@ -// -// Created by loki on 5/16/21. -// +/** + * @file src/platform/linux/audio.cpp + * @brief todo + */ #include #include diff --git a/src/platform/linux/cuda.cpp b/src/platform/linux/cuda.cpp index d1f8c1485d9..c2a2e0fd591 100644 --- a/src/platform/linux/cuda.cpp +++ b/src/platform/linux/cuda.cpp @@ -1,3 +1,7 @@ +/** + * @file src/platform/linux/cuda.cpp + * @brief todo + */ #include #include @@ -346,7 +350,7 @@ namespace cuda { handle.handle_flags[SESSION_HANDLE] = true; - return std::move(handle); + return handle; } const char * @@ -504,9 +508,16 @@ namespace cuda { } platf::capture_e - capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr img, bool *cursor) override { + capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override { auto next_frame = std::chrono::steady_clock::now(); + { + // We must create at least one texture on this thread before calling NvFBCToCudaSetUp() + // Otherwise it fails with "Unable to register an OpenGL buffer to a CUDA resource (result: 201)" message + std::shared_ptr img_dummy; + pull_free_image_cb(img_dummy); + } + // Force display_t::capture to initialize handle_t::capture cursor_visible = !*cursor; @@ -515,7 +526,7 @@ namespace cuda { handle.reset(); }); - while (img) { + while (true) { auto now = std::chrono::steady_clock::now(); if (next_frame > now) { std::this_thread::sleep_for((next_frame - now) / 3 * 2); @@ -526,16 +537,22 @@ namespace cuda { } next_frame = now + delay; - auto status = snapshot(img.get(), 150ms, *cursor); + std::shared_ptr img_out; + auto status = snapshot(pull_free_image_cb, img_out, 150ms, *cursor); switch (status) { case platf::capture_e::reinit: case platf::capture_e::error: + case platf::capture_e::interrupted: return status; case platf::capture_e::timeout: - img = snapshot_cb(img, false); + if (!push_captured_image_cb(std::move(img_out), false)) { + return platf::capture_e::ok; + } break; case platf::capture_e::ok: - img = snapshot_cb(img, true); + if (!push_captured_image_cb(std::move(img_out), true)) { + return platf::capture_e::ok; + } break; default: BOOST_LOG(error) << "Unrecognized capture status ["sv << (int) status << ']'; @@ -618,7 +635,7 @@ namespace cuda { } platf::capture_e - snapshot(platf::img_t *img, std::chrono::milliseconds timeout, bool cursor) { + snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor) { if (cursor != cursor_visible) { auto status = reinit(cursor); if (status != platf::capture_e::ok) { @@ -646,7 +663,12 @@ namespace cuda { return platf::capture_e::error; } - if (((img_t *) img)->tex.copy((std::uint8_t *) device_ptr, img->height, img->row_pitch)) { + if (!pull_free_image_cb(img_out)) { + return platf::capture_e::interrupted; + } + auto img = (img_t *) img_out.get(); + + if (img->tex.copy((std::uint8_t *) device_ptr, img->height, img->row_pitch)) { return platf::capture_e::error; } diff --git a/src/platform/linux/cuda.cu b/src/platform/linux/cuda.cu index b81cd40d85a..107075d99cd 100644 --- a/src/platform/linux/cuda.cu +++ b/src/platform/linux/cuda.cu @@ -1,5 +1,10 @@ +/** + * @file src/platform/linux/cuda.cu + * @brief todo + */ // #include #include +#include #include #include #include @@ -29,13 +34,15 @@ using namespace std::literals; using namespace std::literals; -//////////////////// Special desclarations +// Special declarations /** - * NVCC segfaults when including - * Therefore, some declarations need to be added explicitely + * NVCC tends to have problems with standard headers. + * Don't include common.h, instead use bare minimum + * of standard headers and duplicate declarations of necessary classes. + * Not pretty and extremely error-prone, fix at earliest convenience. */ namespace platf { -struct img_t { +struct img_t: std::enable_shared_from_this { public: std::uint8_t *data {}; std::int32_t width {}; @@ -43,6 +50,8 @@ public: std::int32_t pixel_pitch {}; std::int32_t row_pitch {}; + std::optional frame_timestamp; + virtual ~img_t() = default; }; } // namespace platf @@ -70,10 +79,10 @@ struct alignas(16) color_extern_t { static_assert(sizeof(video::color_t) == sizeof(video::color_extern_t), "color matrix struct mismatch"); -extern color_t colors[4]; +extern color_t colors[6]; } // namespace video -//////////////////// End special declarations +// End special declarations namespace cuda { auto constexpr INVALID_TEXTURE = std::numeric_limits::max(); @@ -225,7 +234,7 @@ std::optional tex_t::make(int height, int pitch) { CU_CHECK_OPT(cudaCreateTextureObject(&tex.texture.linear, &res, &desc, nullptr), "Couldn't create cuda texture that uses linear interpolation"); - return std::move(tex); + return tex; } tex_t::tex_t() : array {}, texture { INVALID_TEXTURE } {} diff --git a/src/platform/linux/cuda.h b/src/platform/linux/cuda.h index e38349400b9..e2094d81b25 100644 --- a/src/platform/linux/cuda.h +++ b/src/platform/linux/cuda.h @@ -1,6 +1,12 @@ -#if !defined(SUNSHINE_PLATFORM_CUDA_H) && defined(SUNSHINE_BUILD_CUDA) - #define SUNSHINE_PLATFORM_CUDA_H +/** + * @file src/platform/linux/cuda.h + * @brief todo + */ +#pragma once +#if defined(SUNSHINE_BUILD_CUDA) + + #include #include #include #include @@ -88,11 +94,11 @@ namespace cuda { sws_t(int in_width, int in_height, int out_width, int out_height, int pitch, int threadsPerBlock, ptr_t &&color_matrix); /** - * in_width, in_height -- The width and height of the captured image in pixels - * out_width, out_height -- the width and height of the NV12 image in pixels - * - * pitch -- The size of a single row of pixels in bytes - */ + * in_width, in_height -- The width and height of the captured image in pixels + * out_width, out_height -- the width and height of the NV12 image in pixels + * + * pitch -- The size of a single row of pixels in bytes + */ static std::optional make(int in_width, int in_height, int out_width, int out_height, int pitch); diff --git a/src/platform/linux/graphics.cpp b/src/platform/linux/graphics.cpp index 51db08467be..fcb7ab234be 100644 --- a/src/platform/linux/graphics.cpp +++ b/src/platform/linux/graphics.cpp @@ -1,3 +1,7 @@ +/** + * @file src/platform/linux/graphics.cpp + * @brief todo + */ #include "graphics.h" #include "src/video.h" @@ -765,7 +769,7 @@ namespace egl { gl_drain_errors; - return std::move(sws); + return sws; } int diff --git a/src/platform/linux/graphics.h b/src/platform/linux/graphics.h index 01fc5304f1e..fbb0e92d3b9 100644 --- a/src/platform/linux/graphics.h +++ b/src/platform/linux/graphics.h @@ -1,5 +1,8 @@ -#ifndef SUNSHINE_PLATFORM_LINUX_OPENGL_H -#define SUNSHINE_PLATFORM_LINUX_OPENGL_H +/** + * @file src/platform/linux/graphics.h + * @brief todo + */ +#pragma once #include #include @@ -94,8 +97,8 @@ namespace gl { } /** - * Copies a part of the framebuffer to texture - */ + * Copies a part of the framebuffer to texture + */ void copy(int id, int texture, int offset_x, int offset_y, int width, int height); }; @@ -352,5 +355,3 @@ namespace egl { bool fail(); } // namespace egl - -#endif diff --git a/src/platform/linux/input.cpp b/src/platform/linux/input.cpp index 5749b30773d..85980ef37b5 100644 --- a/src/platform/linux/input.cpp +++ b/src/platform/linux/input.cpp @@ -1,3 +1,7 @@ +/** + * @file src/platform/linux/input.cpp + * @brief todo + */ #include #include #include @@ -143,9 +147,9 @@ namespace platf { constexpr auto UNKNOWN = 0; /** - * @brief Initializes the keycode constants for translating - * moonlight keycodes to linux/X11 keycodes - */ + * @brief Initializes the keycode constants for translating + * moonlight keycodes to linux/X11 keycodes. + */ static constexpr std::array init_keycodes() { std::array keycodes {}; @@ -1047,16 +1051,16 @@ namespace platf { } /** - * @brief XTest absolute mouse move. - * @param input The input_t instance to use. - * @param x Absolute x position. - * @param y Absolute y position. - * - * EXAMPLES: - * ```cpp - * x_abs_mouse(input, 0, 0); - * ``` - */ + * @brief XTest absolute mouse move. + * @param input The input_t instance to use. + * @param x Absolute x position. + * @param y Absolute y position. + * + * EXAMPLES: + * ```cpp + * x_abs_mouse(input, 0, 0); + * ``` + */ static void x_abs_mouse(input_t &input, float x, float y) { #ifdef SUNSHINE_BUILD_X11 @@ -1070,17 +1074,17 @@ namespace platf { } /** - * @brief Absolute mouse move. - * @param input The input_t instance to use. - * @param touch_port The touch_port instance to use. - * @param x Absolute x position. - * @param y Absolute y position. - * - * EXAMPLES: - * ```cpp - * abs_mouse(input, touch_port, 0, 0); - * ``` - */ + * @brief Absolute mouse move. + * @param input The input_t instance to use. + * @param touch_port The touch_port instance to use. + * @param x Absolute x position. + * @param y Absolute y position. + * + * EXAMPLES: + * ```cpp + * abs_mouse(input, touch_port, 0, 0); + * ``` + */ void abs_mouse(input_t &input, const touch_port_t &touch_port, float x, float y) { auto touchscreen = ((input_raw_t *) input.get())->touch_input.get(); @@ -1101,16 +1105,16 @@ namespace platf { } /** - * @brief XTest relative mouse move. - * @param input The input_t instance to use. - * @param deltaX Relative x position. - * @param deltaY Relative y position. - * - * EXAMPLES: - * ```cpp - * x_move_mouse(input, 10, 10); // Move mouse 10 pixels down and right - * ``` - */ + * @brief XTest relative mouse move. + * @param input The input_t instance to use. + * @param deltaX Relative x position. + * @param deltaY Relative y position. + * + * EXAMPLES: + * ```cpp + * x_move_mouse(input, 10, 10); // Move mouse 10 pixels down and right + * ``` + */ static void x_move_mouse(input_t &input, int deltaX, int deltaY) { #ifdef SUNSHINE_BUILD_X11 @@ -1124,16 +1128,16 @@ namespace platf { } /** - * @brief Relative mouse move. - * @param input The input_t instance to use. - * @param deltaX Relative x position. - * @param deltaY Relative y position. - * - * EXAMPLES: - * ```cpp - * move_mouse(input, 10, 10); // Move mouse 10 pixels down and right - * ``` - */ + * @brief Relative mouse move. + * @param input The input_t instance to use. + * @param deltaX Relative x position. + * @param deltaY Relative y position. + * + * EXAMPLES: + * ```cpp + * move_mouse(input, 10, 10); // Move mouse 10 pixels down and right + * ``` + */ void move_mouse(input_t &input, int deltaX, int deltaY) { auto mouse = ((input_raw_t *) input.get())->mouse_input.get(); @@ -1154,16 +1158,16 @@ namespace platf { } /** - * @brief XTest mouse button press/release. - * @param input The input_t instance to use. - * @param button Which mouse button to emulate. - * @param release Whether the event was a press (false) or a release (true) - * - * EXAMPLES: - * ```cpp - * x_button_mouse(input, 1, false); // Press left mouse button - * ``` - */ + * @brief XTest mouse button press/release. + * @param input The input_t instance to use. + * @param button Which mouse button to emulate. + * @param release Whether the event was a press (false) or a release (true) + * + * EXAMPLES: + * ```cpp + * x_button_mouse(input, 1, false); // Press left mouse button + * ``` + */ static void x_button_mouse(input_t &input, int button, bool release) { #ifdef SUNSHINE_BUILD_X11 @@ -1197,16 +1201,16 @@ namespace platf { } /** - * @brief Mouse button press/release. - * @param input The input_t instance to use. - * @param button Which mouse button to emulate. - * @param release Whether the event was a press (false) or a release (true) - * - * EXAMPLES: - * ```cpp - * button_mouse(input, 1, false); // Press left mouse button - * ``` - */ + * @brief Mouse button press/release. + * @param input The input_t instance to use. + * @param button Which mouse button to emulate. + * @param release Whether the event was a press (false) or a release (true) + * + * EXAMPLES: + * ```cpp + * button_mouse(input, 1, false); // Press left mouse button + * ``` + */ void button_mouse(input_t &input, int button, bool release) { auto mouse = ((input_raw_t *) input.get())->mouse_input.get(); @@ -1245,17 +1249,17 @@ namespace platf { } /** - * @brief XTest mouse scroll. - * @param input The input_t instance to use. - * @param distance How far to scroll - * @param button_pos Which mouse button to emulate for positive scroll. - * @param button_neg Which mouse button to emulate for negative scroll. - * - * EXAMPLES: - * ```cpp - * x_scroll(input, 10, 4, 5); - * ``` - */ + * @brief XTest mouse scroll. + * @param input The input_t instance to use. + * @param distance How far to scroll. + * @param button_pos Which mouse button to emulate for positive scroll. + * @param button_neg Which mouse button to emulate for negative scroll. + * + * EXAMPLES: + * ```cpp + * x_scroll(input, 10, 4, 5); + * ``` + */ static void x_scroll(input_t &input, int distance, int button_pos, int button_neg) { #ifdef SUNSHINE_BUILD_X11 @@ -1274,15 +1278,15 @@ namespace platf { } /** - * @brief Vertical mouse scroll. - * @param input The input_t instance to use. - * @param high_res_distance How far to scroll - * - * EXAMPLES: - * ```cpp - * scroll(input, 1200); - * ``` - */ + * @brief Vertical mouse scroll. + * @param input The input_t instance to use. + * @param high_res_distance How far to scroll. + * + * EXAMPLES: + * ```cpp + * scroll(input, 1200); + * ``` + */ void scroll(input_t &input, int high_res_distance) { int distance = high_res_distance / 120; @@ -1299,15 +1303,15 @@ namespace platf { } /** - * @brief Horizontal mouse scroll. - * @param input The input_t instance to use. - * @param high_res_distance How far to scroll - * - * EXAMPLES: - * ```cpp - * hscroll(input, 1200); - * ``` - */ + * @brief Horizontal mouse scroll. + * @param input The input_t instance to use. + * @param high_res_distance How far to scroll. + * + * EXAMPLES: + * ```cpp + * hscroll(input, 1200); + * ``` + */ void hscroll(input_t &input, int high_res_distance) { int distance = high_res_distance / 120; @@ -1333,18 +1337,19 @@ namespace platf { } /** - * @brief XTest keyboard emulation. - * @param input The input_t instance to use. - * @param modcode The moonlight key code. - * @param release Whether the event was a press (false) or a release (true) - * - * EXAMPLES: - * ```cpp - * x_keyboard(input, 0x5A, false); // Press Z - * ``` - */ + * @brief XTest keyboard emulation. + * @param input The input_t instance to use. + * @param modcode The moonlight key code. + * @param release Whether the event was a press (false) or a release (true). + * @param flags SS_KBE_FLAG_* values. + * + * EXAMPLES: + * ```cpp + * x_keyboard(input, 0x5A, false, 0); // Press Z + * ``` + */ static void - x_keyboard(input_t &input, uint16_t modcode, bool release) { + x_keyboard(input_t &input, uint16_t modcode, bool release, uint8_t flags) { #ifdef SUNSHINE_BUILD_X11 auto keycode = keysym(modcode); if (keycode.keysym == UNKNOWN) { @@ -1367,21 +1372,22 @@ namespace platf { } /** - * @brief Keyboard emulation. - * @param input The input_t instance to use. - * @param modcode The moonlight key code. - * @param release Whether the event was a press (false) or a release (true) - * - * EXAMPLES: - * ```cpp - * keyboard(input, 0x5A, false); // Press Z - * ``` - */ + * @brief Keyboard emulation. + * @param input The input_t instance to use. + * @param modcode The moonlight key code. + * @param release Whether the event was a press (false) or a release (true). + * @param flags SS_KBE_FLAG_* values. + * + * EXAMPLES: + * ```cpp + * keyboard(input, 0x5A, false, 0); // Press Z + * ``` + */ void - keyboard(input_t &input, uint16_t modcode, bool release) { + keyboard(input_t &input, uint16_t modcode, bool release, uint8_t flags) { auto keyboard = ((input_raw_t *) input.get())->keyboard_input.get(); if (!keyboard) { - x_keyboard(input, modcode, release); + x_keyboard(input, modcode, release, flags); return; } @@ -1405,12 +1411,12 @@ namespace platf { } /** - * Takes an UTF-32 encoded string and returns a hex string representation of the bytes (uppercase) - * - * ex: ['👱'] = "1F471" // see UTF encoding at https://www.compart.com/en/unicode/U+1F471 - * - * adapted from: https://stackoverflow.com/a/7639754 - */ + * Takes an UTF-32 encoded string and returns a hex string representation of the bytes (uppercase) + * + * ex: ['👱'] = "1F471" // see UTF encoding at https://www.compart.com/en/unicode/U+1F471 + * + * adapted from: https://stackoverflow.com/a/7639754 + */ std::string to_hex(const std::basic_string &str) { std::stringstream ss; @@ -1425,16 +1431,16 @@ namespace platf { } /** - * Here we receive a single UTF-8 encoded char at a time, - * the trick is to convert it to UTF-32 then send CTRL+SHIFT+U+ in order to produce any - * unicode character, see: https://en.wikipedia.org/wiki/Unicode_input - * - * ex: - * - when receiving UTF-8 [0xF0 0x9F 0x91 0xB1] (which is '👱') - * - we'll convert it to UTF-32 [0x1F471] - * - then type: CTRL+SHIFT+U+1F471 - * see the conversion at: https://www.compart.com/en/unicode/U+1F471 - */ + * Here we receive a single UTF-8 encoded char at a time, + * the trick is to convert it to UTF-32 then send CTRL+SHIFT+U+{HEXCODE} in order to produce any + * unicode character, see: https://en.wikipedia.org/wiki/Unicode_input + * + * ex: + * - when receiving UTF-8 [0xF0 0x9F 0x91 0xB1] (which is '👱') + * - we'll convert it to UTF-32 [0x1F471] + * - then type: CTRL+SHIFT+U+1F471 + * see the conversion at: https://www.compart.com/en/unicode/U+1F471 + */ void unicode(input_t &input, char *utf8, int size) { auto kb = ((input_raw_t *) input.get())->keyboard_input.get(); @@ -1547,13 +1553,13 @@ namespace platf { } /** - * @brief Initalize a new keyboard and return it. - * - * EXAMPLES: - * ```cpp - * auto my_keyboard = keyboard(); - * ``` - */ + * @brief Initialize a new keyboard and return it. + * + * EXAMPLES: + * ```cpp + * auto my_keyboard = keyboard(); + * ``` + */ evdev_t keyboard() { evdev_t dev { libevdev_new() }; @@ -1576,13 +1582,13 @@ namespace platf { } /** - * @brief Initalize a new uinput virtual mouse and return it. - * - * EXAMPLES: - * ```cpp - * auto my_mouse = mouse(); - * ``` - */ + * @brief Initialize a new `uinput` virtual mouse and return it. + * + * EXAMPLES: + * ```cpp + * auto my_mouse = mouse(); + * ``` + */ evdev_t mouse() { evdev_t dev { libevdev_new() }; @@ -1627,13 +1633,13 @@ namespace platf { } /** - * @brief Initalize a new uinput virtual touchscreen and return it. - * - * EXAMPLES: - * ```cpp - * auto my_touchscreen = touchscreen(); - * ``` - */ + * @brief Initialize a new `uinput` virtual touchscreen and return it. + * + * EXAMPLES: + * ```cpp + * auto my_touchscreen = touchscreen(); + * ``` + */ evdev_t touchscreen() { evdev_t dev { libevdev_new() }; @@ -1677,13 +1683,13 @@ namespace platf { } /** - * @brief Initalize a new uinput virtual X360 gamepad and return it. - * - * EXAMPLES: - * ```cpp - * auto my_x360 = x360(); - * ``` - */ + * @brief Initialize a new `uinput` virtual X360 gamepad and return it. + * + * EXAMPLES: + * ```cpp + * auto my_x360 = x360(); + * ``` + */ evdev_t x360() { evdev_t dev { libevdev_new() }; @@ -1754,13 +1760,13 @@ namespace platf { } /** - * @brief Initalize the input system and return it. - * - * EXAMPLES: - * ```cpp - * auto my_input = input(); - * ``` - */ + * @brief Initialize the input system and return it. + * + * EXAMPLES: + * ```cpp + * auto my_input = input(); + * ``` + */ input_t input() { input_t result { new input_raw_t() }; diff --git a/src/platform/linux/kmsgrab.cpp b/src/platform/linux/kmsgrab.cpp index 392bb65db98..094aee5f4e9 100644 --- a/src/platform/linux/kmsgrab.cpp +++ b/src/platform/linux/kmsgrab.cpp @@ -1,3 +1,7 @@ +/** + * @file src/platform/linux/kmsgrab.cpp + * @brief todo + */ #include #include #include @@ -552,7 +556,7 @@ namespace platf { return -1; } - //TODO: surf_sd = fb->to_sd(); + // TODO: surf_sd = fb->to_sd(); auto crct = card.crtc(plane->crtc_id); kms::print(plane.get(), fb.get(), crct.get()); @@ -723,10 +727,10 @@ namespace platf { } capture_e - capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr img, bool *cursor) override { + capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override { auto next_frame = std::chrono::steady_clock::now(); - while (img) { + while (true) { auto now = std::chrono::steady_clock::now(); if (next_frame > now) { @@ -738,16 +742,22 @@ namespace platf { } next_frame = now + delay; - auto status = snapshot(img.get(), 1000ms, *cursor); + std::shared_ptr img_out; + auto status = snapshot(pull_free_image_cb, img_out, 1000ms, *cursor); switch (status) { case platf::capture_e::reinit: case platf::capture_e::error: + case platf::capture_e::interrupted: return status; case platf::capture_e::timeout: - img = snapshot_cb(img, false); + if (!push_captured_image_cb(std::move(img_out), false)) { + return platf::capture_e::ok; + } break; case platf::capture_e::ok: - img = snapshot_cb(img, true); + if (!push_captured_image_cb(std::move(img_out), true)) { + return platf::capture_e::ok; + } break; default: BOOST_LOG(error) << "Unrecognized capture status ["sv << (int) status << ']'; @@ -768,7 +778,7 @@ namespace platf { } capture_e - snapshot(img_t *img_out_base, std::chrono::milliseconds timeout, bool cursor) { + snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor) { file_t fb_fd[4]; egl::surface_descriptor_t sd; @@ -793,10 +803,14 @@ namespace platf { gl::ctx.GetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &h); BOOST_LOG(debug) << "width and height: w "sv << w << " h "sv << h; - gl::ctx.GetTextureSubImage(rgb->tex[0], 0, img_offset_x, img_offset_y, 0, width, height, 1, GL_BGRA, GL_UNSIGNED_BYTE, img_out_base->height * img_out_base->row_pitch, img_out_base->data); + if (!pull_free_image_cb(img_out)) { + return platf::capture_e::interrupted; + } + + gl::ctx.GetTextureSubImage(rgb->tex[0], 0, img_offset_x, img_offset_y, 0, width, height, 1, GL_BGRA, GL_UNSIGNED_BYTE, img_out->height * img_out->row_pitch, img_out->data); if (cursor_opt && cursor) { - cursor_opt->blend(*img_out_base, img_offset_x, img_offset_y); + cursor_opt->blend(*img_out, img_offset_x, img_offset_y); } return capture_e::ok; @@ -855,14 +869,23 @@ namespace platf { int dummy_img(platf::img_t *img) override { - return snapshot(img, 1s, false) != capture_e::ok; + // TODO: stop cheating and give black image + if (!img) { + return -1; + }; + auto pull_dummy_img_callback = [&img](std::shared_ptr &img_out) -> bool { + img_out = img->shared_from_this(); + return true; + }; + std::shared_ptr img_out; + return snapshot(pull_dummy_img_callback, img_out, 1s, false) != platf::capture_e::ok; } capture_e - capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr img, bool *cursor) { + capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) { auto next_frame = std::chrono::steady_clock::now(); - while (img) { + while (true) { auto now = std::chrono::steady_clock::now(); if (next_frame > now) { @@ -874,16 +897,22 @@ namespace platf { } next_frame = now + delay; - auto status = snapshot(img.get(), 1000ms, *cursor); + std::shared_ptr img_out; + auto status = snapshot(pull_free_image_cb, img_out, 1000ms, *cursor); switch (status) { case platf::capture_e::reinit: case platf::capture_e::error: + case platf::capture_e::interrupted: return status; case platf::capture_e::timeout: - img = snapshot_cb(img, false); + if (!push_captured_image_cb(std::move(img_out), false)) { + return platf::capture_e::ok; + } break; case platf::capture_e::ok: - img = snapshot_cb(img, true); + if (!push_captured_image_cb(std::move(img_out), true)) { + return platf::capture_e::ok; + } break; default: BOOST_LOG(error) << "Unrecognized capture status ["sv << (int) status << ']'; @@ -895,10 +924,13 @@ namespace platf { } capture_e - snapshot(img_t *img_out_base, std::chrono::milliseconds /* timeout */, bool cursor) { + snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds /* timeout */, bool cursor) { file_t fb_fd[4]; - auto img = (egl::img_descriptor_t *) img_out_base; + if (!pull_free_image_cb(img_out)) { + return platf::capture_e::interrupted; + } + auto img = (egl::img_descriptor_t *) img_out.get(); img->reset(); auto status = refresh(fb_fd, &img->sd); @@ -909,7 +941,7 @@ namespace platf { img->sequence = ++sequence; if (!cursor || !cursor_opt) { - img_out_base->data = nullptr; + img->data = nullptr; for (auto x = 0; x < 4; ++x) { fb_fd[x].release(); @@ -971,15 +1003,15 @@ namespace platf { } /** - * On Wayland, it's not possible to determine the position of the monitor on the desktop with KMS. - * Wayland does allow applications to query attached monitors on the desktop, - * however, the naming scheme is not standardized across implementations. - * - * As a result, correlating the KMS output to the wayland outputs is guess work at best. - * But, it's necessary for absolute mouse coordinates to work. - * - * This is an ugly hack :( - */ + * On Wayland, it's not possible to determine the position of the monitor on the desktop with KMS. + * Wayland does allow applications to query attached monitors on the desktop, + * however, the naming scheme is not standardized across implementations. + * + * As a result, correlating the KMS output to the wayland outputs is guess work at best. + * But, it's necessary for absolute mouse coordinates to work. + * + * This is an ugly hack :( + */ void correlate_to_wayland(std::vector &cds) { auto monitors = wl::monitors(); diff --git a/src/platform/linux/misc.cpp b/src/platform/linux/misc.cpp index ba41e625d44..73f6011a144 100644 --- a/src/platform/linux/misc.cpp +++ b/src/platform/linux/misc.cpp @@ -1,7 +1,7 @@ /** - * @file misc.cpp + * @file src/misc.cpp + * @brief todo */ - // standard includes #include @@ -103,15 +103,14 @@ namespace platf { std::string from_sockaddr(const sockaddr *const ip_addr) { - char data[INET6_ADDRSTRLEN]; + char data[INET6_ADDRSTRLEN] = {}; auto family = ip_addr->sa_family; if (family == AF_INET6) { inet_ntop(AF_INET6, &((sockaddr_in6 *) ip_addr)->sin6_addr, data, INET6_ADDRSTRLEN); } - - if (family == AF_INET) { + else if (family == AF_INET) { inet_ntop(AF_INET, &((sockaddr_in *) ip_addr)->sin_addr, data, INET_ADDRSTRLEN); } @@ -121,17 +120,16 @@ namespace platf { std::pair from_sockaddr_ex(const sockaddr *const ip_addr) { - char data[INET6_ADDRSTRLEN]; + char data[INET6_ADDRSTRLEN] = {}; auto family = ip_addr->sa_family; - std::uint16_t port; + std::uint16_t port = 0; if (family == AF_INET6) { inet_ntop(AF_INET6, &((sockaddr_in6 *) ip_addr)->sin6_addr, data, INET6_ADDRSTRLEN); port = ((sockaddr_in6 *) ip_addr)->sin6_port; } - - if (family == AF_INET) { + else if (family == AF_INET) { inet_ntop(AF_INET, &((sockaddr_in *) ip_addr)->sin_addr, data, INET_ADDRSTRLEN); port = ((sockaddr_in *) ip_addr)->sin_port; @@ -159,8 +157,7 @@ namespace platf { } bp::child - run_unprivileged(const std::string &cmd, boost::filesystem::path &working_dir, bp::environment &env, FILE *file, std::error_code &ec, bp::group *group) { - BOOST_LOG(warning) << "run_unprivileged() is not yet implemented for this platform. The new process will run with Sunshine's permissions."sv; + run_command(bool elevated, bool interactive, const std::string &cmd, boost::filesystem::path &working_dir, bp::environment &env, FILE *file, std::error_code &ec, bp::group *group) { if (!group) { if (!file) { return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_out > bp::null, bp::std_err > bp::null, ec); @@ -179,6 +176,28 @@ namespace platf { } } + /** + * @brief Open a url in the default web browser. + * @param url The url to open. + */ + void + open_url(const std::string &url) { + // set working dir to user home directory + auto working_dir = boost::filesystem::path(std::getenv("HOME")); + std::string cmd = R"(xdg-open ")" + url + R"(")"; + + boost::process::environment _env = boost::this_process::environment(); + std::error_code ec; + auto child = run_command(false, false, cmd, working_dir, _env, nullptr, ec, nullptr); + if (ec) { + BOOST_LOG(warning) << "Couldn't open url ["sv << url << "]: System: "sv << ec.message(); + } + else { + BOOST_LOG(info) << "Opened url ["sv << url << "]"sv; + child.detach(); + } + } + void adjust_thread_priority(thread_priority_e priority) { // Unimplemented @@ -194,16 +213,34 @@ namespace platf { // Nothing to do } - bool - restart_supported() { - // Restart not supported yet - return false; + void + restart_on_exit() { + char executable[PATH_MAX]; + ssize_t len = readlink("/proc/self/exe", executable, PATH_MAX - 1); + if (len == -1) { + BOOST_LOG(fatal) << "readlink() failed: "sv << errno; + return; + } + executable[len] = '\0'; + + // ASIO doesn't use O_CLOEXEC, so we have to close all fds ourselves + int openmax = (int) sysconf(_SC_OPEN_MAX); + for (int fd = STDERR_FILENO + 1; fd < openmax; fd++) { + close(fd); + } + + // Re-exec ourselves with the same arguments + if (execv(executable, lifetime::get_argv()) < 0) { + BOOST_LOG(fatal) << "execv() failed: "sv << errno; + return; + } } - bool + void restart() { - // Restart not supported yet - return false; + // Gracefully clean up and restart ourselves instead of exiting + atexit(restart_on_exit); + lifetime::exit_sunshine(0, true); } bool diff --git a/src/platform/linux/misc.h b/src/platform/linux/misc.h index e4a59f672f1..8541bf1650e 100644 --- a/src/platform/linux/misc.h +++ b/src/platform/linux/misc.h @@ -1,5 +1,8 @@ -#ifndef SUNSHINE_PLATFORM_MISC_H -#define SUNSHINE_PLATFORM_MISC_H +/** + * @file src/platform/linux/misc.h + * @brief todo + */ +#pragma once #include #include @@ -29,5 +32,3 @@ namespace dyn { handle(const std::vector &libs); } // namespace dyn - -#endif \ No newline at end of file diff --git a/src/platform/linux/publish.cpp b/src/platform/linux/publish.cpp index 6c6d7ede36e..367c7aa3a20 100644 --- a/src/platform/linux/publish.cpp +++ b/src/platform/linux/publish.cpp @@ -1,5 +1,9 @@ - -// adapted from https://www.avahi.org/doxygen/html/client-publish-service_8c-example.html +/** + * @file src/platform/linux/publish.cpp + * @brief todo + * @note Adapted from https://www.avahi.org/doxygen/html/client-publish-service_8c-example.html + * @todo Use a common file for this and src/platform/macos/publish.cpp + */ #include #include "misc.h" @@ -12,7 +16,9 @@ using namespace std::literals; namespace avahi { - /** Error codes used by avahi */ + /** + * @brief Error codes used by avahi. + */ enum err_e { OK = 0, /**< OK */ ERR_FAILURE = -1, /**< Generic error code */ @@ -113,7 +119,9 @@ namespace avahi { CLIENT_NO_FAIL = 2 /**< Don't fail if the daemon is not available when avahi_client_new() is called, instead enter CLIENT_CONNECTING state and wait for the daemon to appear */ }; - /** Some flags for publishing functions */ + /** + * @brief Flags for publishing functions. + */ enum PublishFlags { PUBLISH_UNIQUE = 1, /**< For raw records: The RRset is intended to be unique */ PUBLISH_NO_PROBE = 2, /**< For raw records: Though the RRset is intended to be unique no probes shall be sent */ @@ -434,4 +442,4 @@ namespace platf::publish { return std::make_unique(std::thread { avahi::simple_poll_loop, poll.get() }); } -} // namespace platf::publish \ No newline at end of file +} // namespace platf::publish diff --git a/src/platform/linux/vaapi.cpp b/src/platform/linux/vaapi.cpp index 12281ac4292..4a1e7df23ba 100644 --- a/src/platform/linux/vaapi.cpp +++ b/src/platform/linux/vaapi.cpp @@ -1,3 +1,7 @@ +/** + * @file src/platform/linux/vaapi.cpp + * @brief todo + */ #include #include @@ -7,8 +11,7 @@ extern "C" { #include #include #if !VA_CHECK_VERSION(1, 9, 0) -/* vaSyncBuffer stub allows Sunshine built against libva <2.9.0 - to link against ffmpeg on libva 2.9.0 or later */ +// vaSyncBuffer stub allows Sunshine built against libva <2.9.0 to link against ffmpeg on libva 2.9.0 or later VAStatus vaSyncBuffer( VADisplay dpy, @@ -25,6 +28,7 @@ vaSyncBuffer( #include "src/main.h" #include "src/platform/common.h" #include "src/utility.h" +#include "src/video.h" using namespace std::literals; @@ -55,10 +59,7 @@ namespace va { // Needs to be closed manually int fd; - /* - * Total size of this object (may include regions which are - * not part of the surface). - */ + // Total size of this object (may include regions which are not part of the surface) uint32_t size; // Format modifier applied to this object, not sure what that means uint64_t drm_format_modifier; @@ -84,7 +85,9 @@ namespace va { } layers[4]; }; - /** Currently defined profiles */ + /** + * @brief Defined profiles + */ enum class profile_e { // Profile ID used for video processing. ProfileNone = -1, @@ -134,9 +137,9 @@ namespace va { IDCT = 3, MoComp = 4, Deblocking = 5, - EncSlice = 6, /* slice level encode */ - EncPicture = 7, /* pictuer encode, JPEG, etc */ - /* + EncSlice = 6, /** slice level encode */ + EncPicture = 7, /** picture encode, JPEG, etc */ + /** * For an implementation that supports a low power/high performance variant * for slice level encode, it can choose to expose the * VAEntrypointEncSliceLP entrypoint. Certain encoding tools may not be @@ -147,7 +150,7 @@ namespace va { EncSliceLP = 8, VideoProc = 10, /**< Video pre/post-processing. */ /** - * \brief FEI + * @brief FEI * * The purpose of FEI (Flexible Encoding Infrastructure) is to allow applications to * have more controls and trade off quality for speed with their own IPs. @@ -161,10 +164,10 @@ namespace va { * and VAEncFEIDistortionBufferType) for FEI entry function. * If separate PAK is set, two extra input buffers * (VAEncFEIMVBufferType, VAEncFEIMBModeBufferType) are needed for PAK input. - **/ + */ FEI = 11, /** - * \brief Stats + * @brief Stats * * A pre-processing function for getting some statistics and motion vectors is added, * and some extra controls for Encode pipeline are provided. The application can @@ -178,19 +181,19 @@ namespace va { * (VAStatsStatisticsParameterBufferType) and one or two output buffers * (VAStatsStatisticsBufferType, VAStatsStatisticsBottomFieldBufferType (for interlace only) * and VAStatsMVBufferType) are needed for this entry point. - **/ + */ Stats = 12, /** - * \brief ProtectedTEEComm + * @brief ProtectedTEEComm * * A function for communicating with TEE (Trusted Execution Environment). - **/ + */ ProtectedTEEComm = 13, /** - * \brief ProtectedContent + * @brief ProtectedContent * * A function for protected content to decrypt encrypted content. - **/ + */ ProtectedContent = 14, }; @@ -474,11 +477,11 @@ namespace va { }; /** - * This is a private structure of FFmpeg, I need this to manually create - * a VAAPI hardware context - * - * xdisplay will not be used internally by FFmpeg - */ + * This is a private structure of FFmpeg, I need this to manually create + * a VAAPI hardware context + * + * xdisplay will not be used internally by FFmpeg + */ typedef struct VAAPIDevicePriv { union { void *xdisplay; @@ -488,22 +491,22 @@ namespace va { } VAAPIDevicePriv; /** - * VAAPI connection details. - * - * Allocated as AVHWDeviceContext.hwctx - */ + * VAAPI connection details. + * + * Allocated as AVHWDeviceContext.hwctx + */ typedef struct AVVAAPIDeviceContext { /** - * The VADisplay handle, to be filled by the user. - */ + * The VADisplay handle, to be filled by the user. + */ va::VADisplay display; /** - * Driver quirks to apply - this is filled by av_hwdevice_ctx_init(), - * with reference to a table of known drivers, unless the - * AV_VAAPI_DRIVER_QUIRK_USER_SET bit is already present. The user - * may need to refer to this field when performing any later - * operations using VAAPI with the same VADisplay. - */ + * Driver quirks to apply - this is filled by av_hwdevice_ctx_init(), + * with reference to a table of known drivers, unless the + * AV_VAAPI_DRIVER_QUIRK_USER_SET bit is already present. The user + * may need to refer to this field when performing any later + * operations using VAAPI with the same VADisplay. + */ unsigned int driver_quirks; } AVVAAPIDeviceContext; @@ -512,6 +515,16 @@ namespace va { BOOST_LOG(*(boost::log::sources::severity_logger *) level) << msg; } + static void + vaapi_hwdevice_ctx_free(AVHWDeviceContext *ctx) { + auto hwctx = (AVVAAPIDeviceContext *) ctx->hwctx; + auto priv = (VAAPIDevicePriv *) ctx->user_opaque; + + vaTerminate(hwctx->display); + close(priv->drm_fd); + av_freep(&priv); + } + int vaapi_make_hwdevice_ctx(platf::hwdevice_t *base, AVBufferRef **hw_device_buf) { if (!va::initialize) { @@ -529,7 +542,6 @@ namespace va { auto *priv = (VAAPIDevicePriv *) av_mallocz(sizeof(VAAPIDevicePriv)); priv->drm_fd = fd; - priv->drm.fd = fd; auto fg = util::fail_guard([fd, priv]() { close(fd); @@ -559,9 +571,13 @@ namespace va { BOOST_LOG(debug) << "vaapi vendor: "sv << va::queryVendorString(display.get()); *hw_device_buf = av_hwdevice_ctx_alloc(AV_HWDEVICE_TYPE_VAAPI); - auto ctx = (AVVAAPIDeviceContext *) ((AVHWDeviceContext *) (*hw_device_buf)->data)->hwctx; - ctx->display = display.release(); + auto ctx = (AVHWDeviceContext *) (*hw_device_buf)->data; + auto hwctx = (AVVAAPIDeviceContext *) ctx->hwctx; + // Ownership of the VADisplay and DRM fd is now ours to manage via the free() function + hwctx->display = display.release(); + ctx->user_opaque = priv; + ctx->free = vaapi_hwdevice_ctx_free; fg.disable(); auto err = av_hwdevice_ctx_init(*hw_device_buf); @@ -626,11 +642,11 @@ namespace va { return false; } - if (config::video.hevc_mode > 1 && !query(display.get(), profile_e::HEVCMain)) { + if (video::active_hevc_mode > 1 && !query(display.get(), profile_e::HEVCMain)) { return false; } - if (config::video.hevc_mode > 2 && !query(display.get(), profile_e::HEVCMain10)) { + if (video::active_hevc_mode > 2 && !query(display.get(), profile_e::HEVCMain10)) { return false; } diff --git a/src/platform/linux/vaapi.h b/src/platform/linux/vaapi.h index 7f03c5a7d82..081d004897b 100644 --- a/src/platform/linux/vaapi.h +++ b/src/platform/linux/vaapi.h @@ -1,5 +1,8 @@ -#ifndef SUNSHINE_VAAPI_H -#define SUNSHINE_VAAPI_H +/** + * @file src/platform/linux/vaapi.h + * @brief todo + */ +#pragma once #include "misc.h" #include "src/platform/common.h" @@ -9,12 +12,12 @@ namespace egl { } namespace va { /** - * Width --> Width of the image - * Height --> Height of the image - * offset_x --> Horizontal offset of the image in the texture - * offset_y --> Vertical offset of the image in the texture - * file_t card --> The file descriptor of the render device used for encoding - */ + * Width --> Width of the image + * Height --> Height of the image + * offset_x --> Horizontal offset of the image in the texture + * offset_y --> Vertical offset of the image in the texture + * file_t card --> The file descriptor of the render device used for encoding + */ std::shared_ptr make_hwdevice(int width, int height, bool vram); std::shared_ptr @@ -29,4 +32,3 @@ namespace va { int init(); } // namespace va -#endif \ No newline at end of file diff --git a/src/platform/linux/wayland.cpp b/src/platform/linux/wayland.cpp index 5bd207cd318..d601ba95daa 100644 --- a/src/platform/linux/wayland.cpp +++ b/src/platform/linux/wayland.cpp @@ -1,3 +1,7 @@ +/** + * @file src/platform/linux/wayland.cpp + * @brief todo + */ #include #include diff --git a/src/platform/linux/wayland.h b/src/platform/linux/wayland.h index 18caf990861..a4c3aef17d9 100644 --- a/src/platform/linux/wayland.h +++ b/src/platform/linux/wayland.h @@ -1,5 +1,8 @@ -#ifndef SUNSHINE_WAYLAND_H -#define SUNSHINE_WAYLAND_H +/** + * @file src/platform/linux/wayland.h + * @brief todo + */ +#pragma once #include @@ -180,9 +183,9 @@ namespace wl { class display_t { public: /** - * Initialize display with display_name - * If display_name == nullptr -> display_name = std::getenv("WAYLAND_DISPLAY") - */ + * Initialize display with display_name + * If display_name == nullptr -> display_name = std::getenv("WAYLAND_DISPLAY") + */ int init(const char *display_name = nullptr); @@ -246,5 +249,3 @@ namespace wl { init() { return -1; } } // namespace wl #endif - -#endif \ No newline at end of file diff --git a/src/platform/linux/wlgrab.cpp b/src/platform/linux/wlgrab.cpp index db4d24a5093..6cf7fb78070 100644 --- a/src/platform/linux/wlgrab.cpp +++ b/src/platform/linux/wlgrab.cpp @@ -1,3 +1,7 @@ +/** + * @file src/platform/linux/wlgrab.cpp + * @brief todo + */ #include "src/platform/common.h" #include "src/main.h" @@ -80,7 +84,7 @@ namespace wl { } inline platf::capture_e - snapshot(platf::img_t *img_out_base, std::chrono::milliseconds timeout, bool cursor) { + snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor) { auto to = std::chrono::steady_clock::now() + timeout; dmabuf.listen(interface.dmabuf_manager, output, cursor); @@ -118,10 +122,10 @@ namespace wl { class wlr_ram_t: public wlr_t { public: platf::capture_e - capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr img, bool *cursor) override { + capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override { auto next_frame = std::chrono::steady_clock::now(); - while (img) { + while (true) { auto now = std::chrono::steady_clock::now(); if (next_frame > now) { @@ -132,16 +136,22 @@ namespace wl { } next_frame = now + delay; - auto status = snapshot(img.get(), 1000ms, *cursor); + std::shared_ptr img_out; + auto status = snapshot(pull_free_image_cb, img_out, 1000ms, *cursor); switch (status) { case platf::capture_e::reinit: case platf::capture_e::error: + case platf::capture_e::interrupted: return status; case platf::capture_e::timeout: - img = snapshot_cb(img, false); + if (!push_captured_image_cb(std::move(img_out), false)) { + return platf::capture_e::ok; + } break; case platf::capture_e::ok: - img = snapshot_cb(img, true); + if (!push_captured_image_cb(std::move(img_out), true)) { + return platf::capture_e::ok; + } break; default: BOOST_LOG(error) << "Unrecognized capture status ["sv << (int) status << ']'; @@ -153,8 +163,8 @@ namespace wl { } platf::capture_e - snapshot(platf::img_t *img_out_base, std::chrono::milliseconds timeout, bool cursor) { - auto status = wlr_t::snapshot(img_out_base, timeout, cursor); + snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor) { + auto status = wlr_t::snapshot(pull_free_image_cb, img_out, timeout, cursor); if (status != platf::capture_e::ok) { return status; } @@ -167,6 +177,10 @@ namespace wl { return platf::capture_e::reinit; } + if (!pull_free_image_cb(img_out)) { + return platf::capture_e::interrupted; + } + gl::ctx.BindTexture(GL_TEXTURE_2D, (*rgb_opt)->tex[0]); int w, h; @@ -174,7 +188,7 @@ namespace wl { gl::ctx.GetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &h); BOOST_LOG(debug) << "width and height: w "sv << w << " h "sv << h; - gl::ctx.GetTextureSubImage((*rgb_opt)->tex[0], 0, 0, 0, 0, width, height, 1, GL_BGRA, GL_UNSIGNED_BYTE, img_out_base->height * img_out_base->row_pitch, img_out_base->data); + gl::ctx.GetTextureSubImage((*rgb_opt)->tex[0], 0, 0, 0, 0, width, height, 1, GL_BGRA, GL_UNSIGNED_BYTE, img_out->height * img_out->row_pitch, img_out->data); gl::ctx.BindTexture(GL_TEXTURE_2D, 0); return platf::capture_e::ok; @@ -229,10 +243,10 @@ namespace wl { class wlr_vram_t: public wlr_t { public: platf::capture_e - capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr img, bool *cursor) override { + capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override { auto next_frame = std::chrono::steady_clock::now(); - while (img) { + while (true) { auto now = std::chrono::steady_clock::now(); if (next_frame > now) { @@ -243,16 +257,22 @@ namespace wl { } next_frame = now + delay; - auto status = snapshot(img.get(), 1000ms, *cursor); + std::shared_ptr img_out; + auto status = snapshot(pull_free_image_cb, img_out, 1000ms, *cursor); switch (status) { case platf::capture_e::reinit: case platf::capture_e::error: + case platf::capture_e::interrupted: return status; case platf::capture_e::timeout: - img = snapshot_cb(img, false); + if (!push_captured_image_cb(std::move(img_out), false)) { + return platf::capture_e::ok; + } break; case platf::capture_e::ok: - img = snapshot_cb(img, true); + if (!push_captured_image_cb(std::move(img_out), true)) { + return platf::capture_e::ok; + } break; default: BOOST_LOG(error) << "Unrecognized capture status ["sv << (int) status << ']'; @@ -264,13 +284,16 @@ namespace wl { } platf::capture_e - snapshot(platf::img_t *img_out_base, std::chrono::milliseconds timeout, bool cursor) { - auto status = wlr_t::snapshot(img_out_base, timeout, cursor); + snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor) { + auto status = wlr_t::snapshot(pull_free_image_cb, img_out, timeout, cursor); if (status != platf::capture_e::ok) { return status; } - auto img = (egl::img_descriptor_t *) img_out_base; + if (!pull_free_image_cb(img_out)) { + return platf::capture_e::interrupted; + } + auto img = (egl::img_descriptor_t *) img_out.get(); img->reset(); auto current_frame = dmabuf.current_frame; @@ -311,7 +334,16 @@ namespace wl { int dummy_img(platf::img_t *img) override { - return snapshot(img, 1000ms, false) != platf::capture_e::ok; + // TODO: stop cheating and give black image + if (!img) { + return -1; + }; + auto pull_dummy_img_callback = [&img](std::shared_ptr &img_out) -> bool { + img_out = img->shared_from_this(); + return true; + }; + std::shared_ptr img_out; + return snapshot(pull_dummy_img_callback, img_out, 1000ms, false) != platf::capture_e::ok; } std::uint64_t sequence {}; diff --git a/src/platform/linux/x11grab.cpp b/src/platform/linux/x11grab.cpp index ae294cf1a78..ad8ef0343ff 100644 --- a/src/platform/linux/x11grab.cpp +++ b/src/platform/linux/x11grab.cpp @@ -1,7 +1,7 @@ -// -// Created by loki on 6/21/19. -// - +/** + * @file src/platform/linux/x11grab.cpp + * @brief todo + */ #include "src/platform/common.h" #include @@ -389,10 +389,10 @@ namespace platf { mem_type_e mem_type; - /* - * Last X (NOT the streamed monitor!) size. - * This way we can trigger reinitialization if the dimensions changed while streaming - */ + /** + * Last X (NOT the streamed monitor!) size. + * This way we can trigger reinitialization if the dimensions changed while streaming + */ // int env_width, env_height; x11_attr_t(mem_type_e mem_type): @@ -468,18 +468,18 @@ namespace platf { } /** - * Called when the display attributes should change. - */ + * Called when the display attributes should change. + */ void refresh() { - x11::GetWindowAttributes(xdisplay.get(), xwindow, &xattr); //Update xattr's + x11::GetWindowAttributes(xdisplay.get(), xwindow, &xattr); // Update xattr's } capture_e - capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr img, bool *cursor) override { + capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override { auto next_frame = std::chrono::steady_clock::now(); - while (img) { + while (true) { auto now = std::chrono::steady_clock::now(); if (next_frame > now) { @@ -491,16 +491,22 @@ namespace platf { } next_frame = now + delay; - auto status = snapshot(img.get(), 1000ms, *cursor); + std::shared_ptr img_out; + auto status = snapshot(pull_free_image_cb, img_out, 1000ms, *cursor); switch (status) { case platf::capture_e::reinit: case platf::capture_e::error: + case platf::capture_e::interrupted: return status; case platf::capture_e::timeout: - img = snapshot_cb(img, false); + if (!push_captured_image_cb(std::move(img_out), false)) { + return platf::capture_e::ok; + } break; case platf::capture_e::ok: - img = snapshot_cb(img, true); + if (!push_captured_image_cb(std::move(img_out), true)) { + return platf::capture_e::ok; + } break; default: BOOST_LOG(error) << "Unrecognized capture status ["sv << (int) status << ']'; @@ -512,26 +518,31 @@ namespace platf { } capture_e - snapshot(img_t *img_out_base, std::chrono::milliseconds timeout, bool cursor) { + snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor) { refresh(); - //The whole X server changed, so we must reinit everything + // The whole X server changed, so we must reinit everything if (xattr.width != env_width || xattr.height != env_height) { BOOST_LOG(warning) << "X dimensions changed in non-SHM mode, request reinit"sv; return capture_e::reinit; } - XImage *img { x11::GetImage(xdisplay.get(), xwindow, offset_x, offset_y, width, height, AllPlanes, ZPixmap) }; - auto img_out = (x11_img_t *) img_out_base; - img_out->width = img->width; - img_out->height = img->height; - img_out->data = (uint8_t *) img->data; - img_out->row_pitch = img->bytes_per_line; - img_out->pixel_pitch = img->bits_per_pixel / 8; - img_out->img.reset(img); + if (!pull_free_image_cb(img_out)) { + return platf::capture_e::interrupted; + } + auto img = (x11_img_t *) img_out.get(); + + XImage *x_img { x11::GetImage(xdisplay.get(), xwindow, offset_x, offset_y, width, height, AllPlanes, ZPixmap) }; + + img->width = x_img->width; + img->height = x_img->height; + img->data = (uint8_t *) x_img->data; + img->row_pitch = x_img->bytes_per_line; + img->pixel_pitch = x_img->bits_per_pixel / 8; + img->img.reset(x_img); if (cursor) { - blend_cursor(xdisplay.get(), *img_out_base, offset_x, offset_y); + blend_cursor(xdisplay.get(), *img, offset_x, offset_y); } return capture_e::ok; @@ -559,7 +570,16 @@ namespace platf { int dummy_img(img_t *img) override { - snapshot(img, 0s, true); + // TODO: stop cheating and give black image + if (!img) { + return -1; + }; + auto pull_dummy_img_callback = [&img](std::shared_ptr &img_out) -> bool { + img_out = img->shared_from_this(); + return true; + }; + std::shared_ptr img_out; + snapshot(pull_dummy_img_callback, img_out, 0s, true); return 0; } }; @@ -594,10 +614,10 @@ namespace platf { } capture_e - capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr img, bool *cursor) override { + capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override { auto next_frame = std::chrono::steady_clock::now(); - while (img) { + while (true) { auto now = std::chrono::steady_clock::now(); if (next_frame > now) { @@ -609,16 +629,22 @@ namespace platf { } next_frame = now + delay; - auto status = snapshot(img.get(), 1000ms, *cursor); + std::shared_ptr img_out; + auto status = snapshot(pull_free_image_cb, img_out, 1000ms, *cursor); switch (status) { case platf::capture_e::reinit: case platf::capture_e::error: + case platf::capture_e::interrupted: return status; case platf::capture_e::timeout: - img = snapshot_cb(img, false); + if (!push_captured_image_cb(std::move(img_out), false)) { + return platf::capture_e::ok; + } break; case platf::capture_e::ok: - img = snapshot_cb(img, true); + if (!push_captured_image_cb(std::move(img_out), true)) { + return platf::capture_e::ok; + } break; default: BOOST_LOG(error) << "Unrecognized capture status ["sv << (int) status << ']'; @@ -630,8 +656,8 @@ namespace platf { } capture_e - snapshot(img_t *img, std::chrono::milliseconds timeout, bool cursor) { - //The whole X server changed, so we must reinit everything + snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor) { + // The whole X server changed, so we must reinit everything if (xattr.width != env_width || xattr.height != env_height) { BOOST_LOG(warning) << "X dimensions changed in SHM mode, request reinit"sv; return capture_e::reinit; @@ -645,10 +671,14 @@ namespace platf { return capture_e::reinit; } - std::copy_n((std::uint8_t *) data.data, frame_size(), img->data); + if (!pull_free_image_cb(img_out)) { + return platf::capture_e::interrupted; + } + + std::copy_n((std::uint8_t *) data.data, frame_size(), img_out->data); if (cursor) { - blend_cursor(shm_xdisplay.get(), *img, offset_x, offset_y); + blend_cursor(shm_xdisplay.get(), *img_out, offset_x, offset_y); } return capture_e::ok; diff --git a/src/platform/linux/x11grab.h b/src/platform/linux/x11grab.h index 89428a49525..24e96f6a1fd 100644 --- a/src/platform/linux/x11grab.h +++ b/src/platform/linux/x11grab.h @@ -1,5 +1,8 @@ -#ifndef SUNSHINE_X11_GRAB -#define SUNSHINE_X11_GRAB +/** + * @file src/platform/linux/x11grab.h + * @brief todo + */ +#pragma once #include @@ -34,11 +37,11 @@ namespace platf::x11 { capture(egl::cursor_t &img); /** - * Capture and blend the cursor into the image - * - * img <-- destination image - * offsetX, offsetY <--- Top left corner of the virtual screen - */ + * Capture and blend the cursor into the image + * + * img <-- destination image + * offsetX, offsetY <--- Top left corner of the virtual screen + */ void blend(img_t &img, int offsetX, int offsetY); @@ -66,5 +69,3 @@ namespace platf::x11 { make_display() { return nullptr; } #endif } // namespace platf::x11 - -#endif \ No newline at end of file diff --git a/src/platform/macos/av_audio.h b/src/platform/macos/av_audio.h index 48a83ac4ae5..c0d221734da 100644 --- a/src/platform/macos/av_audio.h +++ b/src/platform/macos/av_audio.h @@ -1,5 +1,8 @@ -#ifndef SUNSHINE_PLATFORM_AV_AUDIO_H -#define SUNSHINE_PLATFORM_AV_AUDIO_H +/** + * @file src/platform/macos/av_audio.h + * @brief todo + */ +#pragma once #import @@ -22,5 +25,3 @@ - (int)setupMicrophone:(AVCaptureDevice *)device sampleRate:(UInt32)sampleRate frameSize:(UInt32)frameSize channels:(UInt8)channels; @end - -#endif //SUNSHINE_PLATFORM_AV_AUDIO_H diff --git a/src/platform/macos/av_audio.m b/src/platform/macos/av_audio.m index ef1ce294488..af695179c7b 100644 --- a/src/platform/macos/av_audio.m +++ b/src/platform/macos/av_audio.m @@ -1,3 +1,7 @@ +/** + * @file src/platform/macos/av_audio.m + * @brief todo + */ #import "av_audio.h" @implementation AVAudio @@ -126,7 +130,7 @@ - (void)captureOutput:(AVCaptureOutput *)output CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer(sampleBuffer, NULL, &audioBufferList, sizeof(audioBufferList), NULL, NULL, 0, &blockBuffer); - //NSAssert(audioBufferList.mNumberBuffers == 1, @"Expected interlveaved PCM format but buffer contained %u streams", audioBufferList.mNumberBuffers); + // NSAssert(audioBufferList.mNumberBuffers == 1, @"Expected interlveaved PCM format but buffer contained %u streams", audioBufferList.mNumberBuffers); // this is safe, because an interleaved PCM stream has exactly one buffer // and we don't want to do sanity checks in a performance critical exec path diff --git a/src/platform/macos/av_img_t.h b/src/platform/macos/av_img_t.h index f946c6d53ce..f42f9ceb12e 100644 --- a/src/platform/macos/av_img_t.h +++ b/src/platform/macos/av_img_t.h @@ -1,5 +1,8 @@ -#ifndef av_img_t_h -#define av_img_t_h +/** + * @file src/platform/macos/av_img_t.h + * @brief todo + */ +#pragma once #include "src/platform/common.h" @@ -14,5 +17,3 @@ namespace platf { ~av_img_t(); }; } // namespace platf - -#endif /* av_img_t_h */ diff --git a/src/platform/macos/av_video.h b/src/platform/macos/av_video.h index d654ff6792b..83eabb8ebd4 100644 --- a/src/platform/macos/av_video.h +++ b/src/platform/macos/av_video.h @@ -1,5 +1,8 @@ -#ifndef SUNSHINE_PLATFORM_AV_VIDEO_H -#define SUNSHINE_PLATFORM_AV_VIDEO_H +/** + * @file src/platform/macos/av_video.h + * @brief todo + */ +#pragma once #import @@ -38,5 +41,3 @@ typedef bool (^FrameCallbackBlock)(CMSampleBufferRef); - (dispatch_semaphore_t)capture:(FrameCallbackBlock)frameCallback; @end - -#endif //SUNSHINE_PLATFORM_AV_VIDEO_H diff --git a/src/platform/macos/av_video.m b/src/platform/macos/av_video.m index 92f5ec083cc..6e3a9f81f66 100644 --- a/src/platform/macos/av_video.m +++ b/src/platform/macos/av_video.m @@ -1,3 +1,7 @@ +/** + * @file src/platform/macos/av_video.m + * @brief todo + */ #import "av_video.h" @implementation AVVideo diff --git a/src/platform/macos/display.mm b/src/platform/macos/display.mm index f053ea03911..65f3c279ddc 100644 --- a/src/platform/macos/display.mm +++ b/src/platform/macos/display.mm @@ -1,3 +1,7 @@ +/** + * @file src/platform/macos/display.mm + * @brief todo + */ #include "src/platform/common.h" #include "src/platform/macos/av_img_t.h" #include "src/platform/macos/av_video.h" @@ -37,38 +41,46 @@ } capture_e - capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr img, bool *cursor) override { - __block auto img_next = std::move(img); - + capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override { auto signal = [av_capture capture:^(CMSampleBufferRef sampleBuffer) { - auto av_img_next = std::static_pointer_cast(img_next); - CFRetain(sampleBuffer); CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); CVPixelBufferLockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly); - if (av_img_next->pixel_buffer != nullptr) - CVPixelBufferUnlockBaseAddress(av_img_next->pixel_buffer, 0); + std::shared_ptr img_out; + if (!pull_free_image_cb(img_out)) { + // got interrupt signal + // returning false here stops capture backend + return false; + } + auto av_img = std::static_pointer_cast(img_out); + + if (av_img->pixel_buffer != nullptr) + CVPixelBufferUnlockBaseAddress(av_img->pixel_buffer, 0); - if (av_img_next->sample_buffer != nullptr) - CFRelease(av_img_next->sample_buffer); + if (av_img->sample_buffer != nullptr) + CFRelease(av_img->sample_buffer); - av_img_next->sample_buffer = sampleBuffer; - av_img_next->pixel_buffer = pixelBuffer; - img_next->data = (uint8_t *) CVPixelBufferGetBaseAddress(pixelBuffer); + av_img->sample_buffer = sampleBuffer; + av_img->pixel_buffer = pixelBuffer; + img_out->data = (uint8_t *) CVPixelBufferGetBaseAddress(pixelBuffer); size_t extraPixels[4]; CVPixelBufferGetExtendedPixels(pixelBuffer, &extraPixels[0], &extraPixels[1], &extraPixels[2], &extraPixels[3]); - img_next->width = CVPixelBufferGetWidth(pixelBuffer) + extraPixels[0] + extraPixels[1]; - img_next->height = CVPixelBufferGetHeight(pixelBuffer) + extraPixels[2] + extraPixels[3]; - img_next->row_pitch = CVPixelBufferGetBytesPerRow(pixelBuffer); - img_next->pixel_pitch = img_next->row_pitch / img_next->width; + img_out->width = CVPixelBufferGetWidth(pixelBuffer) + extraPixels[0] + extraPixels[1]; + img_out->height = CVPixelBufferGetHeight(pixelBuffer) + extraPixels[2] + extraPixels[3]; + img_out->row_pitch = CVPixelBufferGetBytesPerRow(pixelBuffer); + img_out->pixel_pitch = img_out->row_pitch / img_out->width; - img_next = snapshot_cb(img_next, true); + if (!push_captured_image_cb(std::move(img_out), false)) { + // got interrupt signal + // returning false here stops capture backend + return false; + } - return img_next != nullptr; + return true; }]; // FIXME: We should time out if an image isn't returned for a while @@ -132,6 +144,7 @@ img->row_pitch = CVPixelBufferGetBytesPerRow(pixelBuffer); img->pixel_pitch = img->row_pitch / img->width; + // returning false here stops capture backend return false; }]; @@ -141,12 +154,12 @@ } /** - * A bridge from the pure C++ code of the hwdevice_t class to the pure Objective C code. - * - * display --> an opaque pointer to an object of this class - * width --> the intended capture width - * height --> the intended capture height - */ + * A bridge from the pure C++ code of the hwdevice_t class to the pure Objective C code. + * + * display --> an opaque pointer to an object of this class + * width --> the intended capture width + * height --> the intended capture height + */ static void setResolution(void *display, int width, int height) { [static_cast(display) setFrameWidth:width frameHeight:height]; diff --git a/src/platform/macos/input.cpp b/src/platform/macos/input.cpp index 19948a957dd..78079e3f386 100644 --- a/src/platform/macos/input.cpp +++ b/src/platform/macos/input.cpp @@ -1,3 +1,7 @@ +/** + * @file src/platform/macos/input.cpp + * @brief todo + */ #import #include #include @@ -230,7 +234,7 @@ const KeyCodeMap kKeyCodesMap[] = { } void - keyboard(input_t &input, uint16_t modcode, bool release) { + keyboard(input_t &input, uint16_t modcode, bool release, uint8_t flags) { auto key = keysym(modcode); BOOST_LOG(debug) << "got keycode: 0x"sv << std::hex << modcode << ", translated to: 0x" << std::hex << key << ", release:" << release; diff --git a/src/platform/macos/microphone.mm b/src/platform/macos/microphone.mm index df40db9d1a7..854ca6faffe 100644 --- a/src/platform/macos/microphone.mm +++ b/src/platform/macos/microphone.mm @@ -1,3 +1,7 @@ +/** + * @file src/platform/macos/microphone.mm + * @brief todo + */ #include "src/platform/common.h" #include "src/platform/macos/av_audio.h" diff --git a/src/platform/macos/misc.h b/src/platform/macos/misc.h index 10b89bf4459..a6fb1df3244 100644 --- a/src/platform/macos/misc.h +++ b/src/platform/macos/misc.h @@ -1,5 +1,8 @@ -#ifndef SUNSHINE_PLATFORM_MISC_H -#define SUNSHINE_PLATFORM_MISC_H +/** + * @file src/platform/macos/misc.h + * @brief todo + */ +#pragma once #include @@ -14,5 +17,3 @@ namespace dyn { handle(const std::vector &libs); } // namespace dyn - -#endif diff --git a/src/platform/macos/misc.mm b/src/platform/macos/misc.mm index 3d8637297c5..fb2b41b2b39 100644 --- a/src/platform/macos/misc.mm +++ b/src/platform/macos/misc.mm @@ -1,8 +1,13 @@ +/** + * @file src/platform/macos/misc.mm + * @brief todo + */ #include #include #include #include #include +#include #include #include @@ -81,15 +86,14 @@ std::string from_sockaddr(const sockaddr *const ip_addr) { - char data[INET6_ADDRSTRLEN]; + char data[INET6_ADDRSTRLEN] = {}; auto family = ip_addr->sa_family; if (family == AF_INET6) { inet_ntop(AF_INET6, &((sockaddr_in6 *) ip_addr)->sin6_addr, data, INET6_ADDRSTRLEN); } - - if (family == AF_INET) { + else if (family == AF_INET) { inet_ntop(AF_INET, &((sockaddr_in *) ip_addr)->sin_addr, data, INET_ADDRSTRLEN); } @@ -99,17 +103,16 @@ std::pair from_sockaddr_ex(const sockaddr *const ip_addr) { - char data[INET6_ADDRSTRLEN]; + char data[INET6_ADDRSTRLEN] = {}; auto family = ip_addr->sa_family; - std::uint16_t port; + std::uint16_t port = 0; if (family == AF_INET6) { inet_ntop(AF_INET6, &((sockaddr_in6 *) ip_addr)->sin6_addr, data, INET6_ADDRSTRLEN); port = ((sockaddr_in6 *) ip_addr)->sin6_port; } - - if (family == AF_INET) { + else if (family == AF_INET) { inet_ntop(AF_INET, &((sockaddr_in *) ip_addr)->sin_addr, data, INET_ADDRSTRLEN); port = ((sockaddr_in *) ip_addr)->sin_port; @@ -158,8 +161,7 @@ } bp::child - run_unprivileged(const std::string &cmd, boost::filesystem::path &working_dir, bp::environment &env, FILE *file, std::error_code &ec, bp::group *group) { - BOOST_LOG(warning) << "run_unprivileged() is not yet implemented for this platform. The new process will run with Sunshine's permissions."sv; + run_command(bool elevated, bool interactive, const std::string &cmd, boost::filesystem::path &working_dir, bp::environment &env, FILE *file, std::error_code &ec, bp::group *group) { if (!group) { if (!file) { return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_out > bp::null, bp::std_err > bp::null, ec); @@ -178,6 +180,27 @@ } } + /** + * @brief Open a url in the default web browser. + * @param url The url to open. + */ + void + open_url(const std::string &url) { + boost::filesystem::path working_dir; + std::string cmd = R"(open ")" + url + R"(")"; + + boost::process::environment _env = boost::this_process::environment(); + std::error_code ec; + auto child = run_command(false, false, cmd, working_dir, _env, nullptr, ec, nullptr); + if (ec) { + BOOST_LOG(warning) << "Couldn't open url ["sv << url << "]: System: "sv << ec.message(); + } + else { + BOOST_LOG(info) << "Opened url ["sv << url << "]"sv; + child.detach(); + } + } + void adjust_thread_priority(thread_priority_e priority) { // Unimplemented @@ -193,16 +216,33 @@ // Nothing to do } - bool - restart_supported() { - // Restart not supported yet - return false; + void + restart_on_exit() { + char executable[2048]; + uint32_t size = sizeof(executable); + if (_NSGetExecutablePath(executable, &size) < 0) { + BOOST_LOG(fatal) << "NSGetExecutablePath() failed: "sv << errno; + return; + } + + // ASIO doesn't use O_CLOEXEC, so we have to close all fds ourselves + int openmax = (int) sysconf(_SC_OPEN_MAX); + for (int fd = STDERR_FILENO + 1; fd < openmax; fd++) { + close(fd); + } + + // Re-exec ourselves with the same arguments + if (execv(executable, lifetime::get_argv()) < 0) { + BOOST_LOG(fatal) << "execv() failed: "sv << errno; + return; + } } - bool + void restart() { - // Restart not supported yet - return false; + // Gracefully clean up and restart ourselves instead of exiting + atexit(restart_on_exit); + lifetime::exit_sunshine(0, true); } bool diff --git a/src/platform/macos/nv12_zero_device.cpp b/src/platform/macos/nv12_zero_device.cpp index 0c62328f762..21046be3054 100644 --- a/src/platform/macos/nv12_zero_device.cpp +++ b/src/platform/macos/nv12_zero_device.cpp @@ -1,3 +1,7 @@ +/** + * @file src/platform/macos/nv12_zero_device.cpp + * @brief todo + */ #include "src/platform/macos/nv12_zero_device.h" #include "src/platform/macos/av_img_t.h" diff --git a/src/platform/macos/nv12_zero_device.h b/src/platform/macos/nv12_zero_device.h index 53b6eff19d7..059896ea156 100644 --- a/src/platform/macos/nv12_zero_device.h +++ b/src/platform/macos/nv12_zero_device.h @@ -1,5 +1,8 @@ -#ifndef vtdevice_h -#define vtdevice_h +/** + * @file src/platform/macos/nv12_zero_device.h + * @brief todo + */ +#pragma once #include "src/platform/common.h" @@ -29,5 +32,3 @@ namespace platf { }; } // namespace platf - -#endif /* vtdevice_h */ diff --git a/src/platform/macos/publish.cpp b/src/platform/macos/publish.cpp index c22d991dc78..4cb80b8dbdb 100644 --- a/src/platform/macos/publish.cpp +++ b/src/platform/macos/publish.cpp @@ -1,5 +1,9 @@ - -// adapted from https://www.avahi.org/doxygen/html/client-publish-service_8c-example.html +/** + * @file src/platform/macos/publish.cpp + * @brief todo + * @note Adapted from https://www.avahi.org/doxygen/html/client-publish-service_8c-example.html + * @todo Use a common file for this and src/platform/linux/publish.cpp + */ #include #include "misc.h" @@ -12,7 +16,9 @@ using namespace std::literals; namespace avahi { - /** Error codes used by avahi */ + /** + * @brief Error codes used by avahi. + */ enum err_e { OK = 0, /**< OK */ ERR_FAILURE = -1, /**< Generic error code */ @@ -113,7 +119,9 @@ namespace avahi { CLIENT_NO_FAIL = 2 /**< Don't fail if the daemon is not available when avahi_client_new() is called, instead enter CLIENT_CONNECTING state and wait for the daemon to appear */ }; - /** Some flags for publishing functions */ + /** + * @brief Flags for publishing functions. + */ enum PublishFlags { PUBLISH_UNIQUE = 1, /**< For raw records: The RRset is intended to be unique */ PUBLISH_NO_PROBE = 2, /**< For raw records: Though the RRset is intended to be unique no probes shall be sent */ @@ -434,4 +442,4 @@ namespace platf::publish { return std::make_unique(std::thread { avahi::simple_poll_loop, poll.get() }); } -}; // namespace platf::publish +} // namespace platf::publish diff --git a/src/platform/windows/PolicyConfig.h b/src/platform/windows/PolicyConfig.h index b0a1264e481..21772227c19 100644 --- a/src/platform/windows/PolicyConfig.h +++ b/src/platform/windows/PolicyConfig.h @@ -1,13 +1,15 @@ -// ---------------------------------------------------------------------------- -// PolicyConfig.h -// Undocumented COM-interface IPolicyConfig. -// Use for set default audio render endpoint -// @author EreTIk -// http://eretik.omegahg.com/ -// ---------------------------------------------------------------------------- +/** + * @file src/platform/windows/PolicyConfig.h + * @brief Undocumented COM-interface IPolicyConfig. + * @details Use for setting default audio render endpoint. + * @author EreTIk + * @see http://eretik.omegahg.com/ + */ #pragma once +#include + #ifdef __MINGW32__ #undef DEFINE_GUID #ifdef __cplusplus @@ -99,81 +101,3 @@ interface IPolicyConfig: public IUnknown { PCWSTR, INT); }; - -interface DECLSPEC_UUID("568b9108-44bf-40b4-9006-86afe5b5a620") IPolicyConfigVista; -class DECLSPEC_UUID("294935CE-F637-4E7C-A41B-AB255460B862") CPolicyConfigVistaClient; -// ---------------------------------------------------------------------------- -// class CPolicyConfigVistaClient -// {294935CE-F637-4E7C-A41B-AB255460B862} -// -// interface IPolicyConfigVista -// {568b9108-44bf-40b4-9006-86afe5b5a620} -// -// Query interface: -// CComPtr PolicyConfig; -// PolicyConfig.CoCreateInstance(__uuidof(CPolicyConfigVistaClient)); -// -// @compatible: Windows Vista and Later -// ---------------------------------------------------------------------------- -interface IPolicyConfigVista: public IUnknown { -public: - virtual HRESULT - GetMixFormat( - PCWSTR, - WAVEFORMATEX **); // not available on Windows 7, use method from IPolicyConfig - - virtual HRESULT STDMETHODCALLTYPE - GetDeviceFormat( - PCWSTR, - INT, - WAVEFORMATEX **); - - virtual HRESULT STDMETHODCALLTYPE - SetDeviceFormat( - PCWSTR, - WAVEFORMATEX *, - WAVEFORMATEX *); - - virtual HRESULT STDMETHODCALLTYPE GetProcessingPeriod( - PCWSTR, - INT, - PINT64, - PINT64); // not available on Windows 7, use method from IPolicyConfig - - virtual HRESULT STDMETHODCALLTYPE SetProcessingPeriod( - PCWSTR, - PINT64); // not available on Windows 7, use method from IPolicyConfig - - virtual HRESULT STDMETHODCALLTYPE - GetShareMode( - PCWSTR, - struct DeviceShareMode *); // not available on Windows 7, use method from IPolicyConfig - - virtual HRESULT STDMETHODCALLTYPE - SetShareMode( - PCWSTR, - struct DeviceShareMode *); // not available on Windows 7, use method from IPolicyConfig - - virtual HRESULT STDMETHODCALLTYPE - GetPropertyValue( - PCWSTR, - const PROPERTYKEY &, - PROPVARIANT *); - - virtual HRESULT STDMETHODCALLTYPE - SetPropertyValue( - PCWSTR, - const PROPERTYKEY &, - PROPVARIANT *); - - virtual HRESULT STDMETHODCALLTYPE - SetDefaultEndpoint( - PCWSTR wszDeviceId, - ERole eRole); - - virtual HRESULT STDMETHODCALLTYPE SetEndpointVisibility( - PCWSTR, - INT); // not available on Windows 7, use method from IPolicyConfig -}; - -// ---------------------------------------------------------------------------- diff --git a/src/platform/windows/audio.cpp b/src/platform/windows/audio.cpp index 9f38f5b6f87..eac55e89e22 100644 --- a/src/platform/windows/audio.cpp +++ b/src/platform/windows/audio.cpp @@ -1,7 +1,7 @@ -// -// Created by loki on 1/12/20. -// - +/** + * @file src/platform/windows/audio.cpp + * @brief todo + */ #include #include #include @@ -10,6 +10,8 @@ #include +#include + #define INITGUID #include #undef INITGUID @@ -32,10 +34,20 @@ const IID IID_IMMDeviceEnumerator = __uuidof(IMMDeviceEnumerator); const IID IID_IAudioClient = __uuidof(IAudioClient); const IID IID_IAudioCaptureClient = __uuidof(IAudioCaptureClient); +#if defined(__x86_64) || defined(_M_AMD64) + #define STEAM_DRIVER_SUBDIR L"x64" +#elif defined(__i386) || defined(_M_IX86) + #define STEAM_DRIVER_SUBDIR L"x86" +#else + #warning No known Steam audio driver for this architecture +#endif + using namespace std::literals; namespace platf::audio { constexpr auto SAMPLE_RATE = 48000; + constexpr auto STEAM_AUDIO_DRIVER_PATH = L"%CommonProgramFiles(x86)%\\Steam\\drivers\\Windows10\\" STEAM_DRIVER_SUBDIR L"\\SteamStreamingSpeakers.inf"; + template void Release(T *p) { @@ -254,7 +266,7 @@ namespace platf::audio { &device); if (FAILED(status)) { - BOOST_LOG(error) << "Couldn't create audio Device [0x"sv << util::hex(status).to_string_view() << ']'; + BOOST_LOG(error) << "Couldn't get default audio endpoint [0x"sv << util::hex(status).to_string_view() << ']'; return nullptr; } @@ -262,6 +274,78 @@ namespace platf::audio { return device; } + class audio_notification_t: public ::IMMNotificationClient { + public: + audio_notification_t() {} + + // IUnknown implementation (unused by IMMDeviceEnumerator) + ULONG STDMETHODCALLTYPE + AddRef() { + return 1; + } + + ULONG STDMETHODCALLTYPE + Release() { + return 1; + } + + HRESULT STDMETHODCALLTYPE + QueryInterface(REFIID riid, VOID **ppvInterface) { + if (IID_IUnknown == riid) { + AddRef(); + *ppvInterface = (IUnknown *) this; + return S_OK; + } + else if (__uuidof(IMMNotificationClient) == riid) { + AddRef(); + *ppvInterface = (IMMNotificationClient *) this; + return S_OK; + } + else { + *ppvInterface = NULL; + return E_NOINTERFACE; + } + } + + // IMMNotificationClient + HRESULT STDMETHODCALLTYPE + OnDefaultDeviceChanged(EDataFlow flow, ERole role, LPCWSTR pwstrDeviceId) { + if (flow == eRender) { + default_render_device_changed_flag.store(true); + } + return S_OK; + } + + HRESULT STDMETHODCALLTYPE + OnDeviceAdded(LPCWSTR pwstrDeviceId) { return S_OK; } + + HRESULT STDMETHODCALLTYPE + OnDeviceRemoved(LPCWSTR pwstrDeviceId) { return S_OK; } + + HRESULT STDMETHODCALLTYPE + OnDeviceStateChanged( + LPCWSTR pwstrDeviceId, + DWORD dwNewState) { return S_OK; } + + HRESULT STDMETHODCALLTYPE + OnPropertyValueChanged( + LPCWSTR pwstrDeviceId, + const PROPERTYKEY key) { return S_OK; } + + /** + * @brief Checks if the default rendering device changed and resets the change flag + * + * @return true if the device changed since last call + */ + bool + check_default_render_device_changed() { + return default_render_device_changed_flag.exchange(false); + } + + private: + std::atomic_bool default_render_device_changed_flag; + }; + class mic_wasapi_t: public mic_t { public: capture_e @@ -310,6 +394,13 @@ namespace platf::audio { return -1; } + status = device_enum->RegisterEndpointNotificationCallback(&endpt_notification); + if (FAILED(status)) { + BOOST_LOG(error) << "Couldn't register endpoint notification [0x"sv << util::hex(status).to_string_view() << ']'; + + return -1; + } + auto device = default_device(device_enum); if (!device) { return -1; @@ -377,6 +468,10 @@ namespace platf::audio { } ~mic_wasapi_t() override { + if (device_enum) { + device_enum->UnregisterEndpointNotificationCallback(&endpt_notification); + } + if (audio_client) { audio_client->Stop(); } @@ -398,6 +493,17 @@ namespace platf::audio { std::uint32_t audio_sample_size; } block_aligned; + // Check if the default audio device has changed + if (endpt_notification.check_default_render_device_changed()) { + // Invoke the audio_control_t's callback if it wants one + if (default_endpt_changed_cb) { + (*default_endpt_changed_cb)(); + } + + // Reinitialize to pick up the new default device + return capture_e::reinit; + } + status = WaitForSingleObjectEx(audio_event.get(), default_latency_ms, FALSE); switch (status) { case WAIT_OBJECT_0: @@ -465,6 +571,9 @@ namespace platf::audio { audio_client_t audio_client; audio_capture_t audio_capture; + audio_notification_t endpt_notification; + std::optional> default_endpt_changed_cb; + REFERENCE_TIME default_latency_ms; util::buffer_t sample_buf; @@ -480,20 +589,6 @@ namespace platf::audio { sink_t sink; - audio::device_enum_t device_enum; - auto status = CoCreateInstance( - CLSID_MMDeviceEnumerator, - nullptr, - CLSCTX_ALL, - IID_IMMDeviceEnumerator, - (void **) &device_enum); - - if (FAILED(status)) { - BOOST_LOG(error) << "Couldn't create Device Enumerator: [0x"sv << util::hex(status).to_string_view() << ']'; - - return std::nullopt; - } - auto device = default_device(device_enum); if (!device) { return std::nullopt; @@ -505,7 +600,7 @@ namespace platf::audio { sink.host = converter.to_bytes(wstring.get()); collection_t collection; - status = device_enum->EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE, &collection); + auto status = device_enum->EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE, &collection); if (FAILED(status)) { BOOST_LOG(error) << "Couldn't enumerate: [0x"sv << util::hex(status).to_string_view() << ']'; @@ -515,7 +610,10 @@ namespace platf::audio { UINT count; collection->GetCount(&count); - std::string virtual_device_id = config::audio.virtual_sink; + // If the sink isn't a device name, we'll assume it's a device ID + auto virtual_device_id = find_device_id_by_name(config::audio.virtual_sink).value_or(converter.from_bytes(config::audio.virtual_sink)); + auto virtual_device_found = false; + for (auto x = 0; x < count; ++x) { audio::device_t device; collection->Item(x, &device); @@ -526,6 +624,7 @@ namespace platf::audio { audio::wstring_t wstring; device->GetId(&wstring); + std::wstring device_id { wstring.get() }; audio::prop_t prop; device->OpenPropertyStore(STGM_READ, &prop); @@ -548,45 +647,42 @@ namespace platf::audio { << std::endl; if (virtual_device_id.empty() && adapter_name == virtual_adapter_name) { - virtual_device_id = converter.to_bytes(wstring.get()); + virtual_device_id = std::move(device_id); + virtual_device_found = true; + break; + } + else if (virtual_device_id == device_id) { + virtual_device_found = true; + break; } } - if (!virtual_device_id.empty()) { + if (virtual_device_found) { + auto name_suffix = converter.to_bytes(virtual_device_id); sink.null = std::make_optional(sink_t::null_t { - "virtual-"s.append(formats[format_t::stereo - 1].name) + virtual_device_id, - "virtual-"s.append(formats[format_t::surr51 - 1].name) + virtual_device_id, - "virtual-"s.append(formats[format_t::surr71 - 1].name) + virtual_device_id, + "virtual-"s.append(formats[format_t::stereo - 1].name) + name_suffix, + "virtual-"s.append(formats[format_t::surr51 - 1].name) + name_suffix, + "virtual-"s.append(formats[format_t::surr71 - 1].name) + name_suffix, }); } - - return sink; - } - - std::unique_ptr - microphone(const std::uint8_t *mapping, int channels, std::uint32_t sample_rate, std::uint32_t frame_size) override { - auto mic = std::make_unique(); - - if (mic->init(sample_rate, frame_size, channels)) { - return nullptr; + else if (!virtual_device_id.empty()) { + BOOST_LOG(warning) << "Unable to find the specified virtual sink: "sv << virtual_device_id; } - return mic; + return sink; } /** - * If the requested sink is a virtual sink, meaning no speakers attached to - * the host, then we can seamlessly set the format to stereo and surround sound. - * - * Any virtual sink detected will be prefixed by: - * virtual-(format name) - * If it doesn't contain that prefix, then the format will not be changed - */ - std::optional - set_format(const std::string &sink) { + * @brief Gets information encoded in the raw sink name + * + * @param sink The raw sink name + * + * @return A pair of type and the real sink name + */ + std::pair + get_sink_info(const std::string &sink) { std::string_view sv { sink.c_str(), sink.size() }; - format_t::type_e type = format_t::none; // sink format: // [virtual-(format name)]device_id auto prefix = "virtual-"sv; @@ -596,17 +692,50 @@ namespace platf::audio { for (auto &format : formats) { auto &name = format.name; if (sv.find(name) == 0) { - type = format.type; - sv = sv.substr(name.size(), sv.size() - name.size()); - - break; + return std::make_pair(format.type, sv.substr(name.size(), sv.size() - name.size())); } } } - auto wstring_device_id = converter.from_bytes(sv.data()); + return std::make_pair(format_t::none, sv); + } + + std::unique_ptr + microphone(const std::uint8_t *mapping, int channels, std::uint32_t sample_rate, std::uint32_t frame_size) override { + auto mic = std::make_unique(); + + if (mic->init(sample_rate, frame_size, channels)) { + return nullptr; + } + + // If this is a virtual sink, set a callback that will change the sink back if it's changed + auto sink_info = get_sink_info(assigned_sink); + if (sink_info.first != format_t::none) { + mic->default_endpt_changed_cb = [this] { + BOOST_LOG(info) << "Resetting sink to ["sv << assigned_sink << "] after default changed"; + set_sink(assigned_sink); + }; + } + + return mic; + } - if (type == format_t::none) { + /** + * If the requested sink is a virtual sink, meaning no speakers attached to + * the host, then we can seamlessly set the format to stereo and surround sound. + * + * Any virtual sink detected will be prefixed by: + * virtual-(format name) + * If it doesn't contain that prefix, then the format will not be changed + */ + std::optional + set_format(const std::string &sink) { + auto sink_info = get_sink_info(sink); + + // If the sink isn't a device name, we'll assume it's a device ID + auto wstring_device_id = find_device_id_by_name(sink).value_or(converter.from_bytes(sink_info.second.data())); + + if (sink_info.first == format_t::none) { // wstring_device_id does not contain virtual-(format name) // It's a simple deviceId, just pass it back return std::make_optional(std::move(wstring_device_id)); @@ -620,14 +749,14 @@ namespace platf::audio { return std::nullopt; } - set_wave_format(wave_format, formats[(int) type - 1]); + set_wave_format(wave_format, formats[(int) sink_info.first - 1]); WAVEFORMATEXTENSIBLE p {}; status = policy->SetDeviceFormat(wstring_device_id.c_str(), wave_format.get(), (WAVEFORMATEX *) &p); // Surround 5.1 might contain side-{left, right} instead of speaker in the back // Try again with different speaker mask. - if (status == 0x88890008 && type == format_t::surr51) { + if (status == 0x88890008 && sink_info.first == format_t::surr51) { set_wave_format(wave_format, surround_51_side_speakers); status = policy->SetDeviceFormat(wstring_device_id.c_str(), wave_format.get(), (WAVEFORMATEX *) &p); } @@ -651,15 +780,221 @@ namespace platf::audio { for (int x = 0; x < (int) ERole_enum_count; ++x) { auto status = policy->SetDefaultEndpoint(wstring_device_id->c_str(), (ERole) x); if (status) { - BOOST_LOG(warning) << "Couldn't set ["sv << sink << "] to role ["sv << x << ']'; + // Depending on the format of the string, we could get either of these errors + if (status == HRESULT_FROM_WIN32(ERROR_NOT_FOUND) || status == E_INVALIDARG) { + BOOST_LOG(warning) << "Audio sink not found: "sv << sink; + } + else { + BOOST_LOG(warning) << "Couldn't set ["sv << sink << "] to role ["sv << x << "]: 0x"sv << util::hex(status).to_string_view(); + } ++failure; } } + // Remember the assigned sink name, so we have it for later if we need to set it + // back after another application changes it + if (!failure) { + assigned_sink = sink; + } + return failure; } + /** + * @brief Find the audio device ID given a user-specified name. + * @param name The name provided by the user. + * @return The matching device ID, or nothing if not found. + */ + std::optional + find_device_id_by_name(const std::string &name) { + if (name.empty()) { + return std::nullopt; + } + + collection_t collection; + auto status = device_enum->EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE, &collection); + if (FAILED(status)) { + BOOST_LOG(error) << "Couldn't enumerate: [0x"sv << util::hex(status).to_string_view() << ']'; + + return std::nullopt; + } + + UINT count; + collection->GetCount(&count); + + auto wstring_name = converter.from_bytes(name.data()); + + for (auto x = 0; x < count; ++x) { + audio::device_t device; + collection->Item(x, &device); + + if (!validate_device(device)) { + continue; + } + + audio::wstring_t wstring_id; + device->GetId(&wstring_id); + + audio::prop_t prop; + device->OpenPropertyStore(STGM_READ, &prop); + + prop_var_t adapter_friendly_name; + prop_var_t device_friendly_name; + prop_var_t device_desc; + + prop->GetValue(PKEY_Device_FriendlyName, &device_friendly_name.prop); + prop->GetValue(PKEY_DeviceInterface_FriendlyName, &adapter_friendly_name.prop); + prop->GetValue(PKEY_Device_DeviceDesc, &device_desc.prop); + + auto adapter_name = no_null((LPWSTR) adapter_friendly_name.prop.pszVal); + auto device_name = no_null((LPWSTR) device_friendly_name.prop.pszVal); + auto device_description = no_null((LPWSTR) device_desc.prop.pszVal); + + // Match the user-specified name against any of the user-visible strings + if (std::wcscmp(wstring_name.c_str(), adapter_name) == 0 || + std::wcscmp(wstring_name.c_str(), device_name) == 0 || + std::wcscmp(wstring_name.c_str(), device_description) == 0) { + return std::make_optional(std::wstring { wstring_id.get() }); + } + } + + return std::nullopt; + } + + /** + * @brief Resets the default audio device from Steam Streaming Speakers. + */ + void + reset_default_device() { + auto steam_device_id = find_device_id_by_name("Steam Streaming Speakers"s); + if (!steam_device_id) { + return; + } + + { + // Get the current default audio device (if present) + auto current_default_dev = default_device(device_enum); + if (!current_default_dev) { + return; + } + + audio::wstring_t current_default_id; + current_default_dev->GetId(¤t_default_id); + + // If Steam Streaming Speakers are already not default, we're done. + if (*steam_device_id != current_default_id.get()) { + return; + } + } + + // Disable the Steam Streaming Speakers temporarily to allow the OS to pick a new default. + auto hr = policy->SetEndpointVisibility(steam_device_id->c_str(), FALSE); + if (FAILED(hr)) { + BOOST_LOG(warning) << "Failed to disable Steam audio device: "sv << util::hex(hr).to_string_view(); + return; + } + + // Get the newly selected default audio device + auto new_default_dev = default_device(device_enum); + + // Enable the Steam Streaming Speakers again + hr = policy->SetEndpointVisibility(steam_device_id->c_str(), TRUE); + if (FAILED(hr)) { + BOOST_LOG(warning) << "Failed to enable Steam audio device: "sv << util::hex(hr).to_string_view(); + return; + } + + // If there's now no audio device, the Steam Streaming Speakers were the only device available. + // There's no other device to set as the default, so just return. + if (!new_default_dev) { + return; + } + + audio::wstring_t new_default_id; + new_default_dev->GetId(&new_default_id); + + // Set the new default audio device + for (int x = 0; x < (int) ERole_enum_count; ++x) { + policy->SetDefaultEndpoint(new_default_id.get(), (ERole) x); + } + + BOOST_LOG(info) << "Successfully reset default audio device"sv; + } + + /** + * @brief Installs the Steam Streaming Speakers driver, if present. + * @return `true` if installation was successful. + */ + bool + install_steam_audio_drivers() { +#ifdef STEAM_DRIVER_SUBDIR + // MinGW's libnewdev.a is missing DiInstallDriverW() even though the headers have it, + // so we have to load it at runtime. It's Vista or later, so it will always be available. + auto newdev = LoadLibraryExW(L"newdev.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32); + if (!newdev) { + BOOST_LOG(error) << "newdev.dll failed to load"sv; + return false; + } + auto fg = util::fail_guard([newdev]() { + FreeLibrary(newdev); + }); + + auto fn_DiInstallDriverW = (decltype(DiInstallDriverW) *) GetProcAddress(newdev, "DiInstallDriverW"); + if (!fn_DiInstallDriverW) { + BOOST_LOG(error) << "DiInstallDriverW() is missing"sv; + return false; + } + + // Get the current default audio device (if present) + auto old_default_dev = default_device(device_enum); + + // Install the Steam Streaming Speakers driver + WCHAR driver_path[MAX_PATH] = {}; + ExpandEnvironmentStringsW(STEAM_AUDIO_DRIVER_PATH, driver_path, ARRAYSIZE(driver_path)); + if (fn_DiInstallDriverW(nullptr, driver_path, 0, nullptr)) { + BOOST_LOG(info) << "Successfully installed Steam Streaming Speakers"sv; + + // Wait for 5 seconds to allow the audio subsystem to reconfigure things before + // modifying the default audio device or enumerating devices again. + Sleep(5000); + + // If there was a previous default device, restore that original device as the + // default output device just in case installing the new one changed it. + if (old_default_dev) { + audio::wstring_t old_default_id; + old_default_dev->GetId(&old_default_id); + + for (int x = 0; x < (int) ERole_enum_count; ++x) { + policy->SetDefaultEndpoint(old_default_id.get(), (ERole) x); + } + } + + return true; + } + else { + auto err = GetLastError(); + switch (err) { + case ERROR_ACCESS_DENIED: + BOOST_LOG(warning) << "Administrator privileges are required to install Steam Streaming Speakers"sv; + break; + case ERROR_FILE_NOT_FOUND: + case ERROR_PATH_NOT_FOUND: + BOOST_LOG(info) << "Steam audio drivers not found. This is expected if you don't have Steam installed."sv; + break; + default: + BOOST_LOG(warning) << "Failed to install Steam audio drivers: "sv << err; + break; + } + + return false; + } +#else + BOOST_LOG(warning) << "Unable to install Steam Streaming Speakers on unknown architecture"sv; + return false; +#endif + } + int init() { auto status = CoCreateInstance( @@ -675,12 +1010,26 @@ namespace platf::audio { return -1; } + status = CoCreateInstance( + CLSID_MMDeviceEnumerator, + nullptr, + CLSCTX_ALL, + IID_IMMDeviceEnumerator, + (void **) &device_enum); + + if (FAILED(status)) { + BOOST_LOG(error) << "Couldn't create Device Enumerator: [0x"sv << util::hex(status).to_string_view() << ']'; + return -1; + } + return 0; } ~audio_control_t() override {} policy_t policy; + audio::device_enum_t device_enum; + std::string assigned_sink; }; } // namespace platf::audio @@ -700,6 +1049,13 @@ namespace platf { return nullptr; } + // Install Steam Streaming Speakers if needed. We do this during audio_control() to ensure + // the sink information returned includes the new Steam Streaming Speakers device. + if (config::audio.install_steam_drivers && !control->find_device_id_by_name("Steam Streaming Speakers"s)) { + // This is best effort. Don't fail if it doesn't work. + control->install_steam_audio_drivers(); + } + return control; } @@ -708,6 +1064,17 @@ namespace platf { if (dxgi::init()) { return nullptr; } - return std::make_unique(); + + // Initialize COM + auto co_init = std::make_unique(); + + // If Steam Streaming Speakers are currently the default audio device, + // change the default to something else (if another device is available). + audio::audio_control_t audio_ctrl; + if (audio_ctrl.init() == 0) { + audio_ctrl.reset_default_device(); + } + + return co_init; } } // namespace platf diff --git a/src/platform/windows/display.h b/src/platform/windows/display.h index ba6aac38432..2496cd3f55e 100644 --- a/src/platform/windows/display.h +++ b/src/platform/windows/display.h @@ -1,9 +1,8 @@ -// -// Created by loki on 4/23/20. -// - -#ifndef SUNSHINE_DISPLAY_H -#define SUNSHINE_DISPLAY_H +/** + * @file src/platform/windows/display.h + * @brief todo + */ +#pragma once #include #include @@ -110,6 +109,7 @@ namespace platf::dxgi { dup_t dup; bool has_frame {}; bool use_dwmflush {}; + std::chrono::steady_clock::time_point last_protected_content_warning_time {}; capture_e next_frame(DXGI_OUTDUPL_FRAME_INFO &frame_info, std::chrono::milliseconds timeout, resource_t::pointer *res_p); @@ -125,8 +125,9 @@ namespace platf::dxgi { public: int init(const ::video::config_t &config, const std::string &display_name); + capture_e - capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr img, bool *cursor) override; + capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override; std::chrono::nanoseconds delay; @@ -168,19 +169,17 @@ namespace platf::dxgi { colorspace_to_string(DXGI_COLOR_SPACE_TYPE type); virtual capture_e - snapshot(img_t *img, std::chrono::milliseconds timeout, bool cursor_visible) = 0; + snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor_visible) = 0; virtual int complete_img(img_t *img, bool dummy) = 0; virtual std::vector - get_supported_sdr_capture_formats() = 0; - virtual std::vector - get_supported_hdr_capture_formats() = 0; + get_supported_capture_formats() = 0; }; class display_ram_t: public display_base_t { public: virtual capture_e - snapshot(img_t *img, std::chrono::milliseconds timeout, bool cursor_visible) override; + snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor_visible) override; std::shared_ptr alloc_img() override; @@ -189,9 +188,7 @@ namespace platf::dxgi { int complete_img(img_t *img, bool dummy) override; std::vector - get_supported_sdr_capture_formats() override; - std::vector - get_supported_hdr_capture_formats() override; + get_supported_capture_formats() override; int init(const ::video::config_t &config, const std::string &display_name); @@ -204,7 +201,7 @@ namespace platf::dxgi { class display_vram_t: public display_base_t, public std::enable_shared_from_this { public: virtual capture_e - snapshot(img_t *img, std::chrono::milliseconds timeout, bool cursor_visible) override; + snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor_visible) override; std::shared_ptr alloc_img() override; @@ -213,9 +210,7 @@ namespace platf::dxgi { int complete_img(img_t *img_base, bool dummy) override; std::vector - get_supported_sdr_capture_formats() override; - std::vector - get_supported_hdr_capture_formats() override; + get_supported_capture_formats() override; int init(const ::video::config_t &config, const std::string &display_name); @@ -235,10 +230,10 @@ namespace platf::dxgi { gpu_cursor_t cursor_alpha; gpu_cursor_t cursor_xor; - texture2d_t last_frame_copy; + texture2d_t old_surface_delayed_destruction; + std::chrono::steady_clock::time_point old_surface_timestamp; + std::variant> last_frame_variant; std::atomic next_image_id; }; } // namespace platf::dxgi - -#endif diff --git a/src/platform/windows/display_base.cpp b/src/platform/windows/display_base.cpp index f6c3bfb351f..e4483ae77d1 100644 --- a/src/platform/windows/display_base.cpp +++ b/src/platform/windows/display_base.cpp @@ -1,7 +1,7 @@ -// -// Created by loki on 1/12/20. -// - +/** + * @file src/platform/windows/display_base.cpp + * @brief todo + */ #include #include #include @@ -40,6 +40,14 @@ namespace platf::dxgi { switch (status) { case S_OK: + // ProtectedContentMaskedOut seems to semi-randomly be TRUE or FALSE even when protected content + // is on screen the whole time, so we can't just print when it changes. Instead we'll keep track + // of the last time we printed the warning and print another if we haven't printed one recently. + if (frame_info.ProtectedContentMaskedOut && std::chrono::steady_clock::now() > last_protected_content_warning_time + 10s) { + BOOST_LOG(warning) << "Windows is currently blocking DRM-protected content from capture. You may see black regions where this content would be."sv; + last_protected_content_warning_time = std::chrono::steady_clock::now(); + } + has_frame = true; return capture_e::ok; case DXGI_ERROR_WAIT_TIMEOUT: @@ -92,7 +100,7 @@ namespace platf::dxgi { } capture_e - display_base_t::capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr<::platf::img_t> img, bool *cursor) { + display_base_t::capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) { auto next_frame = std::chrono::steady_clock::now(); // Use CREATE_WAITABLE_TIMER_HIGH_RESOLUTION if supported (Windows 10 1809+) @@ -110,7 +118,16 @@ namespace platf::dxgi { CloseHandle(timer); }); - while (img) { + // Keep the display awake during capture. If the display goes to sleep during + // capture, best case is that capture stops until it powers back on. However, + // worst case it will trigger us to reinit DD, waking the display back up in + // a neverending cycle of waking and sleeping the display of an idle machine. + SetThreadExecutionState(ES_CONTINUOUS | ES_DISPLAY_REQUIRED); + auto clear_display_required = util::fail_guard([]() { + SetThreadExecutionState(ES_CONTINUOUS); + }); + + while (true) { // This will return false if the HDR state changes or for any number of other // display or GPU changes. We should reinit to examine the updated state of // the display subsystem. It is recommended to call this once per frame. @@ -135,16 +152,22 @@ namespace platf::dxgi { next_frame = std::chrono::steady_clock::now() + delay; } - auto status = snapshot(img.get(), 1000ms, *cursor); + std::shared_ptr img_out; + auto status = snapshot(pull_free_image_cb, img_out, 1000ms, *cursor); switch (status) { case platf::capture_e::reinit: case platf::capture_e::error: + case platf::capture_e::interrupted: return status; case platf::capture_e::timeout: - img = snapshot_cb(img, false); + if (!push_captured_image_cb(std::move(img_out), false)) { + return capture_e::ok; + } break; case platf::capture_e::ok: - img = snapshot_cb(img, true); + if (!push_captured_image_cb(std::move(img_out), true)) { + return capture_e::ok; + } break; default: BOOST_LOG(error) << "Unrecognized capture status ["sv << (int) status << ']'; @@ -336,46 +359,58 @@ namespace platf::dxgi { auto output_name = converter.from_bytes(display_name); adapter_t::pointer adapter_p; - for (int x = 0; factory->EnumAdapters1(x, &adapter_p) != DXGI_ERROR_NOT_FOUND; ++x) { - dxgi::adapter_t adapter_tmp { adapter_p }; + for (int tries = 0; tries < 2; ++tries) { + for (int x = 0; factory->EnumAdapters1(x, &adapter_p) != DXGI_ERROR_NOT_FOUND; ++x) { + dxgi::adapter_t adapter_tmp { adapter_p }; - DXGI_ADAPTER_DESC1 adapter_desc; - adapter_tmp->GetDesc1(&adapter_desc); + DXGI_ADAPTER_DESC1 adapter_desc; + adapter_tmp->GetDesc1(&adapter_desc); - if (!adapter_name.empty() && adapter_desc.Description != adapter_name) { - continue; - } + if (!adapter_name.empty() && adapter_desc.Description != adapter_name) { + continue; + } - dxgi::output_t::pointer output_p; - for (int y = 0; adapter_tmp->EnumOutputs(y, &output_p) != DXGI_ERROR_NOT_FOUND; ++y) { - dxgi::output_t output_tmp { output_p }; + dxgi::output_t::pointer output_p; + for (int y = 0; adapter_tmp->EnumOutputs(y, &output_p) != DXGI_ERROR_NOT_FOUND; ++y) { + dxgi::output_t output_tmp { output_p }; - DXGI_OUTPUT_DESC desc; - output_tmp->GetDesc(&desc); + DXGI_OUTPUT_DESC desc; + output_tmp->GetDesc(&desc); - if (!output_name.empty() && desc.DeviceName != output_name) { - continue; - } + if (!output_name.empty() && desc.DeviceName != output_name) { + continue; + } - if (desc.AttachedToDesktop && test_dxgi_duplication(adapter_tmp, output_tmp)) { - output = std::move(output_tmp); + if (desc.AttachedToDesktop && test_dxgi_duplication(adapter_tmp, output_tmp)) { + output = std::move(output_tmp); - offset_x = desc.DesktopCoordinates.left; - offset_y = desc.DesktopCoordinates.top; - width = desc.DesktopCoordinates.right - offset_x; - height = desc.DesktopCoordinates.bottom - offset_y; + offset_x = desc.DesktopCoordinates.left; + offset_y = desc.DesktopCoordinates.top; + width = desc.DesktopCoordinates.right - offset_x; + height = desc.DesktopCoordinates.bottom - offset_y; - // left and bottom may be negative, yet absolute mouse coordinates start at 0x0 - // Ensure offset starts at 0x0 - offset_x -= GetSystemMetrics(SM_XVIRTUALSCREEN); - offset_y -= GetSystemMetrics(SM_YVIRTUALSCREEN); + // left and bottom may be negative, yet absolute mouse coordinates start at 0x0 + // Ensure offset starts at 0x0 + offset_x -= GetSystemMetrics(SM_XVIRTUALSCREEN); + offset_y -= GetSystemMetrics(SM_YVIRTUALSCREEN); + } + } + + if (output) { + adapter = std::move(adapter_tmp); + break; } } if (output) { - adapter = std::move(adapter_tmp); break; } + + // If we made it here without finding an output, try to power on the display and retry. + if (tries == 0) { + SetThreadExecutionState(ES_DISPLAY_REQUIRED); + Sleep(500); + } } if (!output) { @@ -510,14 +545,14 @@ namespace platf::dxgi { } } - //FIXME: Duplicate output on RX580 in combination with DOOM (2016) --> BSOD + // FIXME: Duplicate output on RX580 in combination with DOOM (2016) --> BSOD { // IDXGIOutput5 is optional, but can provide improved performance and wide color support dxgi::output5_t output5 {}; status = output->QueryInterface(IID_IDXGIOutput5, (void **) &output5); if (SUCCEEDED(status)) { // Ask the display implementation which formats it supports - auto supported_formats = config.dynamicRange ? get_supported_hdr_capture_formats() : get_supported_sdr_capture_formats(); + auto supported_formats = get_supported_capture_formats(); if (supported_formats.empty()) { BOOST_LOG(warning) << "No compatible capture formats for this encoder"sv; return -1; diff --git a/src/platform/windows/display_ram.cpp b/src/platform/windows/display_ram.cpp index 581d925c51b..631abce7be8 100644 --- a/src/platform/windows/display_ram.cpp +++ b/src/platform/windows/display_ram.cpp @@ -1,4 +1,10 @@ +/** + * @file src/platform/windows/display_ram.cpp + * @brief todo + */ #include "display.h" + +#include "misc.h" #include "src/main.h" namespace platf { @@ -81,7 +87,7 @@ namespace platf::dxgi { auto colors_out = (std::uint8_t *) &cursor_pixel; auto colors_in = (std::uint8_t *) img_pixel_p; - //TODO: When use of IDXGIOutput5 is implemented, support different color formats + // TODO: When use of IDXGIOutput5 is implemented, support different color formats auto alpha = colors_out[3]; if (alpha == 255) { *img_pixel_p = cursor_pixel; @@ -95,7 +101,7 @@ namespace platf::dxgi { void apply_color_masked(int *img_pixel_p, int cursor_pixel) { - //TODO: When use of IDXGIOutput5 is implemented, support different color formats + // TODO: When use of IDXGIOutput5 is implemented, support different color formats auto alpha = ((std::uint8_t *) &cursor_pixel)[3]; if (alpha == 0xFF) { *img_pixel_p ^= cursor_pixel; @@ -171,9 +177,7 @@ namespace platf::dxgi { } capture_e - display_ram_t::snapshot(::platf::img_t *img_base, std::chrono::milliseconds timeout, bool cursor_visible) { - auto img = (img_t *) img_base; - + display_ram_t::snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor_visible) { HRESULT status; DXGI_OUTDUPL_FRAME_INFO frame_info; @@ -194,6 +198,12 @@ namespace platf::dxgi { return capture_e::timeout; } + std::optional frame_timestamp; + if (auto qpc_displayed = std::max(frame_info.LastPresentTime.QuadPart, frame_info.LastMouseUpdateTime.QuadPart)) { + // Translate QueryPerformanceCounter() value to steady_clock time point + frame_timestamp = std::chrono::steady_clock::now() - qpc_time_difference(qpc_counter(), qpc_displayed); + } + if (frame_info.PointerShapeBufferSize > 0) { auto &img_data = cursor.img_data; @@ -264,11 +274,16 @@ namespace platf::dxgi { return capture_e::reinit; } - //Copy from GPU to CPU + // Copy from GPU to CPU device_ctx->CopyResource(texture.get(), src.get()); } } + if (!pull_free_image_cb(img_out)) { + return capture_e::interrupted; + } + auto img = (img_t *) img_out.get(); + // If we don't know the final capture format yet, encode a dummy image if (capture_format == DXGI_FORMAT_UNKNOWN) { BOOST_LOG(debug) << "Capture format is still unknown. Encoding a blank image"sv; @@ -304,6 +319,10 @@ namespace platf::dxgi { blend_cursor(cursor, *img); } + if (img) { + img->frame_timestamp = frame_timestamp; + } + return capture_e::ok; } @@ -358,14 +377,8 @@ namespace platf::dxgi { } std::vector - display_ram_t::get_supported_sdr_capture_formats() { - return { DXGI_FORMAT_B8G8R8A8_UNORM }; - } - - std::vector - display_ram_t::get_supported_hdr_capture_formats() { - // HDR is unsupported - return {}; + display_ram_t::get_supported_capture_formats() { + return { DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_B8G8R8X8_UNORM }; } int diff --git a/src/platform/windows/display_vram.cpp b/src/platform/windows/display_vram.cpp index b6d51335e1e..376a58521da 100644 --- a/src/platform/windows/display_vram.cpp +++ b/src/platform/windows/display_vram.cpp @@ -1,3 +1,7 @@ +/** + * @file src/platform/windows/display_vram.cpp + * @brief todo + */ #include #include @@ -11,6 +15,7 @@ extern "C" { } #include "display.h" +#include "misc.h" #include "src/main.h" #include "src/video.h" @@ -91,16 +96,16 @@ namespace platf::dxgi { blob_t convert_UV_vs_hlsl; blob_t convert_UV_ps_hlsl; + blob_t convert_UV_linear_ps_hlsl; blob_t convert_UV_PQ_ps_hlsl; blob_t scene_vs_hlsl; blob_t convert_Y_ps_hlsl; + blob_t convert_Y_linear_ps_hlsl; blob_t convert_Y_PQ_ps_hlsl; blob_t scene_ps_hlsl; blob_t scene_NW_ps_hlsl; struct img_d3d_t: public platf::img_t { - std::shared_ptr display; - // These objects are owned by the display_t's ID3D11Device texture2d_t capture_texture; render_target_t capture_rt; @@ -116,6 +121,9 @@ namespace platf::dxgi { // Unique identifier for this image uint32_t id = 0; + // DXGI format of this image texture + DXGI_FORMAT format; + virtual ~img_d3d_t() override { if (encoder_texture_handle) { CloseHandle(encoder_texture_handle); @@ -123,6 +131,52 @@ namespace platf::dxgi { }; }; + struct texture_lock_helper { + keyed_mutex_t _mutex; + bool _locked = false; + + texture_lock_helper(const texture_lock_helper &) = delete; + texture_lock_helper & + operator=(const texture_lock_helper &) = delete; + + texture_lock_helper(texture_lock_helper &&other) { + _mutex.reset(other._mutex.release()); + _locked = other._locked; + other._locked = false; + } + + texture_lock_helper & + operator=(texture_lock_helper &&other) { + if (_locked) _mutex->ReleaseSync(0); + _mutex.reset(other._mutex.release()); + _locked = other._locked; + other._locked = false; + return *this; + } + + texture_lock_helper(IDXGIKeyedMutex *mutex): + _mutex(mutex) { + if (_mutex) _mutex->AddRef(); + } + + ~texture_lock_helper() { + if (_locked) _mutex->ReleaseSync(0); + } + + bool + lock() { + if (_locked) return true; + HRESULT status = _mutex->AcquireSync(0, INFINITE); + if (status == S_OK) { + _locked = true; + } + else { + BOOST_LOG(error) << "Failed to acquire texture mutex [0x"sv << util::hex(status).to_string_view() << ']'; + } + return _locked; + } + }; + util::buffer_t make_cursor_xor_image(const util::buffer_t &img_data, DXGI_OUTDUPL_POINTER_SHAPE_INFO shape_info) { constexpr std::uint32_t inverted = 0xFFFFFFFF; @@ -311,6 +365,16 @@ namespace platf::dxgi { public: int convert(platf::img_t &img_base) override { + // Garbage collect mapped capture images whose weak references have expired + for (auto it = img_ctx_map.begin(); it != img_ctx_map.end();) { + if (it->second.img_weak.expired()) { + it = img_ctx_map.erase(it); + } + else { + it++; + } + } + auto &img = (img_d3d_t &) img_base; auto &img_ctx = img_ctx_map[img.id]; @@ -328,24 +392,23 @@ namespace platf::dxgi { device_ctx->OMSetRenderTargets(1, &nv12_Y_rt, nullptr); device_ctx->VSSetShader(scene_vs.get(), nullptr, 0); - device_ctx->PSSetShader(convert_Y_ps.get(), nullptr, 0); + device_ctx->PSSetShader(img.format == DXGI_FORMAT_R16G16B16A16_FLOAT ? convert_Y_fp16_ps.get() : convert_Y_ps.get(), nullptr, 0); device_ctx->RSSetViewports(1, &outY_view); device_ctx->PSSetShaderResources(0, 1, &img_ctx.encoder_input_res); device_ctx->Draw(3, 0); - // Artifacts start appearing on the rendered image if Sunshine doesn't flush - // before rendering on the UV part of the image. - device_ctx->Flush(); - device_ctx->OMSetRenderTargets(1, &nv12_UV_rt, nullptr); device_ctx->VSSetShader(convert_UV_vs.get(), nullptr, 0); - device_ctx->PSSetShader(convert_UV_ps.get(), nullptr, 0); + device_ctx->PSSetShader(img.format == DXGI_FORMAT_R16G16B16A16_FLOAT ? convert_UV_fp16_ps.get() : convert_UV_ps.get(), nullptr, 0); device_ctx->RSSetViewports(1, &outUV_view); device_ctx->Draw(3, 0); // Release encoder mutex to allow capture code to reuse this image img_ctx.encoder_mutex->ReleaseSync(0); + ID3D11ShaderResourceView *emptyShaderResourceView = nullptr; + device_ctx->PSSetShaderResources(0, 1, &emptyShaderResourceView); + return 0; } @@ -474,7 +537,7 @@ namespace platf::dxgi { frame_texture->AddRef(); hwframe_texture.reset(frame_texture); - float info_in[16 / sizeof(float)] { 1.0f / (float) out_width_f }; //aligned to 16-byte + float info_in[16 / sizeof(float)] { 1.0f / (float) out_width_f }; // aligned to 16-byte info_scene = make_buffer(device.get(), info_in); if (!info_scene) { @@ -568,34 +631,48 @@ namespace platf::dxgi { } // If the display is in HDR and we're streaming HDR, we'll be converting scRGB to SMPTE 2084 PQ. - // NB: We can consume scRGB in SDR with our regular shaders because it behaves like UNORM input. if (format == DXGI_FORMAT_P010 && display->is_hdr()) { - status = device->CreatePixelShader(convert_Y_PQ_ps_hlsl->GetBufferPointer(), convert_Y_PQ_ps_hlsl->GetBufferSize(), nullptr, &convert_Y_ps); + status = device->CreatePixelShader(convert_Y_PQ_ps_hlsl->GetBufferPointer(), convert_Y_PQ_ps_hlsl->GetBufferSize(), nullptr, &convert_Y_fp16_ps); if (status) { BOOST_LOG(error) << "Failed to create convertY pixel shader [0x"sv << util::hex(status).to_string_view() << ']'; return -1; } - status = device->CreatePixelShader(convert_UV_PQ_ps_hlsl->GetBufferPointer(), convert_UV_PQ_ps_hlsl->GetBufferSize(), nullptr, &convert_UV_ps); + status = device->CreatePixelShader(convert_UV_PQ_ps_hlsl->GetBufferPointer(), convert_UV_PQ_ps_hlsl->GetBufferSize(), nullptr, &convert_UV_fp16_ps); if (status) { BOOST_LOG(error) << "Failed to create convertUV pixel shader [0x"sv << util::hex(status).to_string_view() << ']'; return -1; } } else { - status = device->CreatePixelShader(convert_Y_ps_hlsl->GetBufferPointer(), convert_Y_ps_hlsl->GetBufferSize(), nullptr, &convert_Y_ps); + // If the display is in Advanced Color mode, the desktop format will be scRGB FP16. + // scRGB uses linear gamma, so we must use our linear to sRGB conversion shaders. + status = device->CreatePixelShader(convert_Y_linear_ps_hlsl->GetBufferPointer(), convert_Y_linear_ps_hlsl->GetBufferSize(), nullptr, &convert_Y_fp16_ps); if (status) { BOOST_LOG(error) << "Failed to create convertY pixel shader [0x"sv << util::hex(status).to_string_view() << ']'; return -1; } - status = device->CreatePixelShader(convert_UV_ps_hlsl->GetBufferPointer(), convert_UV_ps_hlsl->GetBufferSize(), nullptr, &convert_UV_ps); + status = device->CreatePixelShader(convert_UV_linear_ps_hlsl->GetBufferPointer(), convert_UV_linear_ps_hlsl->GetBufferSize(), nullptr, &convert_UV_fp16_ps); if (status) { BOOST_LOG(error) << "Failed to create convertUV pixel shader [0x"sv << util::hex(status).to_string_view() << ']'; return -1; } } + // These shaders consume standard 8-bit sRGB input + status = device->CreatePixelShader(convert_Y_ps_hlsl->GetBufferPointer(), convert_Y_ps_hlsl->GetBufferSize(), nullptr, &convert_Y_ps); + if (status) { + BOOST_LOG(error) << "Failed to create convertY pixel shader [0x"sv << util::hex(status).to_string_view() << ']'; + return -1; + } + + status = device->CreatePixelShader(convert_UV_ps_hlsl->GetBufferPointer(), convert_UV_ps_hlsl->GetBufferSize(), nullptr, &convert_UV_ps); + if (status) { + BOOST_LOG(error) << "Failed to create convertUV pixel shader [0x"sv << util::hex(status).to_string_view() << ']'; + return -1; + } + color_matrix = make_buffer(device.get(), ::video::colors[0]); if (!color_matrix) { BOOST_LOG(error) << "Failed to create color matrix buffer"sv; @@ -648,18 +725,21 @@ namespace platf::dxgi { struct encoder_img_ctx_t { // Used to determine if the underlying texture changes. // Not safe for actual use by the encoder! - texture2d_t::pointer capture_texture_p; + texture2d_t::const_pointer capture_texture_p; texture2d_t encoder_texture; shader_res_t encoder_input_res; keyed_mutex_t encoder_mutex; + std::weak_ptr img_weak; + void reset() { capture_texture_p = nullptr; encoder_texture.reset(); encoder_input_res.reset(); encoder_mutex.reset(); + img_weak.reset(); } }; @@ -703,6 +783,9 @@ namespace platf::dxgi { } img_ctx.capture_texture_p = img.capture_texture.get(); + + img_ctx.img_weak = img.weak_from_this(); + return 0; } @@ -735,7 +818,9 @@ namespace platf::dxgi { vs_t convert_UV_vs; ps_t convert_UV_ps; + ps_t convert_UV_fp16_ps; ps_t convert_Y_ps; + ps_t convert_Y_fp16_ps; vs_t scene_vs; D3D11_VIEWPORT outY_view; @@ -793,9 +878,7 @@ namespace platf::dxgi { } capture_e - display_vram_t::snapshot(platf::img_t *img_base, std::chrono::milliseconds timeout, bool cursor_visible) { - auto img = (img_d3d_t *) img_base; - + display_vram_t::snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor_visible) { HRESULT status; DXGI_OUTDUPL_FRAME_INFO frame_info; @@ -816,6 +899,12 @@ namespace platf::dxgi { return capture_e::timeout; } + std::optional frame_timestamp; + if (auto qpc_displayed = std::max(frame_info.LastPresentTime.QuadPart, frame_info.LastMouseUpdateTime.QuadPart)) { + // Translate QueryPerformanceCounter() value to steady_clock time point + frame_timestamp = std::chrono::steady_clock::now() - qpc_time_difference(qpc_counter(), qpc_displayed); + } + if (frame_info.PointerShapeBufferSize > 0) { DXGI_OUTDUPL_POINTER_SHAPE_INFO shape_info {}; @@ -843,9 +932,10 @@ namespace platf::dxgi { cursor_xor.set_pos(frame_info.PointerPosition.Position.x, frame_info.PointerPosition.Position.y, frame_info.PointerPosition.Visible); } - if (frame_update_flag) { - texture2d_t src {}; + const bool blend_mouse_cursor_flag = (cursor_alpha.visible || cursor_xor.visible) && cursor_visible; + texture2d_t src {}; + if (frame_update_flag) { // Get the texture object from this frame status = res->QueryInterface(IID_ID3D11Texture2D, (void **) &src); if (FAILED(status)) { @@ -867,23 +957,6 @@ namespace platf::dxgi { if (capture_format == DXGI_FORMAT_UNKNOWN) { capture_format = desc.Format; BOOST_LOG(info) << "Capture format ["sv << dxgi_format_to_string(capture_format) << ']'; - - D3D11_TEXTURE2D_DESC t {}; - t.Width = width; - t.Height = height; - t.MipLevels = 1; - t.ArraySize = 1; - t.SampleDesc.Count = 1; - t.Usage = D3D11_USAGE_DEFAULT; - t.Format = capture_format; - t.BindFlags = 0; - - // Create a texture to store the most recent copy of the desktop - status = device->CreateTexture2D(&t, nullptr, &last_frame_copy); - if (FAILED(status)) { - BOOST_LOG(error) << "Failed to create frame copy texture [0x"sv << util::hex(status).to_string_view() << ']'; - return capture_e::error; - } } // It's also possible for the capture format to change on the fly. If that happens, @@ -892,67 +965,201 @@ namespace platf::dxgi { BOOST_LOG(info) << "Capture format changed ["sv << dxgi_format_to_string(capture_format) << " -> "sv << dxgi_format_to_string(desc.Format) << ']'; return capture_e::reinit; } + } - // Now that we know the capture format, we can finish creating the image - if (complete_img(img, false)) { - return capture_e::error; - } + enum class lfa { + nothing, + replace_surface_with_img, + replace_img_with_surface, + copy_src_to_img, + copy_src_to_surface, + }; - // Copy the texture to use for cursor-only updates - device_ctx->CopyResource(last_frame_copy.get(), src.get()); + enum class ofa { + forward_last_img, + copy_last_surface_and_blend_cursor, + dummy_fallback, + }; - // Copy into the capture texture on the image with the mutex held - status = img->capture_mutex->AcquireSync(0, INFINITE); - if (status != S_OK) { - BOOST_LOG(error) << "Failed to acquire capture mutex [0x"sv << util::hex(status).to_string_view() << ']'; - return capture_e::error; + auto last_frame_action = lfa::nothing; + auto out_frame_action = ofa::dummy_fallback; + + if (capture_format == DXGI_FORMAT_UNKNOWN) { + // We don't know the final capture format yet, so we will encode a black dummy image + last_frame_action = lfa::nothing; + out_frame_action = ofa::dummy_fallback; + } + else { + if (src) { + // We got a new frame from DesktopDuplication... + if (blend_mouse_cursor_flag) { + // ...and we need to blend the mouse cursor onto it. + // Copy the frame to intermediate surface so we can blend this and future mouse cursor updates + // without new frames from DesktopDuplication. We use direct3d surface directly here and not + // an image from pull_free_image_cb mainly because it's lighter (surface sharing between + // direct3d devices produce significant memory overhead). + last_frame_action = lfa::copy_src_to_surface; + // Copy the intermediate surface to a new image from pull_free_image_cb and blend the mouse cursor onto it. + out_frame_action = ofa::copy_last_surface_and_blend_cursor; + } + else { + // ...and we don't need to blend the mouse cursor. + // Copy the frame to a new image from pull_free_image_cb and save the shared pointer to the image + // in case the mouse cursor appears without a new frame from DesktopDuplication. + last_frame_action = lfa::copy_src_to_img; + // Use saved last image shared pointer as output image evading copy. + out_frame_action = ofa::forward_last_img; + } + } + else if (!std::holds_alternative(last_frame_variant)) { + // We didn't get a new frame from DesktopDuplication... + if (blend_mouse_cursor_flag) { + // ...but we need to blend the mouse cursor. + if (std::holds_alternative>(last_frame_variant)) { + // We have the shared pointer of the last image, replace it with intermediate surface + // while copying contents so we can blend this and future mouse cursor updates. + last_frame_action = lfa::replace_img_with_surface; + } + // Copy the intermediate surface which contains last DesktopDuplication frame + // to a new image from pull_free_image_cb and blend the mouse cursor onto it. + out_frame_action = ofa::copy_last_surface_and_blend_cursor; + } + else { + // ...and we don't need to blend the mouse cursor. + // This happens when the mouse cursor disappears from screen, + // or there's mouse cursor on screen, but its drawing is disabled in sunshine. + if (std::holds_alternative(last_frame_variant)) { + // We have the intermediate surface that was used as the mouse cursor blending base. + // Replace it with an image from pull_free_image_cb copying contents and freeing up the surface memory. + // Save the shared pointer to the image in case the mouse cursor reappears. + last_frame_action = lfa::replace_surface_with_img; + } + // Use saved last image shared pointer as output image evading copy. + out_frame_action = ofa::forward_last_img; + } } - device_ctx->CopyResource(img->capture_texture.get(), src.get()); } - else if (capture_format == DXGI_FORMAT_UNKNOWN) { - // We don't know the final capture format yet, so we will encode a dummy image - BOOST_LOG(debug) << "Capture format is still unknown. Encoding a blank image"sv; - // Finish creating the image as a dummy (if it hasn't happened already) - if (complete_img(img, true)) { - return capture_e::error; + auto create_surface = [&](texture2d_t &surface) -> bool { + // Try to reuse the old surface if it hasn't been destroyed yet. + if (old_surface_delayed_destruction) { + surface.reset(old_surface_delayed_destruction.release()); + return true; } - auto dummy_data = std::make_unique(img->row_pitch * img->height); - std::fill_n(dummy_data.get(), img->row_pitch * img->height, 0); + // Otherwise create a new surface. + D3D11_TEXTURE2D_DESC t {}; + t.Width = width; + t.Height = height; + t.MipLevels = 1; + t.ArraySize = 1; + t.SampleDesc.Count = 1; + t.Usage = D3D11_USAGE_DEFAULT; + t.Format = capture_format; + t.BindFlags = 0; + status = device->CreateTexture2D(&t, nullptr, &surface); + if (FAILED(status)) { + BOOST_LOG(error) << "Failed to create frame copy texture [0x"sv << util::hex(status).to_string_view() << ']'; + return false; + } - status = img->capture_mutex->AcquireSync(0, INFINITE); - if (status != S_OK) { - BOOST_LOG(error) << "Failed to acquire capture mutex [0x"sv << util::hex(status).to_string_view() << ']'; - return capture_e::error; + return true; + }; + + auto get_locked_d3d_img = [&](std::shared_ptr &img, bool dummy = false) -> std::tuple, texture_lock_helper> { + auto d3d_img = std::static_pointer_cast(img); + + // Finish creating the image (if it hasn't happened already), + // also creates synchonization primitives for shared access from multiple direct3d devices. + if (complete_img(d3d_img.get(), dummy)) return { nullptr, nullptr }; + + // This image is shared between capture direct3d device and encoders direct3d devices, + // we must acquire lock before doing anything to it. + texture_lock_helper lock_helper(d3d_img->capture_mutex.get()); + if (!lock_helper.lock()) { + BOOST_LOG(error) << "Failed to lock capture texture"; + return { nullptr, nullptr }; } - // Populate the image with dummy data. This is required because these images could be reused - // after rendering (in which case they would have a cursor already rendered into them). - device_ctx->UpdateSubresource(img->capture_texture.get(), 0, nullptr, dummy_data.get(), img->row_pitch, 0); - } - else { - // We must know the capture format in this path or we would have hit the above unknown format case - if (complete_img(img, false)) { - return capture_e::error; + return { std::move(d3d_img), std::move(lock_helper) }; + }; + + switch (last_frame_action) { + case lfa::nothing: { + break; } - // We have a previously captured frame to reuse. We can't just grab the src texture from - // the call to AcquireNextFrame() because that won't be valid. It seems to return a texture - // in the unmodified desktop format (rather than the formats we passed to DuplicateOutput1()) - // if called in that case. - status = img->capture_mutex->AcquireSync(0, INFINITE); - if (status != S_OK) { - BOOST_LOG(error) << "Failed to acquire capture mutex [0x"sv << util::hex(status).to_string_view() << ']'; - return capture_e::error; + case lfa::replace_surface_with_img: { + auto p_surface = std::get_if(&last_frame_variant); + if (!p_surface) { + BOOST_LOG(error) << "Logical error at " << __FILE__ << ":" << __LINE__; + return capture_e::error; + } + + std::shared_ptr img; + if (!pull_free_image_cb(img)) return capture_e::interrupted; + + auto [d3d_img, lock] = get_locked_d3d_img(img); + if (!d3d_img) return capture_e::error; + + device_ctx->CopyResource(d3d_img->capture_texture.get(), p_surface->get()); + + // We delay the destruction of intermediate surface in case the mouse cursor reappears shortly. + old_surface_delayed_destruction.reset(p_surface->release()); + old_surface_timestamp = std::chrono::steady_clock::now(); + + last_frame_variant = img; + break; + } + + case lfa::replace_img_with_surface: { + auto p_img = std::get_if>(&last_frame_variant); + if (!p_img) { + BOOST_LOG(error) << "Logical error at " << __FILE__ << ":" << __LINE__; + return capture_e::error; + } + auto [d3d_img, lock] = get_locked_d3d_img(*p_img); + if (!d3d_img) return capture_e::error; + + p_img = nullptr; + last_frame_variant = texture2d_t {}; + auto &surface = std::get(last_frame_variant); + if (!create_surface(surface)) return capture_e::error; + + device_ctx->CopyResource(surface.get(), d3d_img->capture_texture.get()); + break; + } + + case lfa::copy_src_to_img: { + last_frame_variant = {}; + + std::shared_ptr img; + if (!pull_free_image_cb(img)) return capture_e::interrupted; + + auto [d3d_img, lock] = get_locked_d3d_img(img); + if (!d3d_img) return capture_e::error; + + device_ctx->CopyResource(d3d_img->capture_texture.get(), src.get()); + last_frame_variant = img; + break; + } + + case lfa::copy_src_to_surface: { + auto p_surface = std::get_if(&last_frame_variant); + if (!p_surface) { + last_frame_variant = texture2d_t {}; + p_surface = std::get_if(&last_frame_variant); + if (!create_surface(*p_surface)) return capture_e::error; + } + device_ctx->CopyResource(p_surface->get(), src.get()); + break; } - device_ctx->CopyResource(img->capture_texture.get(), last_frame_copy.get()); } - if ((cursor_alpha.visible || cursor_xor.visible) && cursor_visible) { + auto blend_cursor = [&](img_d3d_t &d3d_img) { device_ctx->VSSetShader(scene_vs.get(), nullptr, 0); device_ctx->PSSetShader(scene_ps.get(), nullptr, 0); - device_ctx->OMSetRenderTargets(1, &img->capture_rt, nullptr); + device_ctx->OMSetRenderTargets(1, &d3d_img.capture_rt, nullptr); if (cursor_alpha.texture.get()) { // Perform an alpha blending operation @@ -973,10 +1180,79 @@ namespace platf::dxgi { } device_ctx->OMSetBlendState(blend_disable.get(), nullptr, 0xFFFFFFFFu); + + ID3D11RenderTargetView *emptyRenderTarget = nullptr; + device_ctx->OMSetRenderTargets(1, &emptyRenderTarget, nullptr); + device_ctx->RSSetViewports(0, nullptr); + ID3D11ShaderResourceView *emptyShaderResourceView = nullptr; + device_ctx->PSSetShaderResources(0, 1, &emptyShaderResourceView); + }; + + switch (out_frame_action) { + case ofa::forward_last_img: { + auto p_img = std::get_if>(&last_frame_variant); + if (!p_img) { + BOOST_LOG(error) << "Logical error at " << __FILE__ << ":" << __LINE__; + return capture_e::error; + } + img_out = *p_img; + break; + } + + case ofa::copy_last_surface_and_blend_cursor: { + auto p_surface = std::get_if(&last_frame_variant); + if (!p_surface) { + BOOST_LOG(error) << "Logical error at " << __FILE__ << ":" << __LINE__; + return capture_e::error; + } + if (!blend_mouse_cursor_flag) { + BOOST_LOG(error) << "Logical error at " << __FILE__ << ":" << __LINE__; + return capture_e::error; + } + + if (!pull_free_image_cb(img_out)) return capture_e::interrupted; + + auto [d3d_img, lock] = get_locked_d3d_img(img_out); + if (!d3d_img) return capture_e::error; + + device_ctx->CopyResource(d3d_img->capture_texture.get(), p_surface->get()); + blend_cursor(*d3d_img); + break; + } + + case ofa::dummy_fallback: { + if (!pull_free_image_cb(img_out)) return capture_e::interrupted; + + // Clear the image if it has been used as a dummy. + // It can have the mouse cursor blended onto it. + auto old_d3d_img = (img_d3d_t *) img_out.get(); + bool reclear_dummy = old_d3d_img->dummy && old_d3d_img->capture_texture; + + auto [d3d_img, lock] = get_locked_d3d_img(img_out, true); + if (!d3d_img) return capture_e::error; + + if (reclear_dummy) { + auto dummy_data = std::make_unique(d3d_img->row_pitch * d3d_img->height); + std::fill_n(dummy_data.get(), d3d_img->row_pitch * d3d_img->height, 0); + device_ctx->UpdateSubresource(d3d_img->capture_texture.get(), 0, nullptr, dummy_data.get(), d3d_img->row_pitch, 0); + } + + if (blend_mouse_cursor_flag) { + blend_cursor(*d3d_img); + } + + break; + } } - // Release the mutex to allow encoding of this frame - img->capture_mutex->ReleaseSync(0); + // Perform delayed destruction of the unused surface if the time is due. + if (old_surface_delayed_destruction && old_surface_timestamp + 10s < std::chrono::steady_clock::now()) { + old_surface_delayed_destruction.reset(); + } + + if (img_out) { + img_out->frame_timestamp = frame_timestamp; + } return capture_e::ok; } @@ -1058,7 +1334,6 @@ namespace platf::dxgi { // Initialize format-independent fields img->width = width; img->height = height; - img->display = shared_from_this(); img->id = next_image_id++; return img; @@ -1094,6 +1369,7 @@ namespace platf::dxgi { img->pixel_pitch = get_pixel_pitch(); img->row_pitch = img->pixel_pitch * img->width; img->dummy = dummy; + img->format = (capture_format == DXGI_FORMAT_UNKNOWN) ? DXGI_FORMAT_B8G8R8A8_UNORM : capture_format; D3D11_TEXTURE2D_DESC t {}; t.Width = img->width; @@ -1102,19 +1378,24 @@ namespace platf::dxgi { t.ArraySize = 1; t.SampleDesc.Count = 1; t.Usage = D3D11_USAGE_DEFAULT; - t.Format = (capture_format == DXGI_FORMAT_UNKNOWN) ? DXGI_FORMAT_B8G8R8A8_UNORM : capture_format; + t.Format = img->format; t.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET; t.MiscFlags = D3D11_RESOURCE_MISC_SHARED_NTHANDLE | D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX; - auto dummy_data = std::make_unique(img->row_pitch * img->height); - std::fill_n(dummy_data.get(), img->row_pitch * img->height, 0); - D3D11_SUBRESOURCE_DATA initial_data { - dummy_data.get(), - (UINT) img->row_pitch, - 0 - }; - - auto status = device->CreateTexture2D(&t, &initial_data, &img->capture_texture); + HRESULT status; + if (dummy) { + auto dummy_data = std::make_unique(img->row_pitch * img->height); + std::fill_n(dummy_data.get(), img->row_pitch * img->height, 0); + D3D11_SUBRESOURCE_DATA initial_data { + dummy_data.get(), + (UINT) img->row_pitch, + 0 + }; + status = device->CreateTexture2D(&t, &initial_data, &img->capture_texture); + } + else { + status = device->CreateTexture2D(&t, nullptr, &img->capture_texture); + } if (FAILED(status)) { BOOST_LOG(error) << "Failed to create img buf texture [0x"sv << util::hex(status).to_string_view() << ']'; return -1; @@ -1159,15 +1440,11 @@ namespace platf::dxgi { } std::vector - display_vram_t::get_supported_sdr_capture_formats() { - return { DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_R8G8B8A8_UNORM }; - } - - std::vector - display_vram_t::get_supported_hdr_capture_formats() { + display_vram_t::get_supported_capture_formats() { return { - // scRGB FP16 is the desired format for HDR content. This will also handle - // 10-bit SDR displays with the increased precision of FP16 vs 8-bit UNORMs. + // scRGB FP16 is the ideal format for Wide Color Gamut and Advanced Color + // displays (both SDR and HDR). This format uses linear gamma, so we will + // use a linear->PQ shader for HDR and a linear->sRGB shader for SDR. DXGI_FORMAT_R16G16B16A16_FLOAT, // DXGI_FORMAT_R10G10B10A2_UNORM seems like it might give us frames already @@ -1179,9 +1456,10 @@ namespace platf::dxgi { // but we avoid it for now. // We include the 8-bit modes too for when the display is in SDR mode, - // while the client stream is HDR-capable. These UNORM formats behave - // like a degenerate case of scRGB FP16 with values between 0.0f-1.0f. + // while the client stream is HDR-capable. These UNORM formats can + // use our normal pixel shaders that expect sRGB input. DXGI_FORMAT_B8G8R8A8_UNORM, + DXGI_FORMAT_B8G8R8X8_UNORM, DXGI_FORMAT_R8G8B8A8_UNORM, }; } @@ -1226,6 +1504,11 @@ namespace platf::dxgi { return -1; } + convert_Y_linear_ps_hlsl = compile_pixel_shader(SUNSHINE_SHADERS_DIR "/ConvertYPS_Linear.hlsl"); + if (!convert_Y_linear_ps_hlsl) { + return -1; + } + convert_UV_ps_hlsl = compile_pixel_shader(SUNSHINE_SHADERS_DIR "/ConvertUVPS.hlsl"); if (!convert_UV_ps_hlsl) { return -1; @@ -1236,6 +1519,11 @@ namespace platf::dxgi { return -1; } + convert_UV_linear_ps_hlsl = compile_pixel_shader(SUNSHINE_SHADERS_DIR "/ConvertUVPS_Linear.hlsl"); + if (!convert_UV_linear_ps_hlsl) { + return -1; + } + convert_UV_vs_hlsl = compile_vertex_shader(SUNSHINE_SHADERS_DIR "/ConvertUVVS.hlsl"); if (!convert_UV_vs_hlsl) { return -1; diff --git a/src/platform/windows/input.cpp b/src/platform/windows/input.cpp index 0e53766527d..c94904267b3 100644 --- a/src/platform/windows/input.cpp +++ b/src/platform/windows/input.cpp @@ -1,3 +1,7 @@ +/** + * @file src/platform/windows/input.cpp + * @brief todo + */ #include #include @@ -338,15 +342,18 @@ namespace platf { } void - keyboard(input_t &input, uint16_t modcode, bool release) { + keyboard(input_t &input, uint16_t modcode, bool release, uint8_t flags) { auto raw = (input_raw_t *) input.get(); INPUT i {}; i.type = INPUT_KEYBOARD; auto &ki = i.ki; - // For some reason, MapVirtualKey(VK_LWIN, MAPVK_VK_TO_VSC) doesn't seem to work :/ - if (modcode != VK_LWIN && modcode != VK_RWIN && modcode != VK_PAUSE && raw->keyboard_layout != NULL) { + // If the client did not normalize this VK code to a US English layout, we can't accurately convert it to a scancode. + bool send_scancode = !(flags & SS_KBE_FLAG_NON_NORMALIZED) || config::input.always_send_scancodes; + + if (send_scancode && modcode != VK_LWIN && modcode != VK_RWIN && modcode != VK_PAUSE && raw->keyboard_layout != NULL) { + // For some reason, MapVirtualKey(VK_LWIN, MAPVK_VK_TO_VSC) doesn't seem to work :/ ki.wScan = MapVirtualKeyEx(modcode, MAPVK_VK_TO_VSC, raw->keyboard_layout); } @@ -355,7 +362,7 @@ namespace platf { ki.dwFlags = KEYEVENTF_SCANCODE; } else { - // If there is no scancode mapping, send it as a regular VK event. + // If there is no scancode mapping or it's non-normalized, send it as a regular VK event. ki.wVk = modcode; } @@ -494,6 +501,7 @@ namespace platf { if(flags & LEFT_BUTTON) buttons |= DS4_BUTTON_SHOULDER_LEFT; if(flags & RIGHT_BUTTON) buttons |= DS4_BUTTON_SHOULDER_RIGHT; if(flags & START) buttons |= DS4_BUTTON_OPTIONS; + if(flags & BACK) buttons |= DS4_BUTTON_SHARE; if(flags & A) buttons |= DS4_BUTTON_CROSS; if(flags & B) buttons |= DS4_BUTTON_CIRCLE; if(flags & X) buttons |= DS4_BUTTON_SQUARE; @@ -510,7 +518,6 @@ namespace platf { ds4_special_buttons(const gamepad_state_t &gamepad_state) { int buttons {}; - if (gamepad_state.buttonFlags & BACK) buttons |= DS4_SPECIAL_BUTTON_TOUCHPAD; if (gamepad_state.buttonFlags & HOME) buttons |= DS4_SPECIAL_BUTTON_PS; return (DS4_SPECIAL_BUTTONS) buttons; diff --git a/src/platform/windows/misc.cpp b/src/platform/windows/misc.cpp index 45b25e3ddef..d16c1d6bdbb 100644 --- a/src/platform/windows/misc.cpp +++ b/src/platform/windows/misc.cpp @@ -1,3 +1,8 @@ +/** + * @file src/platform/windows/misc.cpp + * @brief todo + */ +#include #include #include #include @@ -19,6 +24,8 @@ #include #include #include +#include +#include // clang-format on #include "src/main.h" @@ -46,6 +53,8 @@ using namespace std::literals; namespace platf { using adapteraddrs_t = util::c_ptr; + static std::wstring_convert, wchar_t> converter; + bool enabled_mouse_keys = false; MOUSEKEYS previous_mouse_keys_state; @@ -72,14 +81,13 @@ namespace platf { std::string from_sockaddr(const sockaddr *const socket_address) { - char data[INET6_ADDRSTRLEN]; + char data[INET6_ADDRSTRLEN] = {}; auto family = socket_address->sa_family; if (family == AF_INET6) { inet_ntop(AF_INET6, &((sockaddr_in6 *) socket_address)->sin6_addr, data, INET6_ADDRSTRLEN); } - - if (family == AF_INET) { + else if (family == AF_INET) { inet_ntop(AF_INET, &((sockaddr_in *) socket_address)->sin_addr, data, INET_ADDRSTRLEN); } @@ -88,16 +96,15 @@ namespace platf { std::pair from_sockaddr_ex(const sockaddr *const ip_addr) { - char data[INET6_ADDRSTRLEN]; + char data[INET6_ADDRSTRLEN] = {}; auto family = ip_addr->sa_family; - std::uint16_t port; + std::uint16_t port = 0; if (family == AF_INET6) { inet_ntop(AF_INET6, &((sockaddr_in6 *) ip_addr)->sin6_addr, data, INET6_ADDRSTRLEN); port = ((sockaddr_in6 *) ip_addr)->sin6_port; } - - if (family == AF_INET) { + else if (family == AF_INET) { inet_ntop(AF_INET, &((sockaddr_in *) ip_addr)->sin_addr, data, INET_ADDRSTRLEN); port = ((sockaddr_in *) ip_addr)->sin_port; } @@ -174,127 +181,96 @@ namespace platf { BOOST_LOG(error) << prefix << ": "sv << std::string_view { err_string, bytes }; } - std::wstring - utf8_to_wide_string(const std::string &str) { - // Determine the size required for the destination string - int chars = MultiByteToWideChar(CP_UTF8, 0, str.data(), str.length(), NULL, 0); - - // Allocate it - wchar_t buffer[chars] = {}; - - // Do the conversion for real - chars = MultiByteToWideChar(CP_UTF8, 0, str.data(), str.length(), buffer, chars); - return std::wstring(buffer, chars); - } - - std::string - wide_to_utf8_string(const std::wstring &str) { - // Determine the size required for the destination string - int bytes = WideCharToMultiByte(CP_UTF8, 0, str.data(), str.length(), NULL, 0, NULL, NULL); - - // Allocate it - char buffer[bytes] = {}; - - // Do the conversion for real - bytes = WideCharToMultiByte(CP_UTF8, 0, str.data(), str.length(), buffer, bytes, NULL, NULL); - return std::string(buffer, bytes); - } - - HANDLE - duplicate_shell_token() { - // Get the shell window (will usually be owned by explorer.exe) - HWND shell_window = GetShellWindow(); - if (!shell_window) { - BOOST_LOG(error) << "No shell window found. Is explorer.exe running?"sv; - return NULL; - } - - // Open a handle to the explorer.exe process - DWORD shell_pid; - GetWindowThreadProcessId(shell_window, &shell_pid); - HANDLE shell_process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, shell_pid); - if (!shell_process) { - BOOST_LOG(error) << "Failed to open shell process: "sv << GetLastError(); - return NULL; - } - - // Open explorer's token to clone for process creation - HANDLE shell_token; - BOOL ret = OpenProcessToken(shell_process, TOKEN_DUPLICATE, &shell_token); - CloseHandle(shell_process); - if (!ret) { - BOOST_LOG(error) << "Failed to open shell process token: "sv << GetLastError(); - return NULL; + bool + IsUserAdmin(HANDLE user_token) { + WINBOOL ret; + SID_IDENTIFIER_AUTHORITY NtAuthority = SECURITY_NT_AUTHORITY; + PSID AdministratorsGroup; + ret = AllocateAndInitializeSid( + &NtAuthority, + 2, + SECURITY_BUILTIN_DOMAIN_RID, + DOMAIN_ALIAS_RID_ADMINS, + 0, 0, 0, 0, 0, 0, + &AdministratorsGroup); + if (ret) { + if (!CheckTokenMembership(user_token, AdministratorsGroup, &ret)) { + ret = false; + BOOST_LOG(error) << "Failed to verify token membership for administrative access: " << GetLastError(); + } + FreeSid(AdministratorsGroup); } - - // Duplicate the token to make it usable for process creation - HANDLE new_token; - ret = DuplicateTokenEx(shell_token, TOKEN_ALL_ACCESS, NULL, SecurityImpersonation, TokenPrimary, &new_token); - CloseHandle(shell_token); - if (!ret) { - BOOST_LOG(error) << "Failed to duplicate shell process token: "sv << GetLastError(); - return NULL; + else { + BOOST_LOG(error) << "Unable to allocate SID to check administrative access: " << GetLastError(); } - return new_token; + return ret; } - PTOKEN_USER - get_token_user(HANDLE token) { - DWORD return_length; - if (GetTokenInformation(token, TokenUser, NULL, 0, &return_length) || GetLastError() != ERROR_INSUFFICIENT_BUFFER) { - auto winerr = GetLastError(); - BOOST_LOG(error) << "Failed to get token information size: "sv << winerr; + /** + * @brief Obtain the current sessions user's primary token with elevated privileges. + * @return The user's token. If user has admin capability it will be elevated, otherwise it will be a limited token. On error, `nullptr`. + */ + HANDLE + retrieve_users_token(bool elevated) { + DWORD consoleSessionId; + HANDLE userToken; + TOKEN_ELEVATION_TYPE elevationType; + DWORD dwSize; + + // Get the session ID of the active console session + consoleSessionId = WTSGetActiveConsoleSessionId(); + if (0xFFFFFFFF == consoleSessionId) { + // If there is no active console session, log a warning and return null + BOOST_LOG(warning) << "There isn't an active user session, therefore it is not possible to execute commands under the users profile."; return nullptr; } - auto user = (PTOKEN_USER) HeapAlloc(GetProcessHeap(), 0, return_length); - if (!user) { + // Get the user token for the active console session + if (!WTSQueryUserToken(consoleSessionId, &userToken)) { + BOOST_LOG(debug) << "QueryUserToken failed, this would prevent commands from launching under the users profile."; return nullptr; } - if (!GetTokenInformation(token, TokenUser, user, return_length, &return_length)) { - auto winerr = GetLastError(); - BOOST_LOG(error) << "Failed to get token information: "sv << winerr; - HeapFree(GetProcessHeap(), 0, user); + // We need to know if this is an elevated token or not. + // Get the elevation type of the user token + // Elevation - Default: User is not an admin, UAC enabled/disabled does not matter. + // Elevation - Limited: User is an admin, has UAC enabled. + // Elevation - Full: User is an admin, has UAC disabled. + if (!GetTokenInformation(userToken, TokenElevationType, &elevationType, sizeof(TOKEN_ELEVATION_TYPE), &dwSize)) { + BOOST_LOG(debug) << "Retrieving token information failed: " << GetLastError(); + CloseHandle(userToken); return nullptr; } - return user; - } - - void - free_token_user(PTOKEN_USER user) { - HeapFree(GetProcessHeap(), 0, user); - } - - bool - is_token_same_user_as_process(HANDLE other_token) { - HANDLE process_token; - if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &process_token)) { - auto winerr = GetLastError(); - BOOST_LOG(error) << "Failed to open process token: "sv << winerr; - return false; - } - - auto process_user = get_token_user(process_token); - CloseHandle(process_token); - if (!process_user) { - return false; + // User is currently not an administrator + // The documentation for this scenario is conflicting, so we'll double check to see if user is actually an admin. + if (elevated && (elevationType == TokenElevationTypeDefault && !IsUserAdmin(userToken))) { + // We don't have to strip the token or do anything here, but let's give the user a warning so they're aware what is happening. + BOOST_LOG(warning) << "This command requires elevation and the current user account logged in does not have administrator rights. " + << "For security reasons Sunshine will retain the same access level as the current user and will not elevate it."; } - auto token_user = get_token_user(other_token); - if (!token_user) { - free_token_user(process_user); - return false; - } + // User has a limited token, this means they have UAC enabled and is an Administrator + if (elevated && elevationType == TokenElevationTypeLimited) { + TOKEN_LINKED_TOKEN linkedToken; + // Retrieve the administrator token that is linked to the limited token + if (!GetTokenInformation(userToken, TokenLinkedToken, reinterpret_cast(&linkedToken), sizeof(TOKEN_LINKED_TOKEN), &dwSize)) { + // If the retrieval failed, log an error message and return null + BOOST_LOG(error) << "Retrieving linked token information failed: " << GetLastError(); + CloseHandle(userToken); - bool ret = EqualSid(process_user->User.Sid, token_user->User.Sid); + // There is no scenario where this should be hit, except for an actual error. + return nullptr; + } - free_token_user(process_user); - free_token_user(token_user); + // Since we need the elevated token, we'll replace it with their administrative token. + CloseHandle(userToken); + userToken = linkedToken.LinkedToken; + } - return ret; + // We don't need to do anything for TokenElevationTypeFull users here, because they're already elevated. + return userToken; } bool @@ -308,7 +284,7 @@ namespace platf { // Parse the environment block and populate env for (auto c = (PWCHAR) env_block; *c != UNICODE_NULL; c += wcslen(c) + 1) { // Environment variable entries end with a null-terminator, so std::wstring() will get an entire entry. - std::string env_tuple = wide_to_utf8_string(std::wstring { c }); + std::string env_tuple = converter.to_bytes(std::wstring { c }); std::string env_name = env_tuple.substr(0, env_tuple.find('=')); std::string env_val = env_tuple.substr(env_tuple.find('=') + 1); @@ -334,6 +310,41 @@ namespace platf { return true; } + /** + * @brief Check if the current process is running with system-level privileges. + * @return `true` if the current process has system-level privileges, `false` otherwise. + */ + bool + is_running_as_system() { + BOOL ret; + PSID SystemSid; + DWORD dwSize = SECURITY_MAX_SID_SIZE; + + // Allocate memory for the SID structure + SystemSid = LocalAlloc(LMEM_FIXED, dwSize); + if (SystemSid == nullptr) { + BOOST_LOG(error) << "Failed to allocate memory for the SID structure: " << GetLastError(); + return false; + } + + // Create a SID for the local system account + ret = CreateWellKnownSid(WinLocalSystemSid, nullptr, SystemSid, &dwSize); + if (ret) { + // Check if the current process token contains this SID + if (!CheckTokenMembership(nullptr, SystemSid, &ret)) { + BOOST_LOG(error) << "Failed to check token membership: " << GetLastError(); + ret = false; + } + } + else { + BOOST_LOG(error) << "Failed to create a SID for the local system account. This may happen if the system is out of memory or if the SID buffer is too small: " << GetLastError(); + } + + // Free the memory allocated for the SID structure + LocalFree(SystemSid); + return ret; + } + // Note: This does NOT append a null terminator void append_string_to_environment_block(wchar_t *env_block, int &offset, const std::wstring &wstr) { @@ -347,7 +358,7 @@ namespace platf { for (const auto &entry : env) { auto name = entry.get_name(); auto value = entry.to_string(); - size += utf8_to_wide_string(name).length() + 1 /* L'=' */ + utf8_to_wide_string(value).length() + 1 /* L'\0' */; + size += converter.from_bytes(name).length() + 1 /* L'=' */ + converter.from_bytes(value).length() + 1 /* L'\0' */; } size += 1 /* L'\0' */; @@ -359,9 +370,9 @@ namespace platf { auto value = entry.to_string(); // Construct the NAME=VAL\0 string - append_string_to_environment_block(env_block, offset, utf8_to_wide_string(name)); + append_string_to_environment_block(env_block, offset, converter.from_bytes(name)); env_block[offset++] = L'='; - append_string_to_environment_block(env_block, offset, utf8_to_wide_string(value)); + append_string_to_environment_block(env_block, offset, converter.from_bytes(value)); env_block[offset++] = L'\0'; } @@ -395,46 +406,111 @@ namespace platf { HeapFree(GetProcessHeap(), 0, list); } + /** + * @brief Create a `bp::child` object from the results of launching a process. + * @param process_launched A boolean indicating if the launch was successful. + * @param cmd The command that was used to launch the process. + * @param ec A reference to an `std::error_code` object that will store any error that occurred during the launch. + * @param process_info A reference to a `PROCESS_INFORMATION` structure that contains information about the new process. + * @param group A pointer to a `bp::group` object that will add the new process to its group, if not null. + * @return A `bp::child` object representing the new process, or an empty `bp::child` object if the launch failed. + */ bp::child - run_unprivileged(const std::string &cmd, boost::filesystem::path &working_dir, bp::environment &env, FILE *file, std::error_code &ec, bp::group *group) { - HANDLE shell_token = duplicate_shell_token(); - if (!shell_token) { - // This can happen if the shell has crashed. Fail the launch rather than risking launching with - // Sunshine's permissions unmodified. - ec = std::make_error_code(std::errc::no_such_process); + create_boost_child_from_results(bool process_launched, const std::string &cmd, std::error_code &ec, PROCESS_INFORMATION &process_info, bp::group *group) { + // Use RAII to ensure the process is closed when we're done with it, even if there was an error. + auto close_process_handles = util::fail_guard([process_launched, process_info]() { + if (process_launched) { + CloseHandle(process_info.hThread); + CloseHandle(process_info.hProcess); + } + }); + + if (ec) { + // If there was an error, return an empty bp::child object return bp::child(); } - auto token_close = util::fail_guard([shell_token]() { - CloseHandle(shell_token); - }); + if (process_launched) { + // If the launch was successful, create a new bp::child object representing the new process + auto child = bp::child((bp::pid_t) process_info.dwProcessId); + if (group) { + // If a group was provided, add the new process to the group + group->add(child); + } - // Populate env with user-specific environment variables - if (!merge_user_environment_block(env, shell_token)) { - ec = std::make_error_code(std::errc::not_enough_memory); + BOOST_LOG(info) << cmd << " running with PID "sv << child.id(); + return child; + } + else { + auto winerror = GetLastError(); + BOOST_LOG(error) << "Failed to launch process: "sv << winerror; + ec = std::make_error_code(std::errc::invalid_argument); + // We must NOT attach the failed process here, since this case can potentially be induced by ACL + // manipulation (denying yourself execute permission) to cause an escalation of privilege. + // So to protect ourselves against that, we'll return an empty child process instead. return bp::child(); } + } - // Most Win32 APIs can't consume UTF-8 strings directly, so we must convert them into UTF-16 - std::wstring wcmd = utf8_to_wide_string(cmd); - std::wstring env_block = create_environment_block(env); - std::wstring start_dir = utf8_to_wide_string(working_dir.string()); + /** + * @brief Impersonate the current user and invoke the callback function. + * @param user_token A handle to the user's token that was obtained from the shell. + * @param callback A function that will be executed while impersonating the user. + * @return An `std::error_code` object that will store any error that occurred during the impersonation + */ + std::error_code + impersonate_current_user(HANDLE user_token, std::function callback) { + std::error_code ec; + // Impersonate the user when launching the process. This will ensure that appropriate access + // checks are done against the user token, not our SYSTEM token. It will also allow network + // shares and mapped network drives to be used as launch targets, since those credentials + // are stored per-user. + if (!ImpersonateLoggedOnUser(user_token)) { + auto winerror = GetLastError(); + // Log the failure of impersonating the user and its error code + BOOST_LOG(error) << "Failed to impersonate user: "sv << winerror; + ec = std::make_error_code(std::errc::permission_denied); + return ec; + } + + // Execute the callback function while impersonating the user + callback(); + + // End impersonation of the logged on user. If this fails (which is extremely unlikely), + // we will be running with an unknown user token. The only safe thing to do in that case + // is terminate ourselves. + if (!RevertToSelf()) { + auto winerror = GetLastError(); + // Log the failure of reverting to self and its error code + BOOST_LOG(fatal) << "Failed to revert to self after impersonation: "sv << winerror; + std::abort(); + } + return ec; + } + + /** + * @brief A function to create a `STARTUPINFOEXW` structure for launching a process. + * @param file A pointer to a `FILE` object that will be used as the standard output and error for the new process, or null if not needed. + * @param ec A reference to a `std::error_code` object that will store any error that occurred during the creation of the structure. + * @return A `STARTUPINFOEXW` structure that contains information about how to launch the new process. + */ + STARTUPINFOEXW + create_startup_info(FILE *file, std::error_code &ec) { + // Initialize a zeroed-out STARTUPINFOEXW structure and set its size STARTUPINFOEXW startup_info = {}; startup_info.StartupInfo.cb = sizeof(startup_info); // Allocate a process attribute list with space for 1 element startup_info.lpAttributeList = allocate_proc_thread_attr_list(1); if (startup_info.lpAttributeList == NULL) { + // If the allocation failed, set ec to an appropriate error code and return the structure ec = std::make_error_code(std::errc::not_enough_memory); - return bp::child(); + return startup_info; } - auto attr_list_free = util::fail_guard([list = startup_info.lpAttributeList]() { - free_proc_thread_attr_list(list); - }); - if (file) { + // If a file was provided, get its handle and use it as the standard output and error for the new process HANDLE log_file_handle = (HANDLE) _get_osfhandle(_fileno(file)); // Populate std handles if the caller gave us a log file to use @@ -445,119 +521,154 @@ namespace platf { // Allow the log file handle to be inherited by the child process (without inheriting all of // our inheritable handles, such as our own log file handle created by SunshineSvc). + // + // Note: The value we point to here must be valid for the lifetime of the attribute list, + // so we need to point into the STARTUPINFO instead of our log_file_variable on the stack. UpdateProcThreadAttribute(startup_info.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, - &log_file_handle, - sizeof(log_file_handle), + &startup_info.StartupInfo.hStdOutput, + sizeof(startup_info.StartupInfo.hStdOutput), NULL, NULL); } - // If we're running with the same user account as the shell, just use CreateProcess(). - // This will launch the child process elevated if Sunshine is elevated. - PROCESS_INFORMATION process_info; + return startup_info; + } + + /** + * @brief Run a command on the users profile. + * + * Launches a child process as the user, using the current user's environment and a specific working directory. + * + * @param elevated Specify whether to elevate the process. + * @param interactive Specify whether this will run in a window or hidden. + * @param cmd The command to run. + * @param working_dir The working directory for the new process. + * @param env The environment variables to use for the new process. + * @param file A file object to redirect the child process's output to (may be `nullptr`). + * @param ec An error code, set to indicate any errors that occur during the launch process. + * @param group A pointer to a `bp::group` object to which the new process should belong (may be `nullptr`). + * @return A `bp::child` object representing the new process, or an empty `bp::child` object if the launch fails. + */ + bp::child + run_command(bool elevated, bool interactive, const std::string &cmd, boost::filesystem::path &working_dir, bp::environment &env, FILE *file, std::error_code &ec, bp::group *group) { BOOL ret; - if (!is_token_same_user_as_process(shell_token)) { - // Impersonate the user when launching the process. This will ensure that appropriate access - // checks are done against the user token, not our SYSTEM token. It will also allow network - // shares and mapped network drives to be used as launch targets, since those credentials - // are stored per-user. - if (!ImpersonateLoggedOnUser(shell_token)) { - auto winerror = GetLastError(); - BOOST_LOG(error) << "Failed to impersonate user: "sv << winerror; + // Convert cmd, env, and working_dir to the appropriate character sets for Win32 APIs + std::wstring wcmd = converter.from_bytes(cmd); + std::wstring start_dir = converter.from_bytes(working_dir.string()); + + STARTUPINFOEXW startup_info = create_startup_info(file, ec); + PROCESS_INFORMATION process_info; + + if (ec) { + // In the event that startup_info failed, return a blank child process. + return bp::child(); + } + + // Use RAII to ensure the attribute list is freed when we're done with it + auto attr_list_free = util::fail_guard([list = startup_info.lpAttributeList]() { + free_proc_thread_attr_list(list); + }); + + DWORD creation_flags = EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT | CREATE_BREAKAWAY_FROM_JOB; + + // Create a new console for interactive processes and use no console for non-interactive processes + creation_flags |= interactive ? CREATE_NEW_CONSOLE : CREATE_NO_WINDOW; + + if (is_running_as_system()) { + // Duplicate the current user's token + HANDLE user_token = retrieve_users_token(elevated); + if (!user_token) { + // Fail the launch rather than risking launching with Sunshine's permissions unmodified. ec = std::make_error_code(std::errc::permission_denied); return bp::child(); } - // Launch the process with the duplicated shell token. - // Set CREATE_BREAKAWAY_FROM_JOB to avoid the child being killed if SunshineSvc.exe is terminated. - // Set CREATE_NEW_CONSOLE to avoid writing stdout to Sunshine's log if 'file' is not specified. - ret = CreateProcessAsUserW(shell_token, - NULL, - (LPWSTR) wcmd.c_str(), - NULL, - NULL, - !!(startup_info.StartupInfo.dwFlags & STARTF_USESTDHANDLES), - EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT | CREATE_NEW_CONSOLE | CREATE_BREAKAWAY_FROM_JOB, - env_block.data(), - start_dir.empty() ? NULL : start_dir.c_str(), - (LPSTARTUPINFOW) &startup_info, - &process_info); + // Use RAII to ensure the shell token is closed when we're done with it + auto token_close = util::fail_guard([user_token]() { + CloseHandle(user_token); + }); - if (!ret) { - auto error = GetLastError(); - - if (error == 740) { - BOOST_LOG(info) << "Could not execute previous command because it required elevation. Running the command again with elevation, for security reasons this will prompt user interaction."sv; - startup_info.StartupInfo.wShowWindow = SW_HIDE; - startup_info.StartupInfo.dwFlags = startup_info.StartupInfo.dwFlags | STARTF_USESHOWWINDOW; - std::wstring elevated_command = L"tools\\elevator.exe "; - elevated_command += wcmd; - - // For security reasons, Windows enforces that an application can have only one "interactive thread" responsible for processing user input and managing the user interface (UI). - // Since UAC prompts are interactive, we cannot have a UAC prompt while Sunshine is already running because it would block the thread. - // To work around this issue, we will launch a separate process that will elevate the command, which will prompt the user to confirm the elevation. - // This is our intended behavior: to require interaction before elevating the command. - ret = CreateProcessAsUserW(shell_token, - nullptr, - (LPWSTR) elevated_command.c_str(), - nullptr, - nullptr, - !!(startup_info.StartupInfo.dwFlags & STARTF_USESTDHANDLES), - EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT | CREATE_NEW_CONSOLE | CREATE_BREAKAWAY_FROM_JOB, - env_block.data(), - start_dir.empty() ? nullptr : start_dir.c_str(), - (LPSTARTUPINFOW) &startup_info, - &process_info); - } + // Populate env with user-specific environment variables + if (!merge_user_environment_block(env, user_token)) { + ec = std::make_error_code(std::errc::not_enough_memory); + return bp::child(); } - // End impersonation of the logged on user. If this fails (which is extremely unlikely), - // we will be running with an unknown user token. The only safe thing to do in that case - // is terminate ourselves. - if (!RevertToSelf()) { - auto winerror = GetLastError(); - BOOST_LOG(fatal) << "Failed to revert to self after impersonation: "sv << winerror; - std::abort(); - } + // Open the process as the current user account, elevation is handled in the token itself. + ec = impersonate_current_user(user_token, [&]() { + std::wstring env_block = create_environment_block(env); + ret = CreateProcessAsUserW(user_token, + NULL, + (LPWSTR) wcmd.c_str(), + NULL, + NULL, + !!(startup_info.StartupInfo.dwFlags & STARTF_USESTDHANDLES), + creation_flags, + env_block.data(), + start_dir.empty() ? NULL : start_dir.c_str(), + (LPSTARTUPINFOW) &startup_info, + &process_info); + }); } + // Otherwise, launch the process using CreateProcessW() + // This will inherit the elevation of whatever the user launched Sunshine with. else { + // Open our current token to resolve environment variables + HANDLE process_token; + if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY | TOKEN_DUPLICATE, &process_token)) { + ec = std::make_error_code(std::errc::permission_denied); + return bp::child(); + } + auto token_close = util::fail_guard([process_token]() { + CloseHandle(process_token); + }); + + // Populate env with user-specific environment variables + if (!merge_user_environment_block(env, process_token)) { + ec = std::make_error_code(std::errc::not_enough_memory); + return bp::child(); + } + + std::wstring env_block = create_environment_block(env); ret = CreateProcessW(NULL, (LPWSTR) wcmd.c_str(), NULL, NULL, !!(startup_info.StartupInfo.dwFlags & STARTF_USESTDHANDLES), - EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT | CREATE_NEW_CONSOLE | CREATE_BREAKAWAY_FROM_JOB, + creation_flags, env_block.data(), start_dir.empty() ? NULL : start_dir.c_str(), (LPSTARTUPINFOW) &startup_info, &process_info); } - if (ret) { - // Since we are always spawning a process with a less privileged token than ourselves, - // bp::child() should have no problem opening it with any access rights it wants. - auto child = bp::child((bp::pid_t) process_info.dwProcessId); - if (group) { - group->add(child); - } - - // Only close handles after bp::child() has opened the process. If the process terminates - // quickly, the PID could be reused if we close the process handle. - CloseHandle(process_info.hThread); - CloseHandle(process_info.hProcess); + // Use the results of the launch to create a bp::child object + return create_boost_child_from_results(ret, cmd, ec, process_info, group); + } - BOOST_LOG(info) << cmd << " running with PID "sv << child.id(); - return child; + /** + * @brief Open a url in the default web browser. + * @param url The url to open. + */ + void + open_url(const std::string &url) { + // set working dir to Windows system directory + auto working_dir = boost::filesystem::path(std::getenv("SystemRoot")); + + boost::process::environment _env = boost::this_process::environment(); + std::error_code ec; + + // Launch this as a non-interactive non-elevated command to avoid an extra console window + std::string cmd = R"(cmd /C "start )" + url + R"(")"; + auto child = run_command(false, false, cmd, working_dir, _env, nullptr, ec, nullptr); + if (ec) { + BOOST_LOG(warning) << "Couldn't open url ["sv << url << "]: System: "sv << ec.message(); } else { - // We must NOT try bp::child() here, since this case can potentially be induced by ACL - // manipulation (denying yourself execute permission) to cause an escalation of privilege. - auto winerror = GetLastError(); - BOOST_LOG(error) << "Failed to launch process: "sv << winerror; - ec = std::make_error_code(std::errc::invalid_argument); - return bp::child(); + BOOST_LOG(info) << "Opened url ["sv << url << "]"sv; + child.detach(); } } @@ -719,18 +830,49 @@ namespace platf { } } - bool - restart_supported() { - // Restart is supported if we're running from the service - return (GetConsoleWindow() == NULL); + void + restart_on_exit() { + STARTUPINFOEXW startup_info {}; + startup_info.StartupInfo.cb = sizeof(startup_info); + + WCHAR executable[MAX_PATH]; + if (GetModuleFileNameW(NULL, executable, ARRAYSIZE(executable)) == 0) { + auto winerr = GetLastError(); + BOOST_LOG(fatal) << "Failed to get Sunshine path: "sv << winerr; + return; + } + + PROCESS_INFORMATION process_info; + if (!CreateProcessW(executable, + GetCommandLineW(), + nullptr, + nullptr, + false, + CREATE_UNICODE_ENVIRONMENT | EXTENDED_STARTUPINFO_PRESENT, + nullptr, + nullptr, + (LPSTARTUPINFOW) &startup_info, + &process_info)) { + auto winerr = GetLastError(); + BOOST_LOG(fatal) << "Unable to restart Sunshine: "sv << winerr; + return; + } + + CloseHandle(process_info.hProcess); + CloseHandle(process_info.hThread); } - bool + void restart() { - // Raise SIGINT to trigger the graceful exit logic. The service will - // restart us in a few seconds. - std::raise(SIGINT); - return true; + // If we're running standalone, we have to respawn ourselves via CreateProcess(). + // If we're running from the service, we should just exit and let it respawn us. + if (GetConsoleWindow() != NULL) { + // Avoid racing with the new process by waiting until we're exiting to start it. + atexit(restart_on_exit); + } + + // We use an async exit call here because we can't block the HTTP thread or we'll hang shutdown. + lifetime::exit_sunshine(0, true); } SOCKADDR_IN @@ -899,5 +1041,25 @@ namespace platf { return std::make_unique(flow_id); } + int64_t + qpc_counter() { + LARGE_INTEGER performace_counter; + if (QueryPerformanceCounter(&performace_counter)) return performace_counter.QuadPart; + return 0; + } -} // namespace platf \ No newline at end of file + std::chrono::nanoseconds + qpc_time_difference(int64_t performance_counter1, int64_t performance_counter2) { + auto get_frequency = []() { + LARGE_INTEGER frequency; + frequency.QuadPart = 0; + QueryPerformanceFrequency(&frequency); + return frequency.QuadPart; + }; + static const double frequency = get_frequency(); + if (frequency) { + return std::chrono::nanoseconds((int64_t) ((performance_counter1 - performance_counter2) * frequency / std::nano::den)); + } + return {}; + } +} // namespace platf diff --git a/src/platform/windows/misc.h b/src/platform/windows/misc.h index 4bcd31fd397..9228ce59fe7 100644 --- a/src/platform/windows/misc.h +++ b/src/platform/windows/misc.h @@ -1,6 +1,10 @@ -#ifndef SUNSHINE_WINDOWS_MISC_H -#define SUNSHINE_WINDOWS_MISC_H +/** + * @file src/platform/windows/misc.h + * @brief todo + */ +#pragma once +#include #include #include #include @@ -10,6 +14,10 @@ namespace platf { print_status(const std::string_view &prefix, HRESULT status); HDESK syncThreadDesktop(); -} // namespace platf -#endif \ No newline at end of file + int64_t + qpc_counter(); + + std::chrono::nanoseconds + qpc_time_difference(int64_t performance_counter1, int64_t performance_counter2); +} // namespace platf diff --git a/src/platform/windows/publish.cpp b/src/platform/windows/publish.cpp index 56cfdfc67c6..3d9383f7e49 100644 --- a/src/platform/windows/publish.cpp +++ b/src/platform/windows/publish.cpp @@ -1,3 +1,7 @@ +/** + * @file src/platform/windows/publish.cpp + * @brief todo + */ #include #include diff --git a/src/process.cpp b/src/process.cpp index 6053e73bbcf..1a0f8e53e7b 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -1,5 +1,7 @@ -// Created by loki on 12/14/19. - +/** + * @file src/process.cpp + * @brief todo + */ #define BOOST_BIND_GLOBAL_PLACEHOLDERS #include "process.h" @@ -38,6 +40,22 @@ namespace proc { proc_t proc; + class deinit_t: public platf::deinit_t { + public: + ~deinit_t() { + proc.terminate(); + } + }; + + /** + * @brief Initializes proc functions + * @return Unique pointer to `deinit_t` to manage cleanup + */ + std::unique_ptr + init() { + return std::make_unique(); + } + void process_end(bp::child &proc, bp::group &proc_handle) { if (!proc.running()) { @@ -125,16 +143,21 @@ namespace proc { }); for (; _app_prep_it != std::end(_app.prep_cmds); ++_app_prep_it) { - auto &cmd = _app_prep_it->do_cmd; + auto &cmd = *_app_prep_it; + + // Skip empty commands + if (cmd.do_cmd.empty()) { + continue; + } boost::filesystem::path working_dir = _app.working_dir.empty() ? - find_working_directory(cmd, _env) : + find_working_directory(cmd.do_cmd, _env) : boost::filesystem::path(_app.working_dir); - BOOST_LOG(info) << "Executing Do Cmd: ["sv << cmd << ']'; - auto child = platf::run_unprivileged(cmd, working_dir, _env, _pipe.get(), ec, nullptr); + 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); if (ec) { - BOOST_LOG(error) << "Couldn't run ["sv << cmd << "]: System: "sv << ec.message(); + BOOST_LOG(error) << "Couldn't run ["sv << cmd.do_cmd << "]: System: "sv << ec.message(); return -1; } @@ -142,7 +165,7 @@ namespace proc { auto ret = child.exit_code(); if (ret != 0) { - BOOST_LOG(error) << '[' << cmd << "] failed with code ["sv << ret << ']'; + BOOST_LOG(error) << '[' << cmd.do_cmd << "] failed with code ["sv << ret << ']'; return -1; } } @@ -152,7 +175,7 @@ namespace proc { find_working_directory(cmd, _env) : boost::filesystem::path(_app.working_dir); BOOST_LOG(info) << "Spawning ["sv << cmd << "] in ["sv << working_dir << ']'; - auto child = platf::run_unprivileged(cmd, working_dir, _env, _pipe.get(), ec, nullptr); + auto child = platf::run_command(_app.elevated, true, cmd, working_dir, _env, _pipe.get(), ec, nullptr); if (ec) { BOOST_LOG(warning) << "Couldn't spawn ["sv << cmd << "]: System: "sv << ec.message(); } @@ -170,7 +193,7 @@ namespace proc { find_working_directory(_app.cmd, _env) : boost::filesystem::path(_app.working_dir); BOOST_LOG(info) << "Executing: ["sv << _app.cmd << "] in ["sv << working_dir << ']'; - _process = platf::run_unprivileged(_app.cmd, working_dir, _env, _pipe.get(), ec, &_process_handle); + _process = platf::run_command(_app.elevated, true, _app.cmd, working_dir, _env, _pipe.get(), ec, &_process_handle); if (ec) { BOOST_LOG(warning) << "Couldn't run ["sv << _app.cmd << "]: System: "sv << ec.message(); return -1; @@ -208,17 +231,17 @@ namespace proc { _app_id = -1; for (; _app_prep_it != _app_prep_begin; --_app_prep_it) { - auto &cmd = (_app_prep_it - 1)->undo_cmd; + auto &cmd = *(_app_prep_it - 1); - if (cmd.empty()) { + if (cmd.undo_cmd.empty()) { continue; } boost::filesystem::path working_dir = _app.working_dir.empty() ? - find_working_directory(cmd, _env) : + find_working_directory(cmd.undo_cmd, _env) : boost::filesystem::path(_app.working_dir); - BOOST_LOG(info) << "Executing Undo Cmd: ["sv << cmd << ']'; - auto child = platf::run_unprivileged(cmd, working_dir, _env, _pipe.get(), ec, nullptr); + 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); if (ec) { BOOST_LOG(warning) << "System: "sv << ec.message(); @@ -259,7 +282,12 @@ namespace proc { } proc_t::~proc_t() { - terminate(); + // 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, + // we return a deinit_t to main() to handle termination when we're exiting. + // Once we reach this point here, termination must have already happened. + assert(!placebo); + assert(!_process.running()); } std::string_view::iterator @@ -481,6 +509,7 @@ namespace proc { auto cmd = app_node.get_optional("cmd"s); auto image_path = app_node.get_optional("image-path"s); auto working_dir = app_node.get_optional("working-dir"s); + auto elevated = app_node.get_optional("elevated"s); std::vector prep_cmds; if (!exclude_global_prep.value_or(false)) { @@ -489,7 +518,10 @@ namespace proc { auto do_cmd = parse_env_val(this_env, prep_cmd.do_cmd); auto undo_cmd = parse_env_val(this_env, prep_cmd.undo_cmd); - prep_cmds.emplace_back(std::move(do_cmd), std::move(undo_cmd)); + prep_cmds.emplace_back( + std::move(do_cmd), + std::move(undo_cmd), + std::move(prep_cmd.elevated)); } } @@ -498,15 +530,14 @@ namespace proc { prep_cmds.reserve(prep_cmds.size() + prep_nodes.size()); for (auto &[_, prep_node] : prep_nodes) { - auto do_cmd = parse_env_val(this_env, prep_node.get("do"s)); + auto do_cmd = prep_node.get_optional("do"s); auto undo_cmd = prep_node.get_optional("undo"s); + auto elevated = prep_node.get_optional("elevated"); - if (undo_cmd) { - prep_cmds.emplace_back(std::move(do_cmd), parse_env_val(this_env, *undo_cmd)); - } - else { - prep_cmds.emplace_back(std::move(do_cmd)); - } + prep_cmds.emplace_back( + parse_env_val(this_env, do_cmd.value_or("")), + parse_env_val(this_env, undo_cmd.value_or("")), + std::move(elevated.value_or(false))); } } @@ -536,6 +567,8 @@ namespace proc { ctx.image_path = parse_env_val(this_env, *image_path); } + ctx.elevated = elevated.value_or(false); + auto possible_ids = calculate_app_id(name, ctx.image_path, i++); if (ids.count(std::get<0>(possible_ids)) == 0) { // Avoid using index to generate id if possible diff --git a/src/process.h b/src/process.h index 12f4b5d9077..038e86f0e82 100644 --- a/src/process.h +++ b/src/process.h @@ -1,7 +1,8 @@ -// Created by loki on 12/14/19. - -#ifndef SUNSHINE_PROCESS_H -#define SUNSHINE_PROCESS_H +/** + * @file src/process.h + * @brief todo + */ +#pragma once #ifndef __kernel_entry #define __kernel_entry @@ -13,33 +14,34 @@ #include #include "config.h" +#include "platform/common.h" #include "utility.h" namespace proc { using file_t = util::safe_ptr_v2; typedef config::prep_cmd_t cmd_t; - /* - * pre_cmds -- guaranteed to be executed unless any of the commands fail. - * detached -- commands detached from Sunshine - * cmd -- Runs indefinitely until: - * No session is running and a different set of commands it to be executed - * Command exits - * working_dir -- the process working directory. This is required for some games to run properly. - * cmd_output -- - * empty -- The output of the commands are appended to the output of sunshine - * "null" -- The output of the commands are discarded - * filename -- The output of the commands are appended to filename - */ + /** + * pre_cmds -- guaranteed to be executed unless any of the commands fail. + * detached -- commands detached from Sunshine + * cmd -- Runs indefinitely until: + * No session is running and a different set of commands it to be executed + * Command exits + * working_dir -- the process working directory. This is required for some games to run properly. + * cmd_output -- + * empty -- The output of the commands are appended to the output of sunshine + * "null" -- The output of the commands are discarded + * filename -- The output of the commands are appended to filename + */ struct ctx_t { std::vector prep_cmds; /** - * Some applications, such as Steam, - * either exit quickly, or keep running indefinitely. - * Steam.exe is one such application. - * That is why some applications need be run and forgotten about - */ + * Some applications, such as Steam, + * either exit quickly, or keep running indefinitely. + * Steam.exe is one such application. + * That is why some applications need be run and forgotten about + */ std::vector detached; std::string name; @@ -48,6 +50,7 @@ namespace proc { std::string output; std::string image_path; std::string id; + bool elevated; }; class proc_t { @@ -65,8 +68,8 @@ namespace proc { execute(int app_id); /** - * @return _app_id if a process is running, otherwise returns 0 - */ + * @return _app_id if a process is running, otherwise returns 0 + */ int running(); @@ -101,9 +104,9 @@ namespace proc { }; /** - * Calculate a stable id based on name and image data - * @return tuple of id calculated without index (for use if no collision) and one with -*/ + * Calculate a stable id based on name and image data + * @return tuple of id calculated without index (for use if no collision) and one with + */ std::tuple calculate_app_id(const std::string &app_name, std::string app_image_path, int index); @@ -114,6 +117,8 @@ namespace proc { std::optional parse(const std::string &file_name); + std::unique_ptr + init(); + extern proc_t proc; } // namespace proc -#endif // SUNSHINE_PROCESS_H diff --git a/src/round_robin.h b/src/round_robin.h index 37e010331bf..658fd94e324 100644 --- a/src/round_robin.h +++ b/src/round_robin.h @@ -1,5 +1,8 @@ -#ifndef KITTY_UTIL_ITERATOR_H -#define KITTY_UTIL_ITERATOR_H +/** + * @file src/round_robin.h + * @brief todo + */ +#pragma once #include @@ -11,7 +14,9 @@ namespace round_robin_util { using value_type = V; using difference_type = V; using pointer = V *; + using const_pointer = V const *; using reference = V &; + using const_reference = V const &; typedef T iterator; typedef std::ptrdiff_t diff_t; @@ -90,12 +95,12 @@ namespace round_robin_util { reference operator*() { return *_this().get(); } - const reference + const_reference operator*() const { return *_this().get(); } pointer operator->() { return &*_this(); } - const pointer + const_pointer operator->() const { return &*_this(); } bool @@ -180,5 +185,3 @@ namespace round_robin_util { return round_robin_t(begin, end); } } // namespace round_robin_util - -#endif diff --git a/src/rtsp.cpp b/src/rtsp.cpp index 98606be93d6..a2e4a2fc3b1 100644 --- a/src/rtsp.cpp +++ b/src/rtsp.cpp @@ -1,5 +1,7 @@ -// Created by loki on 2/2/20. - +/** + * @file src/rstp.cpp + * @brief todo + */ #define BOOST_BIND_GLOBAL_PLACEHOLDERS extern "C" { @@ -19,6 +21,7 @@ extern "C" { #include "rtsp.h" #include "stream.h" #include "sync.h" +#include "video.h" #include @@ -489,7 +492,7 @@ namespace rtsp_stream { option.content = const_cast(seqn_str.c_str()); std::stringstream ss; - if (config::video.hevc_mode != 1) { + if (video::active_hevc_mode != 1) { ss << "sprop-parameter-sets=AAAAAU"sv << std::endl; } @@ -500,10 +503,10 @@ namespace rtsp_stream { auto mapping_p = stream_config.mapping; /** - * GFE advertises incorrect mapping for normal quality configurations, - * as a result, Moonlight rotates all channels from index '3' to the right - * To work around this, rotate channels to the left from index '3' - */ + * GFE advertises incorrect mapping for normal quality configurations, + * as a result, Moonlight rotates all channels from index '3' to the right + * To work around this, rotate channels to the left from index '3' + */ if (x == audio::SURROUND51 || x == audio::SURROUND71) { std::copy_n(mapping_p, stream_config.channelCount, mapping); std::rotate(mapping + 3, mapping + 4, mapping + audio::MAX_STREAM_CONFIG); @@ -690,7 +693,7 @@ namespace rtsp_stream { } } - if (config.monitor.videoFormat != 0 && config::video.hevc_mode == 1) { + if (config.monitor.videoFormat != 0 && video::active_hevc_mode == 1) { BOOST_LOG(warning) << "HEVC is disabled, yet the client requested HEVC"sv; respond(sock, &option, 400, "BAD REQUEST", req->sequenceNumber, {}); diff --git a/src/rtsp.h b/src/rtsp.h index f33030a0515..2b0355fd947 100644 --- a/src/rtsp.h +++ b/src/rtsp.h @@ -1,7 +1,8 @@ -// Created by loki on 2/2/20. - -#ifndef SUNSHINE_RTSP_H -#define SUNSHINE_RTSP_H +/** + * @file src/rtsp.h + * @brief todo + */ +#pragma once #include @@ -27,5 +28,3 @@ namespace rtsp_stream { rtpThread(); } // namespace rtsp_stream - -#endif // SUNSHINE_RTSP_H diff --git a/src/stat_trackers.cpp b/src/stat_trackers.cpp new file mode 100644 index 00000000000..4496ebac72a --- /dev/null +++ b/src/stat_trackers.cpp @@ -0,0 +1,14 @@ +/** + * @file src/stat_trackers.cpp + * @brief todo + */ +#include "stat_trackers.h" + +namespace stat_trackers { + + boost::format + one_digit_after_decimal() { + return boost::format("%1$.1f"); + } + +} // namespace stat_trackers diff --git a/src/stat_trackers.h b/src/stat_trackers.h new file mode 100644 index 00000000000..c26c8f455f6 --- /dev/null +++ b/src/stat_trackers.h @@ -0,0 +1,45 @@ +/** + * @file src/stat_trackers.h + * @brief todo + */ +#pragma once + +#include +#include +#include + +#include + +namespace stat_trackers { + + boost::format + one_digit_after_decimal(); + + template + class min_max_avg_tracker { + public: + using callback_function = std::function; + + void + collect_and_callback_on_interval(T stat, const callback_function &callback, std::chrono::seconds interval_in_seconds) { + if (std::chrono::steady_clock::now() > data.last_callback_time + interval_in_seconds) { + callback(data.stat_min, data.stat_max, data.stat_total / data.calls); + data = {}; + } + data.stat_min = std::min(data.stat_min, stat); + data.stat_max = std::max(data.stat_max, stat); + data.stat_total += stat; + data.calls += 1; + } + + private: + struct { + std::chrono::steady_clock::steady_clock::time_point last_callback_time = std::chrono::steady_clock::now(); + T stat_min = std::numeric_limits::max(); + T stat_max = 0; + double stat_total = 0; + uint32_t calls = 0; + } data; + }; + +} // namespace stat_trackers diff --git a/src/stream.cpp b/src/stream.cpp index 654905934cb..18c1473cdad 100644 --- a/src/stream.cpp +++ b/src/stream.cpp @@ -1,5 +1,7 @@ -// Created by loki on 6/5/19. - +/** + * @file src/stream.cpp + * @brief todo + */ #include "process.h" #include @@ -8,6 +10,8 @@ #include #include +#include + extern "C" { #include #include @@ -18,6 +22,7 @@ extern "C" { #include "input.h" #include "main.h" #include "network.h" +#include "stat_trackers.h" #include "stream.h" #include "sync.h" #include "thread_safe.h" @@ -43,7 +48,7 @@ static const short packetTypes[] = { 0x0204, // Frame Stats (unused) 0x0206, // Input data 0x010b, // Rumble data - 0x0100, // Termination + 0x0109, // Termination 0x0200, // Periodic Ping 0x0302, // IDR frame 0x0001, // fully encrypted @@ -74,7 +79,11 @@ namespace stream { } std::uint8_t headerType; // Always 0x01 for short headers - std::uint8_t unknown[2]; + + // Sunshine extension + // Frame processing latency, in 1/10 ms units + // zero when the frame is repeated or there is no backend implementation + boost::endian::little_uint16_at frame_processing_latency; // Currently known values: // 1 = Normal P-frame @@ -352,11 +361,11 @@ namespace stream { }; /** - * First part of cipher must be struct of type control_encrypted_t - * - * returns empty string_view on failure - * returns string_view pointing to payload data - */ + * First part of cipher must be struct of type control_encrypted_t + * + * returns empty string_view on failure + * returns string_view pointing to payload data + */ template static inline std::string_view encode_control(session_t *session, const std::string_view &plaintext, std::array &tagged_cipher) { @@ -818,6 +827,8 @@ namespace stream { auto shutdown_event = mail::man->event(mail::broadcast_shutdown); while (!shutdown_event->peek()) { + bool has_session_awaiting_peer = false; + { auto lg = server->_map_addr_session.lock(); @@ -843,6 +854,13 @@ namespace stream { continue; } + // Remember if we have a session that's waiting for a peer to connect to the + // control stream. This ensures the clients are properly notified even when + // the app terminates before they finish connecting. + if (!session->control.peer) { + has_session_awaiting_peer = true; + } + auto &rumble_queue = session->control.rumble_queue; while (rumble_queue->peek()) { auto rumble = rumble_queue->pop(); @@ -864,9 +882,9 @@ namespace stream { }) } - if (proc::proc.running() == 0) { - BOOST_LOG(debug) << "Process terminated"sv; - + // Don't break until any pending sessions either expire or connect + if (proc::proc.running() == 0 && !has_session_awaiting_peer) { + BOOST_LOG(info) << "Process terminated"sv; break; } @@ -880,7 +898,7 @@ namespace stream { control_terminate_t plaintext; plaintext.header.type = packetTypes[IDX_TERMINATION]; plaintext.header.payloadLength = sizeof(plaintext.ec); - plaintext.ec = reason; + plaintext.ec = util::endian::big(reason); std::array @@ -890,11 +908,14 @@ namespace stream { for (auto pos = std::begin(*server->_map_addr_session); pos != std::end(*server->_map_addr_session); ++pos) { auto session = pos->second.second; - auto payload = encode_control(session, util::view(plaintext), encrypted_payload); + // We may not have gotten far enough to have an ENet connection yet + if (session->control.peer) { + auto payload = encode_control(session, util::view(plaintext), encrypted_payload); - if (server->send(payload, session->control.peer)) { - TUPLE_2D(port, addr, platf::from_sockaddr_ex((sockaddr *) &session->control.peer->address.address)); - BOOST_LOG(warning) << "Couldn't send termination code to ["sv << addr << ':' << port << ']'; + if (server->send(payload, session->control.peer)) { + TUPLE_2D(port, addr, platf::from_sockaddr_ex((sockaddr *) &session->control.peer->address.address)); + BOOST_LOG(warning) << "Couldn't send termination code to ["sv << addr << ':' << port << ']'; + } } session->shutdown_event->raise(true); @@ -997,6 +1018,8 @@ namespace stream { // Video traffic is sent on this thread platf::adjust_thread_priority(platf::thread_priority_e::high); + stat_trackers::min_max_avg_tracker frame_processing_latency_tracker; + while (auto packet = packets->pop()) { if (shutdown_event->peek()) { break; @@ -1013,6 +1036,29 @@ namespace stream { frame_header.headerType = 0x01; // Short header type frame_header.frameType = (av_packet->flags & AV_PKT_FLAG_KEY) ? 2 : 1; + if (packet->frame_timestamp) { + auto duration_to_latency = [](const std::chrono::steady_clock::duration &duration) { + const auto duration_us = std::chrono::duration_cast(duration).count(); + return (uint16_t) std::clamp((duration_us + 50) / 100, 0, std::numeric_limits::max()); + }; + + uint16_t latency = duration_to_latency(std::chrono::steady_clock::now() - *packet->frame_timestamp); + + if (config::sunshine.min_log_level <= 1) { + // Print frame processing latency stats to debug log every 20 seconds + auto print_info = [&](uint16_t min_latency, uint16_t max_latency, double avg_latency) { + auto f = stat_trackers::one_digit_after_decimal(); + BOOST_LOG(debug) << "Frame processing latency (min/max/avg): " << f % (min_latency / 10.) << "ms/" << f % (max_latency / 10.) << "ms/" << f % (avg_latency / 10.) << "ms"; + }; + frame_processing_latency_tracker.collect_and_callback_on_interval(latency, print_info, 20s); + } + + frame_header.frame_processing_latency = latency; + } + else { + frame_header.frame_processing_latency = 0; + } + std::copy_n((uint8_t *) &frame_header, sizeof(frame_header), std::back_inserter(payload_new)); std::copy(std::begin(payload), std::end(payload), std::back_inserter(payload_new)); @@ -1429,8 +1475,8 @@ namespace stream { // Enable QoS tagging on video traffic if requested by the client if (session->config.videoQosType) { auto address = session->video.peer.address(); - session->video.qos = std::move(platf::enable_socket_qos(ref->video_sock.native_handle(), address, - session->video.peer.port(), platf::qos_data_type_e::video)); + session->video.qos = platf::enable_socket_qos(ref->video_sock.native_handle(), address, + session->video.peer.port(), platf::qos_data_type_e::video); } BOOST_LOG(debug) << "Start capturing Video"sv; @@ -1454,8 +1500,8 @@ namespace stream { // Enable QoS tagging on audio traffic if requested by the client if (session->config.audioQosType) { auto address = session->audio.peer.address(); - session->audio.qos = std::move(platf::enable_socket_qos(ref->audio_sock.native_handle(), address, - session->audio.peer.port(), platf::qos_data_type_e::audio)); + session->audio.qos = platf::enable_socket_qos(ref->audio_sock.native_handle(), address, + session->audio.peer.port(), platf::qos_data_type_e::audio); } BOOST_LOG(debug) << "Start capturing Audio"sv; diff --git a/src/stream.h b/src/stream.h index c8803f3ce88..302b9c0e64e 100644 --- a/src/stream.h +++ b/src/stream.h @@ -1,7 +1,8 @@ -// Created by loki on 6/5/19. - -#ifndef SUNSHINE_STREAM_H -#define SUNSHINE_STREAM_H +/** + * @file src/stream.h + * @brief todo + */ +#pragma once #include @@ -49,5 +50,3 @@ namespace stream { state(session_t &session); } // namespace session } // namespace stream - -#endif // SUNSHINE_STREAM_H diff --git a/src/sync.h b/src/sync.h index 9deeaa271d8..5b64606e42e 100644 --- a/src/sync.h +++ b/src/sync.h @@ -1,7 +1,8 @@ -// Created by loki on 16-4-19. - -#ifndef SUNSHINE_SYNC_H -#define SUNSHINE_SYNC_H +/** + * @file src/sync.h + * @brief todo + */ +#pragma once #include #include @@ -98,5 +99,3 @@ namespace sync_util { }; } // namespace sync_util - -#endif // SUNSHINE_SYNC_H diff --git a/src/system_tray.cpp b/src/system_tray.cpp index 4b10acffcaf..ed66357a6ac 100644 --- a/src/system_tray.cpp +++ b/src/system_tray.cpp @@ -1,10 +1,14 @@ /** - * @file system_tray.cpp + * @file src/system_tray.cpp + * @brief todo */ // macros #if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 - #if defined(_WIN32) || defined(_WIN64) + #if defined(_WIN32) + #define WIN32_LEAN_AND_MEAN + #include + #include #define TRAY_ICON WEB_DIR "images/favicon.ico" #elif defined(__linux__) || defined(linux) || defined(__linux) #define TRAY_ICON "sunshine" @@ -34,107 +38,85 @@ using namespace std::literals; namespace system_tray { /** - * @brief Open a url in the default web browser. - * @param url The url to open. - */ - void - open_url(const std::string &url) { - boost::filesystem::path working_dir; - - // if windows - #if defined(_WIN32) || defined(_WIN64) - // set working dir to Windows system directory - working_dir = boost::filesystem::path(std::getenv("SystemRoot")); - - // this isn't ideal as it briefly shows a command window - // but start a command built into cmd, not an executable - std::string cmd = R"(cmd /C "start )" + url + R"(")"; - #elif defined(__linux__) || defined(linux) || defined(__linux) - // set working dir to user home directory - working_dir = boost::filesystem::path(std::getenv("HOME")); - std::string cmd = R"(xdg-open ")" + url + R"(")"; - #elif defined(__APPLE__) || defined(__MACH__) - std::string cmd = R"(open ")" + url + R"(")"; - #endif - - boost::process::environment _env = boost::this_process::environment(); - std::error_code ec; - auto child = platf::run_unprivileged(cmd, working_dir, _env, nullptr, ec, nullptr); - if (ec) { - BOOST_LOG(warning) << "Couldn't open url ["sv << url << "]: System: "sv << ec.message(); - } - else { - BOOST_LOG(info) << "Opened url ["sv << url << "]"sv; - child.detach(); - } - } - - /** - * @brief Callback for opening the UI from the system tray. - * @param item The tray menu item. - */ + * @brief Callback for opening the UI from the system tray. + * @param item The tray menu item. + */ void tray_open_ui_cb(struct tray_menu *item) { BOOST_LOG(info) << "Opening UI from system tray"sv; - - // create the url with the port - std::string url = "https://localhost:" + std::to_string(map_port(confighttp::PORT_HTTPS)); - - // open the url in the default web browser - open_url(url); + launch_ui(); } /** - * @brief Callback for opening GitHub Sponsors from the system tray. - * @param item The tray menu item. - */ + * @brief Callback for opening GitHub Sponsors from the system tray. + * @param item The tray menu item. + */ void tray_donate_github_cb(struct tray_menu *item) { - open_url("https://github.com/sponsors/LizardByte"); + platf::open_url("https://github.com/sponsors/LizardByte"); } /** - * @brief Callback for opening MEE6 donation from the system tray. - * @param item The tray menu item. - */ + * @brief Callback for opening MEE6 donation from the system tray. + * @param item The tray menu item. + */ void tray_donate_mee6_cb(struct tray_menu *item) { - open_url("https://mee6.xyz/m/804382334370578482"); + platf::open_url("https://mee6.xyz/m/804382334370578482"); } /** - * @brief Callback for opening Patreon from the system tray. - * @param item The tray menu item. - */ + * @brief Callback for opening Patreon from the system tray. + * @param item The tray menu item. + */ void tray_donate_patreon_cb(struct tray_menu *item) { - open_url("https://www.patreon.com/LizardByte"); + platf::open_url("https://www.patreon.com/LizardByte"); } /** - * @brief Callback for opening PayPal donation from the system tray. - * @param item The tray menu item. - */ + * @brief Callback for opening PayPal donation from the system tray. + * @param item The tray menu item. + */ void tray_donate_paypal_cb(struct tray_menu *item) { - open_url("https://www.paypal.com/paypalme/ReenigneArcher"); + platf::open_url("https://www.paypal.com/paypalme/ReenigneArcher"); } /** - * @brief Callback for exiting Sunshine from the system tray. - * @param item The tray menu item. - */ + * @brief Callback for restarting Sunshine from the system tray. + * @param item The tray menu item. + */ + void + tray_restart_cb(struct tray_menu *item) { + BOOST_LOG(info) << "Restarting from system tray"sv; + + platf::restart(); + } + + /** + * @brief Callback for exiting Sunshine from the system tray. + * @param item The tray menu item. + */ void tray_quit_cb(struct tray_menu *item) { BOOST_LOG(info) << "Quiting from system tray"sv; - std::raise(SIGINT); + #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, false); + } + #endif + + lifetime::exit_sunshine(0, false); } // Tray menu static struct tray tray = { .icon = TRAY_ICON, - #if defined(_WIN32) || defined(_WIN64) + #if defined(_WIN32) .tooltip = const_cast("Sunshine"), // cast the string literal to a non-const char* pointer #endif .menu = @@ -151,17 +133,93 @@ namespace system_tray { { .text = "PayPal", .cb = tray_donate_paypal_cb }, { .text = nullptr } } }, { .text = "-" }, + { .text = "Restart", .cb = tray_restart_cb }, { .text = "Quit", .cb = tray_quit_cb }, { .text = nullptr } }, }; /** - * @brief Create the system tray. - * @details This function has an endless loop, so it should be run in a separate thread. - * @return 1 if the system tray failed to create, otherwise 0 once the tray has been terminated. - */ + * @brief Create the system tray. + * @details This function has an endless loop, so it should be run in a separate thread. + * @return 1 if the system tray failed to create, otherwise 0 once the tray has been terminated. + */ int system_tray() { + #ifdef _WIN32 + // If we're running as SYSTEM, Explorer.exe will not have permission to open our thread handle + // to monitor for thread termination. If Explorer fails to open our thread, our tray icon + // will persist forever if we terminate unexpectedly. To avoid this, we will modify our thread + // DACL to add an ACE that allows SYNCHRONIZE access to Everyone. + { + PACL old_dacl; + PSECURITY_DESCRIPTOR sd; + auto error = GetSecurityInfo(GetCurrentThread(), + SE_KERNEL_OBJECT, + DACL_SECURITY_INFORMATION, + nullptr, + nullptr, + &old_dacl, + nullptr, + &sd); + if (error != ERROR_SUCCESS) { + BOOST_LOG(warning) << "GetSecurityInfo() failed: "sv << error; + return 1; + } + + auto free_sd = util::fail_guard([sd]() { + LocalFree(sd); + }); + + SID_IDENTIFIER_AUTHORITY sid_authority = SECURITY_WORLD_SID_AUTHORITY; + PSID world_sid; + if (!AllocateAndInitializeSid(&sid_authority, 1, SECURITY_WORLD_RID, 0, 0, 0, 0, 0, 0, 0, &world_sid)) { + error = GetLastError(); + BOOST_LOG(warning) << "AllocateAndInitializeSid() failed: "sv << error; + return 1; + } + + auto free_sid = util::fail_guard([world_sid]() { + FreeSid(world_sid); + }); + + EXPLICIT_ACCESS ea {}; + ea.grfAccessPermissions = SYNCHRONIZE; + ea.grfAccessMode = GRANT_ACCESS; + ea.grfInheritance = NO_INHERITANCE; + ea.Trustee.TrusteeForm = TRUSTEE_IS_SID; + ea.Trustee.ptstrName = (LPSTR) world_sid; + + PACL new_dacl; + error = SetEntriesInAcl(1, &ea, old_dacl, &new_dacl); + if (error != ERROR_SUCCESS) { + BOOST_LOG(warning) << "SetEntriesInAcl() failed: "sv << error; + return 1; + } + + auto free_new_dacl = util::fail_guard([new_dacl]() { + LocalFree(new_dacl); + }); + + error = SetSecurityInfo(GetCurrentThread(), + SE_KERNEL_OBJECT, + DACL_SECURITY_INFORMATION, + nullptr, + nullptr, + new_dacl, + nullptr); + if (error != ERROR_SUCCESS) { + BOOST_LOG(warning) << "SetSecurityInfo() failed: "sv << error; + return 1; + } + } + + // Wait for the shell to be initialized before registering the tray icon. + // This ensures the tray icon works reliably after a logoff/logon cycle. + while (GetShellWindow() == nullptr) { + Sleep(1000); + } + #endif + if (tray_init(&tray) < 0) { BOOST_LOG(warning) << "Failed to create system tray"sv; return 1; @@ -178,9 +236,9 @@ namespace system_tray { } /** - * @brief Run the system tray with platform specific options. - * @note macOS requires that UI elements be created on the main thread, so the system tray is not implemented for macOS. - */ + * @brief Run the system tray with platform specific options. + * @note macOS requires that UI elements be created on the main thread, so the system tray is not currently implemented for macOS. + */ void run_tray() { // create the system tray @@ -201,9 +259,9 @@ namespace system_tray { } /** - * @brief Exit the system tray. - * @return 0 after exiting the system tray. - */ + * @brief Exit the system tray. + * @return 0 after exiting the system tray. + */ int end_tray() { tray_exit(); diff --git a/src/system_tray.h b/src/system_tray.h index 46849883381..18e3445f6bc 100644 --- a/src/system_tray.h +++ b/src/system_tray.h @@ -1,14 +1,13 @@ /** -* @file system_tray.h -*/ -// macros -#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 + * @file src/system_tray.h + * @brief todo + */ + +#pragma once // system_tray namespace namespace system_tray { - void - open_url(const std::string &url); void tray_open_ui_cb(struct tray_menu *item); void @@ -30,4 +29,3 @@ namespace system_tray { end_tray(); } // namespace system_tray -#endif diff --git a/src/task_pool.h b/src/task_pool.h index 193f8f238f9..8da85ed0ac9 100644 --- a/src/task_pool.h +++ b/src/task_pool.h @@ -1,5 +1,8 @@ -#ifndef KITTY_TASK_POOL_H -#define KITTY_TASK_POOL_H +/** + * @file src/task_pool.h + * @brief todo + */ +#pragma once #include #include @@ -111,8 +114,8 @@ namespace task_pool_util { } /** - * @return an id to potentially delay the task - */ + * @return an id to potentially delay the task. + */ template auto pushDelayed(Function &&newTask, std::chrono::duration duration, Args &&...args) { @@ -146,8 +149,9 @@ namespace task_pool_util { } /** - * @param duration The delay before executing the task - */ + * @param task_id The id of the task to delay. + * @param duration The delay before executing the task. + */ template void delay(task_id_t task_id, std::chrono::duration duration) { @@ -218,14 +222,13 @@ namespace task_pool_util { if (!_tasks.empty()) { __task task = std::move(_tasks.front()); _tasks.pop_front(); - return std::move(task); + return task; } if (!_timer_tasks.empty() && std::get<0>(_timer_tasks.back()) <= std::chrono::steady_clock::now()) { __task task = std::move(std::get<1>(_timer_tasks.back())); _timer_tasks.pop_back(); - - return std::move(task); + return task; } return std::nullopt; @@ -257,4 +260,3 @@ namespace task_pool_util { } }; } // namespace task_pool_util -#endif diff --git a/src/thread_pool.h b/src/thread_pool.h index 5de54758856..c97f29a1fed 100644 --- a/src/thread_pool.h +++ b/src/thread_pool.h @@ -1,14 +1,16 @@ -#ifndef KITTY_THREAD_POOL_H -#define KITTY_THREAD_POOL_H +/** + * @file src/thread_pool.h + * @brief todo + */ +#pragma once #include "task_pool.h" #include namespace thread_pool_util { - /* - * Allow threads to execute unhindered - * while keeping full control over the threads. - */ + /** + * Allow threads to execute unhindered while keeping full control over the threads. + */ class ThreadPool: public task_pool_util::TaskPool { public: typedef TaskPool::__task __task; @@ -127,4 +129,3 @@ namespace thread_pool_util { } }; } // namespace thread_pool_util -#endif diff --git a/src/thread_safe.h b/src/thread_safe.h index 1f29761f1e2..d745bf0886f 100644 --- a/src/thread_safe.h +++ b/src/thread_safe.h @@ -1,8 +1,10 @@ -// Created by loki on 6/10/19. - -#ifndef SUNSHINE_THREAD_SAFE_H -#define SUNSHINE_THREAD_SAFE_H +/** + * @file src/thread_safe.h + * @brief todo + */ +#pragma once +#include #include #include #include @@ -99,6 +101,25 @@ namespace safe { return _status; } + // pop and view shoud not be used interchangeably + template + status_t + view(std::chrono::duration delay) { + std::unique_lock ul { _lock }; + + if (!_continue) { + return util::false_v; + } + + while (!_status) { + if (!_continue || _cv.wait_for(ul, delay) == std::cv_status::timeout) { + return util::false_v; + } + } + + return _status; + } + bool peek() { return _continue && (bool) _status; @@ -140,14 +161,12 @@ namespace safe { public: using status_t = util::optional_t; - alarm_raw_t(): - _status { util::false_v } {} - void ring(const status_t &status) { std::lock_guard lg(_lock); _status = status; + _rang = true; _cv.notify_one(); } @@ -156,6 +175,7 @@ namespace safe { std::lock_guard lg(_lock); _status = std::move(status); + _rang = true; _cv.notify_one(); } @@ -164,7 +184,7 @@ namespace safe { wait_for(const std::chrono::duration &rel_time) { std::unique_lock ul(_lock); - return _cv.wait_for(ul, rel_time, [this]() { return (bool) status(); }); + return _cv.wait_for(ul, rel_time, [this]() { return _rang; }); } template @@ -172,7 +192,7 @@ namespace safe { wait_for(const std::chrono::duration &rel_time, Pred &&pred) { std::unique_lock ul(_lock); - return _cv.wait_for(ul, rel_time, [this, &pred]() { return (bool) status() || pred(); }); + return _cv.wait_for(ul, rel_time, [this, &pred]() { return _rang || pred(); }); } template @@ -180,7 +200,7 @@ namespace safe { wait_until(const std::chrono::duration &rel_time) { std::unique_lock ul(_lock); - return _cv.wait_until(ul, rel_time, [this]() { return (bool) status(); }); + return _cv.wait_until(ul, rel_time, [this]() { return _rang; }); } template @@ -188,20 +208,20 @@ namespace safe { wait_until(const std::chrono::duration &rel_time, Pred &&pred) { std::unique_lock ul(_lock); - return _cv.wait_until(ul, rel_time, [this, &pred]() { return (bool) status() || pred(); }); + return _cv.wait_until(ul, rel_time, [this, &pred]() { return _rang || pred(); }); } auto wait() { std::unique_lock ul(_lock); - _cv.wait(ul, [this]() { return (bool) status(); }); + _cv.wait(ul, [this]() { return _rang; }); } template auto wait(Pred &&pred) { std::unique_lock ul(_lock); - _cv.wait(ul, [this, &pred]() { return (bool) status() || pred(); }); + _cv.wait(ul, [this, &pred]() { return _rang || pred(); }); } const status_t & @@ -217,13 +237,15 @@ namespace safe { void reset() { _status = status_t {}; + _rang = false; } private: std::mutex _lock; std::condition_variable _cv; - status_t _status; + status_t _status { util::false_v }; + bool _rang { false }; }; template @@ -553,5 +575,3 @@ namespace safe { mail->cleanup(); } } // namespace safe - -#endif // SUNSHINE_THREAD_SAFE_H diff --git a/src/upnp.cpp b/src/upnp.cpp index 7a0a921392c..6fc5a1309a7 100644 --- a/src/upnp.cpp +++ b/src/upnp.cpp @@ -1,3 +1,7 @@ +/** + * @file src/upnp.cpp + * @brief todo + */ #include #include @@ -16,6 +20,9 @@ using namespace std::literals; namespace upnp { constexpr auto INET6_ADDRESS_STRLEN = 46; + constexpr auto PORT_MAPPING_LIFETIME = 3600s; + constexpr auto REFRESH_INTERVAL = 120s; + constexpr auto IPv4 = 0; constexpr auto IPv6 = 1; @@ -29,50 +36,10 @@ namespace upnp { struct { std::string wan; std::string lan; + std::string proto; } port; std::string description; - bool tcp; - }; - - void - unmap( - const urls_t &urls, - const IGDdatas &data, - std::vector::const_reverse_iterator begin, - std::vector::const_reverse_iterator end) { - BOOST_LOG(debug) << "Unmapping UPNP ports"sv; - - for (auto it = begin; it != end; ++it) { - auto status = UPNP_DeletePortMapping( - urls->controlURL, - data.first.servicetype, - it->port.wan.c_str(), - it->tcp ? "TCP" : "UDP", - nullptr); - - if (status) { - BOOST_LOG(warning) << "Failed to unmap port ["sv << it->port.wan << "] to port ["sv << it->port.lan << "]: error code ["sv << status << ']'; - break; - } - } - } - - class deinit_t: public platf::deinit_t { - public: - using iter_t = std::vector::const_reverse_iterator; - deinit_t(urls_t &&urls, IGDdatas data, std::vector &&mapping): - urls { std::move(urls) }, data { data }, mapping { std::move(mapping) } {} - - ~deinit_t() { - BOOST_LOG(info) << "Unmapping UPNP ports..."sv; - unmap(urls, data, std::rbegin(mapping), std::rend(mapping)); - } - - urls_t urls; - IGDdatas data; - - std::vector mapping; }; static std::string_view @@ -91,98 +58,239 @@ namespace upnp { return "Unknown status"sv; } - std::unique_ptr - start() { - if (!config::sunshine.flags[config::flag::UPNP]) { - return nullptr; - } - - int err {}; - - device_t device { upnpDiscover(2000, nullptr, nullptr, 0, IPv4, 2, &err) }; - if (!device || err) { - BOOST_LOG(error) << "Couldn't discover any UPNP devices"sv; - - return nullptr; - } + class deinit_t: public platf::deinit_t { + public: + deinit_t() { + auto rtsp = std::to_string(::map_port(rtsp_stream::RTSP_SETUP_PORT)); + auto video = std::to_string(::map_port(stream::VIDEO_STREAM_PORT)); + auto audio = std::to_string(::map_port(stream::AUDIO_STREAM_PORT)); + auto control = std::to_string(::map_port(stream::CONTROL_PORT)); + auto gs_http = std::to_string(::map_port(nvhttp::PORT_HTTP)); + auto gs_https = std::to_string(::map_port(nvhttp::PORT_HTTPS)); + auto wm_http = std::to_string(::map_port(confighttp::PORT_HTTPS)); + + mappings.assign({ + { { rtsp, rtsp, "TCP"s }, "Sunshine - RTSP"s }, + { { video, video, "UDP"s }, "Sunshine - Video"s }, + { { audio, audio, "UDP"s }, "Sunshine - Audio"s }, + { { control, control, "UDP"s }, "Sunshine - Control"s }, + { { gs_http, gs_http, "TCP"s }, "Sunshine - Client HTTP"s }, + { { gs_https, gs_https, "TCP"s }, "Sunshine - Client HTTPS"s }, + }); + + // Only map port for the Web Manager if it is configured to accept connection from WAN + if (net::from_enum_string(config::nvhttp.origin_web_ui_allowed) > net::LAN) { + mappings.emplace_back(mapping_t { { wm_http, wm_http, "TCP"s }, "Sunshine - Web UI"s }); + } - for (auto dev = device.get(); dev != nullptr; dev = dev->pNext) { - BOOST_LOG(debug) << "Found device: "sv << dev->descURL; + // Start the mapping thread + upnp_thread = std::thread { &deinit_t::upnp_thread_proc, this }; } - std::array lan_addr; - std::array wan_addr; - - urls_t urls; - IGDdatas data; - - auto status = UPNP_GetValidIGD(device.get(), &urls.el, &data, lan_addr.data(), lan_addr.size()); - if (status != 1 && status != 2) { - BOOST_LOG(error) << status_string(status); - return nullptr; + ~deinit_t() { + upnp_thread.join(); } - BOOST_LOG(debug) << "Found valid IGD device: "sv << urls->rootdescURL; - - if (UPNP_GetExternalIPAddress(urls->controlURL, data.first.servicetype, wan_addr.data())) { - BOOST_LOG(warning) << "Could not get external ip"sv; - } - else { - BOOST_LOG(debug) << "Found external ip: "sv << wan_addr.data(); - if (config::nvhttp.external_ip.empty()) { - config::nvhttp.external_ip = wan_addr.data(); + /** + * @brief Maps a port via UPnP. + * @param data IGDdatas from UPNP_GetValidIGD() + * @param urls urls_t from UPNP_GetValidIGD() + * @param lan_addr Local IP address to map to + * @param mapping Information about port to map + * @return `true` on success. + */ + bool + map_port(const IGDdatas &data, const urls_t &urls, const std::string &lan_addr, const mapping_t &mapping) { + char intClient[16]; + char intPort[6]; + char desc[80]; + char enabled[4]; + char leaseDuration[16]; + bool indefinite = false; + + // First check if this port is already mapped successfully + BOOST_LOG(debug) << "Checking for existing UPnP port mapping for "sv << mapping.port.wan; + auto err = UPNP_GetSpecificPortMappingEntry( + urls->controlURL, + data.first.servicetype, + // In params + mapping.port.wan.c_str(), + mapping.port.proto.c_str(), + nullptr, + // Out params + intClient, intPort, desc, enabled, leaseDuration); + if (err == 714) { // NoSuchEntryInArray + BOOST_LOG(debug) << "Mapping entry not found for "sv << mapping.port.wan; + } + else if (err == UPNPCOMMAND_SUCCESS) { + // Some routers change the description, so we can't check that here + if (!std::strcmp(intClient, lan_addr.c_str())) { + if (std::atoi(leaseDuration) == 0) { + BOOST_LOG(debug) << "Static mapping entry found for "sv << mapping.port.wan; + + // It's a static mapping, so we're done here + return true; + } + else { + BOOST_LOG(debug) << "Mapping entry found for "sv << mapping.port.wan << " ("sv << leaseDuration << " seconds remaining)"sv; + } + } + else { + BOOST_LOG(warning) << "UPnP conflict detected with: "sv << intClient; + + // Some UPnP IGDs won't let unauthenticated clients delete other conflicting port mappings + // for security reasons, but we will give it a try anyway. + err = UPNP_DeletePortMapping( + urls->controlURL, + data.first.servicetype, + mapping.port.wan.c_str(), + mapping.port.proto.c_str(), + nullptr); + if (err) { + BOOST_LOG(error) << "Unable to delete conflicting UPnP port mapping: "sv << err; + return false; + } + } + } + else { + BOOST_LOG(error) << "UPNP_GetSpecificPortMappingEntry() failed: "sv << err; + + // If we get a strange error from the router, we'll assume it's some old broken IGDv1 + // device and only use indefinite lease durations to hopefully avoid confusing it. + if (err != 606) { // Unauthorized + indefinite = true; + } } - } - - auto rtsp = std::to_string(map_port(rtsp_stream::RTSP_SETUP_PORT)); - auto video = std::to_string(map_port(stream::VIDEO_STREAM_PORT)); - auto audio = std::to_string(map_port(stream::AUDIO_STREAM_PORT)); - auto control = std::to_string(map_port(stream::CONTROL_PORT)); - auto gs_http = std::to_string(map_port(nvhttp::PORT_HTTP)); - auto gs_https = std::to_string(map_port(nvhttp::PORT_HTTPS)); - auto wm_http = std::to_string(map_port(confighttp::PORT_HTTPS)); - - std::vector mappings { - { rtsp, rtsp, "RTSP setup port"s, true }, - { video, video, "Video stream port"s, false }, - { audio, audio, "Control stream port"s, false }, - { control, control, "Audio stream port"s, false }, - { gs_http, gs_http, "Gamestream http port"s, true }, - { gs_https, gs_https, "Gamestream https port"s, true }, - }; - - // Only map port for the Web Manager if it is configured to accept connection from WAN - if (net::from_enum_string(config::nvhttp.origin_web_ui_allowed) > net::LAN) { - mappings.emplace_back(mapping_t { wm_http, wm_http, "Sunshine Web UI port"s, true }); - } - - auto it = std::begin(mappings); - status = 0; - for (; it != std::end(mappings); ++it) { - status = UPNP_AddPortMapping( + // Add/update the port mapping + auto mapping_period = std::to_string(indefinite ? 0 : PORT_MAPPING_LIFETIME.count()); + err = UPNP_AddPortMapping( urls->controlURL, data.first.servicetype, - it->port.wan.c_str(), - it->port.lan.c_str(), + mapping.port.wan.c_str(), + mapping.port.lan.c_str(), lan_addr.data(), - it->description.c_str(), - it->tcp ? "TCP" : "UDP", + mapping.description.c_str(), + mapping.port.proto.c_str(), nullptr, - "86400"); + mapping_period.c_str()); + + if (err != UPNPCOMMAND_SUCCESS && !indefinite) { + // This may be an old/broken IGD that doesn't like non-static mappings. + BOOST_LOG(debug) << "Trying static mapping after failure: "sv << err; + err = UPNP_AddPortMapping( + urls->controlURL, + data.first.servicetype, + mapping.port.wan.c_str(), + mapping.port.lan.c_str(), + lan_addr.data(), + mapping.description.c_str(), + mapping.port.proto.c_str(), + nullptr, + "0"); + } + + if (err) { + BOOST_LOG(error) << "Failed to map "sv << mapping.port.proto << ' ' << mapping.port.lan << ": "sv << err; + return false; + } + + BOOST_LOG(debug) << "Successfully mapped "sv << mapping.port.proto << ' ' << mapping.port.lan; + return true; + } - if (status) { - BOOST_LOG(error) << "Failed to map port ["sv << it->port.wan << "] to port ["sv << it->port.lan << "]: error code ["sv << status << ']'; - break; + /** + * @brief Unmaps all ports. + * @param data IGDdatas from UPNP_GetValidIGD() + * @param data urls_t from UPNP_GetValidIGD() + */ + void + unmap_all_ports(const urls_t &urls, const IGDdatas &data) { + for (auto it = std::begin(mappings); it != std::end(mappings); ++it) { + auto status = UPNP_DeletePortMapping( + urls->controlURL, + data.first.servicetype, + it->port.wan.c_str(), + it->port.proto.c_str(), + nullptr); + + if (status && status != 714) { // NoSuchEntryInArray + BOOST_LOG(warning) << "Failed to unmap "sv << it->port.proto << ' ' << it->port.lan << ": "sv << status; + } + else { + BOOST_LOG(debug) << "Successfully unmapped "sv << it->port.proto << ' ' << it->port.lan; + } } } - if (status) { - unmap(urls, data, std::make_reverse_iterator(it), std::rend(mappings)); + /** + * @brief Maintains UPnP port forwarding rules + */ + void + upnp_thread_proc() { + auto shutdown_event = mail::man->event(mail::shutdown); + bool mapped = false; + IGDdatas data; + urls_t mapped_urls; + + // Refresh UPnP rules every few minutes. They can be lost if the router reboots, + // WAN IP address changes, or various other conditions. + do { + int err = 0; + device_t device { upnpDiscover(2000, nullptr, nullptr, 0, IPv4, 2, &err) }; + if (!device || err) { + BOOST_LOG(warning) << "Couldn't discover any UPNP devices"sv; + mapped = false; + continue; + } + + for (auto dev = device.get(); dev != nullptr; dev = dev->pNext) { + BOOST_LOG(debug) << "Found device: "sv << dev->descURL; + } + + std::array lan_addr; + + urls_t urls; + auto status = UPNP_GetValidIGD(device.get(), &urls.el, &data, lan_addr.data(), lan_addr.size()); + if (status != 1 && status != 2) { + BOOST_LOG(error) << status_string(status); + mapped = false; + continue; + } + + std::string lan_addr_str { lan_addr.data() }; + + BOOST_LOG(debug) << "Found valid IGD device: "sv << urls->rootdescURL; + + for (auto it = std::begin(mappings); it != std::end(mappings) && !shutdown_event->peek(); ++it) { + map_port(data, urls, lan_addr_str, *it); + } + + if (!mapped) { + BOOST_LOG(info) << "Completed UPnP port mappings to "sv << lan_addr_str << " via "sv << urls->rootdescURL; + } + + mapped = true; + mapped_urls = std::move(urls); + } while (!shutdown_event->view(REFRESH_INTERVAL)); + + if (mapped) { + // Unmap ports upon termination + BOOST_LOG(info) << "Unmapping UPNP ports..."sv; + unmap_all_ports(mapped_urls, data); + } + } + + std::vector mappings; + std::thread upnp_thread; + }; + std::unique_ptr + start() { + if (!config::sunshine.flags[config::flag::UPNP]) { return nullptr; } - return std::make_unique(std::move(urls), data, std::move(mappings)); + return std::make_unique(); } } // namespace upnp diff --git a/src/upnp.h b/src/upnp.h index 43068dca699..73fc4f79b3f 100644 --- a/src/upnp.h +++ b/src/upnp.h @@ -1,5 +1,8 @@ -#ifndef SUNSHINE_UPNP_H -#define SUNSHINE_UPNP_H +/** + * @file src/upnp.h + * @brief todo + */ +#pragma once #include "platform/common.h" @@ -7,5 +10,3 @@ namespace upnp { [[nodiscard]] std::unique_ptr start(); } - -#endif diff --git a/src/utility.h b/src/utility.h index bb7f87a0bdf..3ed321203c2 100644 --- a/src/utility.h +++ b/src/utility.h @@ -1,5 +1,8 @@ -#ifndef UTILITY_H -#define UTILITY_H +/** + * @file src/utility.h + * @brief todo + */ +#pragma once #include #include @@ -487,6 +490,7 @@ namespace util { public: using element_type = T; using pointer = element_type *; + using const_pointer = element_type const *; using deleter_type = D; constexpr uniq_ptr() noexcept: @@ -560,12 +564,12 @@ namespace util { return _p; } - const pointer + const_pointer get() const { return _p; } - const std::add_lvalue_reference_t + std::add_lvalue_reference_t operator*() const { return *_p; } @@ -573,7 +577,7 @@ namespace util { operator*() { return *_p; } - const pointer + const_pointer operator->() const { return _p; } @@ -684,7 +688,9 @@ namespace util { public: using element_type = T; using pointer = element_type *; + using const_pointer = element_type const *; using reference = element_type &; + using const_reference = element_type const &; wrap_ptr(): _own_ptr { false }, _p { nullptr } {} @@ -741,7 +747,7 @@ namespace util { _own_ptr = false; } - const reference + const_reference operator*() const { return *_p; } @@ -749,7 +755,7 @@ namespace util { operator*() { return *_p; } - const pointer + const_pointer operator->() const { return _p; } @@ -1030,4 +1036,3 @@ namespace util { big(T x) { return endian_helper::big(x); } } // namespace endian } // namespace util -#endif diff --git a/src/uuid.h b/src/uuid.h index e1a893ef6c9..60c1b231ffb 100644 --- a/src/uuid.h +++ b/src/uuid.h @@ -1,7 +1,8 @@ -// Created by loki on 8-2-19. - -#ifndef T_MAN_UUID_H -#define T_MAN_UUID_H +/** + * @file src/uuid.h + * @brief todo + */ +#pragma once #include @@ -80,4 +81,3 @@ namespace uuid_util { } }; } // namespace uuid_util -#endif // T_MAN_UUID_H diff --git a/src/version.h.in b/src/version.h.in index 2be10bf6528..badb5aa0179 100644 --- a/src/version.h.in +++ b/src/version.h.in @@ -1,10 +1,12 @@ -#ifndef INCLUDE_GUARD -#define INCLUDE_GUARD +/** + * @file src/version.h.in + * @brief Version definitions for Sunshine. + * @note The final `version.h` is generated from this file during the CMake build. + */ +#pragma once #define PROJECT_NAME "@PROJECT_NAME@" #define PROJECT_VER "@PROJECT_VERSION@" #define PROJECT_VER_MAJOR "@PROJECT_VERSION_MAJOR@" #define PROJECT_VER_MINOR "@PROJECT_VERSION_MINOR@" #define PROJECT_VER_PATCH "@PROJECT_VERSION_PATCH@" - -#endif // INCLUDE_GUARD diff --git a/src/video.cpp b/src/video.cpp index 0ee04ff39aa..545e2fbdfea 100644 --- a/src/video.cpp +++ b/src/video.cpp @@ -1,7 +1,10 @@ -// Created by loki on 6/6/19. - +/** + * @file src/video.cpp + * @brief todo + */ #include #include +#include #include extern "C" { @@ -14,7 +17,6 @@ extern "C" { #include "input.h" #include "main.h" #include "platform/common.h" -#include "round_robin.h" #include "sync.h" #include "video.h" @@ -27,9 +29,6 @@ extern "C" { using namespace std::literals; namespace video { - constexpr auto hevc_nalu = "\000\000\000\001("sv; - constexpr auto h264_nalu = "\000\000\000\001e"sv; - void free_ctx(AVCodecContext *ctx) { avcodec_free_context(&ctx); @@ -141,7 +140,7 @@ namespace video { } int - set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) { + set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) override { this->frame = frame; // If it's a hwframe, allocate buffers for hardware @@ -166,8 +165,8 @@ namespace video { } /** - * When preserving aspect ratio, ensure that padding is black - */ + * When preserving aspect ratio, ensure that padding is black + */ int prefill() { auto frame = sw_frame ? sw_frame.get() : this->frame; @@ -277,12 +276,9 @@ namespace video { enum flag_e { PASSED, // Is supported REF_FRAMES_RESTRICT, // Set maximum reference frames - REF_FRAMES_AUTOSELECT, // Allow encoder to select maximum reference frames (If !REF_FRAMES_RESTRICT --> REF_FRAMES_AUTOSELECT) - SLICE, // Allow frame to be partitioned into multiple slices CBR, // Some encoders don't support CBR, if not supported --> attempt constant quantatication parameter instead DYNAMIC_RANGE, // hdr VUI_PARAMETERS, // AMD encoder with VAAPI doesn't add VUI parameters to SPS - NALU_PREFIX_5b, // libx264/libx265 have a 3-byte nalu prefix instead of 4-byte nalu prefix MAX_FLAGS }; @@ -294,12 +290,9 @@ namespace video { switch (flag) { _CONVERT(PASSED); _CONVERT(REF_FRAMES_RESTRICT); - _CONVERT(REF_FRAMES_AUTOSELECT); - _CONVERT(SLICE); _CONVERT(CBR); _CONVERT(DYNAMIC_RANGE); _CONVERT(VUI_PARAMETERS); - _CONVERT(NALU_PREFIX_5b); _CONVERT(MAX_FLAGS); } #undef _CONVERT @@ -403,7 +396,6 @@ namespace video { struct sync_session_t { sync_session_ctx_t *ctx; - platf::img_t *img_tmp; session_t session; }; @@ -668,7 +660,7 @@ namespace video { std::make_optional("qp"s, &config::video.qp), "h264_vaapi"s, }, - LIMITED_GOP_SIZE | PARALLEL_ENCODING | SINGLE_SLICE_ONLY, + LIMITED_GOP_SIZE | PARALLEL_ENCODING | SINGLE_SLICE_ONLY | NO_RC_BUF_LIMIT, vaapi_make_hwdevice_ctx }; @@ -710,23 +702,26 @@ namespace video { }; #endif - static std::vector encoders { + static const std::vector encoders { #ifndef __APPLE__ - nvenc, + &nvenc, #endif #ifdef _WIN32 - quicksync, - amdvce, + &quicksync, + &amdvce, #endif #ifdef __linux__ - vaapi, + &vaapi, #endif #ifdef __APPLE__ - videotoolbox, + &videotoolbox, #endif - software + &software }; + static encoder_t *chosen_encoder; + int active_hevc_mode; + void reset_display(std::shared_ptr &disp, AVHWDeviceType type, const std::string &display_name, const config_t &config) { // We try this twice, in case we still get an error on reinitialization @@ -781,9 +776,12 @@ namespace video { } } - if (auto capture_ctx = capture_ctx_queue->pop()) { - capture_ctxs.emplace_back(std::move(*capture_ctx)); + // Wait for the initial capture context or a request to stop the queue + auto initial_capture_ctx = capture_ctx_queue->pop(); + if (!initial_capture_ctx) { + return; } + capture_ctxs.emplace_back(std::move(*initial_capture_ctx)); auto disp = platf::display(map_base_dev_type(encoder.base_dev_type), display_names[display_p], capture_ctxs.front().config); if (!disp) { @@ -791,16 +789,101 @@ namespace video { } display_wp = disp; - std::vector> imgs(12); - auto round_robin = round_robin_util::make_round_robin>(std::begin(imgs), std::end(imgs)); + constexpr auto capture_buffer_size = 12; + std::list> imgs(capture_buffer_size); + + std::vector> imgs_used_timestamps; + const std::chrono::seconds trim_timeot = 3s; + auto trim_imgs = [&]() { + // count allocated and used within current pool + size_t allocated_count = 0; + size_t used_count = 0; + for (const auto &img : imgs) { + if (img) { + allocated_count += 1; + if (img.use_count() > 1) { + used_count += 1; + } + } + } - for (auto &img : imgs) { - img = disp->alloc_img(); - if (!img) { - BOOST_LOG(error) << "Couldn't initialize an image"sv; - return; + // remember the timestamp of currently used count + const auto now = std::chrono::steady_clock::now(); + if (imgs_used_timestamps.size() <= used_count) { + imgs_used_timestamps.resize(used_count + 1); + } + imgs_used_timestamps[used_count] = now; + + // decide whether to trim allocated unused above the currently used count + // based on last used timestamp and universal timeout + size_t trim_target = used_count; + for (size_t i = used_count; i < imgs_used_timestamps.size(); i++) { + if (imgs_used_timestamps[i] && now - *imgs_used_timestamps[i] < trim_timeot) { + trim_target = i; + } } - } + + // trim allocated unused above the newly decided trim target + if (allocated_count > trim_target) { + size_t to_trim = allocated_count - trim_target; + // prioritize trimming least recently used + for (auto it = imgs.rbegin(); it != imgs.rend(); it++) { + auto &img = *it; + if (img && img.use_count() == 1) { + img.reset(); + to_trim -= 1; + if (to_trim == 0) break; + } + } + // forget timestamps that no longer relevant + imgs_used_timestamps.resize(trim_target + 1); + } + }; + + auto pull_free_image_callback = [&](std::shared_ptr &img_out) -> bool { + img_out.reset(); + while (capture_ctx_queue->running()) { + // pick first allocated but unused + for (auto it = imgs.begin(); it != imgs.end(); it++) { + if (*it && it->use_count() == 1) { + img_out = *it; + if (it != imgs.begin()) { + // move image to the front of the list to prioritize its reusal + imgs.erase(it); + imgs.push_front(img_out); + } + break; + } + } + // otherwise pick first unallocated + if (!img_out) { + for (auto it = imgs.begin(); it != imgs.end(); it++) { + if (!*it) { + // allocate image + *it = disp->alloc_img(); + img_out = *it; + if (it != imgs.begin()) { + // move image to the front of the list to prioritize its reusal + imgs.erase(it); + imgs.push_front(img_out); + } + break; + } + } + } + if (img_out) { + // trim allocated but unused portion of the pool based on timeouts + trim_imgs(); + img_out->frame_timestamp.reset(); + return true; + } + else { + // sleep and retry if image pool is full + std::this_thread::sleep_for(1ms); + } + } + return false; + }; // Capture takes place on this thread platf::adjust_thread_priority(platf::thread_priority_e::critical); @@ -808,7 +891,7 @@ namespace video { while (capture_ctx_queue->running()) { bool artificial_reinit = false; - auto status = disp->capture([&](std::shared_ptr &img, bool frame_captured) -> std::shared_ptr { + auto push_captured_image_callback = [&](std::shared_ptr &&img, bool frame_captured) -> bool { KITTY_WHILE_LOOP(auto capture_ctx = std::begin(capture_ctxs), capture_ctx != std::end(capture_ctxs), { if (!capture_ctx->images->running()) { capture_ctx = capture_ctxs.erase(capture_ctx); @@ -824,8 +907,9 @@ namespace video { }) if (!capture_ctx_queue->running()) { - return nullptr; + return false; } + while (capture_ctx_queue->peek()) { capture_ctxs.emplace_back(std::move(*capture_ctx_queue->pop())); } @@ -834,18 +918,13 @@ namespace video { artificial_reinit = true; display_p = std::clamp(*switch_display_event->pop(), 0, (int) display_names.size() - 1); - return nullptr; + return false; } - auto &next_img = *round_robin++; - while (next_img.use_count() > 1) { - // Sleep a bit to avoid starving the encoder threads - std::this_thread::sleep_for(2ms); - } + return true; + }; - return next_img; - }, - *round_robin++, &display_cursor); + auto status = disp->capture(push_captured_image_callback, pull_free_image_callback, &display_cursor); if (artificial_reinit && status != platf::capture_e::error) { status = platf::capture_e::reinit; @@ -899,21 +978,13 @@ namespace video { display_wp = disp; - // Re-allocate images - for (auto &img : imgs) { - img = disp->alloc_img(); - if (!img) { - BOOST_LOG(error) << "Couldn't initialize an image"sv; - return; - } - } - reinit_event.reset(); continue; } case platf::capture_e::error: case platf::capture_e::ok: case platf::capture_e::timeout: + case platf::capture_e::interrupted: return; default: BOOST_LOG(error) << "Unrecognized capture status ["sv << (int) status << ']'; @@ -923,7 +994,7 @@ namespace video { } int - encode(int64_t frame_nr, session_t &session, frame_t::pointer frame, safe::mail_raw_t::queue_t &packets, void *channel_data) { + encode(int64_t frame_nr, session_t &session, frame_t::pointer frame, safe::mail_raw_t::queue_t &packets, void *channel_data, const std::optional &frame_timestamp) { frame->pts = frame_nr; auto &ctx = session.ctx; @@ -931,7 +1002,7 @@ namespace video { auto &sps = session.sps; auto &vps = session.vps; - /* send the frame to the encoder */ + // send the frame to the encoder auto ret = avcodec_send_frame(ctx.get(), frame); if (ret < 0) { char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 }; @@ -976,6 +1047,10 @@ namespace video { std::string_view((char *) std::begin(sps._new), sps._new.size())); } + if (av_packet && av_packet->pts == frame_nr) { + packet->frame_timestamp = frame_timestamp; + } + packet->replacements = &session.replacements; packet->channel_data = channel_data; packets->raise(std::move(packet)); @@ -1032,12 +1107,14 @@ namespace video { ctx->keyint_min = std::numeric_limits::max(); - if (config.numRefFrames == 0) { - ctx->refs = video_format[encoder_t::REF_FRAMES_AUTOSELECT] ? 0 : 16; - } - else { - // Some client decoders have limits on the number of reference frames - ctx->refs = video_format[encoder_t::REF_FRAMES_RESTRICT] ? config.numRefFrames : 0; + // Some client decoders have limits on the number of reference frames + if (config.numRefFrames) { + if (video_format[encoder_t::REF_FRAMES_RESTRICT]) { + ctx->refs = config.numRefFrames; + } + else { + BOOST_LOG(warning) << "Client requested reference frame limit, but encoder doesn't support it!"sv; + } } ctx->flags |= (AV_CODEC_FLAG_CLOSED_GOP | AV_CODEC_FLAG_LOW_DELAY); @@ -1146,7 +1223,7 @@ namespace video { ctx->slices = std::max(config.slicesPerFrame, config::video.min_threads); } - if (!video_format[encoder_t::SLICE]) { + if (encoder.flags & SINGLE_SLICE_ONLY) { ctx->slices = 1; } @@ -1286,12 +1363,6 @@ namespace video { (1 - (int) video_format[encoder_t::VUI_PARAMETERS]) * (1 + config.videoFormat), }; - if (!video_format[encoder_t::NALU_PREFIX_5b]) { - auto nalu_prefix = config.videoFormat ? hevc_nalu : h264_nalu; - - session.replacements.emplace_back(nalu_prefix.substr(1), nalu_prefix); - } - return std::make_optional(std::move(session)); } @@ -1317,11 +1388,15 @@ namespace video { auto packets = mail::man->queue(mail::video_packets); auto idr_events = mail->event(mail::idr); - // Load a dummy image into the AVFrame to ensure we have something to encode - // even if we timeout waiting on the first frame. - auto dummy_img = disp->alloc_img(); - if (!dummy_img || disp->dummy_img(dummy_img.get()) || session->device->convert(*dummy_img)) { - return; + { + // Load a dummy image into the AVFrame to ensure we have something to encode + // even if we timeout waiting on the first frame. This is a relatively large + // allocation which can be freed immediately after convert(), so we do this + // in a separate scope. + auto dummy_img = disp->alloc_img(); + if (!dummy_img || disp->dummy_img(dummy_img.get()) || session->device->convert(*dummy_img)) { + return; + } } while (true) { @@ -1336,9 +1411,12 @@ namespace video { idr_events->pop(); } + std::optional frame_timestamp; + // Encode at a minimum of 10 FPS to avoid image quality issues with static content if (!frame->key_frame || images->peek()) { if (auto img = images->pop(100ms)) { + frame_timestamp = img->frame_timestamp; if (session->device->convert(*img)) { BOOST_LOG(error) << "Could not convert image"sv; return; @@ -1349,7 +1427,7 @@ namespace video { } } - if (encode(frame_nr++, *session, frame, packets, channel_data)) { + if (encode(frame_nr++, *session, frame, packets, channel_data, frame_timestamp)) { BOOST_LOG(error) << "Could not encode video packet"sv; return; } @@ -1376,10 +1454,12 @@ namespace video { auto offsetY = (config.height - h2) * 0.5f; return input::touch_port_t { - display->offset_x, - display->offset_y, - config.width, - config.height, + { + display->offset_x, + display->offset_y, + config.width, + config.height, + }, display->env_width, display->env_height, offsetX, @@ -1418,14 +1498,14 @@ namespace video { encode_session.session = std::move(*session); - return std::move(encode_session); + return encode_session; } encode_e encode_run_sync( std::vector> &synced_session_ctxs, encode_session_ctx_queue_t &encode_session_ctx_queue) { - const auto &encoder = encoders.front(); + const auto &encoder = *chosen_encoder; auto display_names = platf::display_names(map_base_dev_type(encoder.base_dev_type)); int display_p = 0; @@ -1483,11 +1563,11 @@ namespace video { auto ec = platf::capture_e::ok; while (encode_session_ctx_queue.running()) { - auto snapshot_cb = [&](std::shared_ptr &img, bool frame_captured) -> std::shared_ptr { + auto push_captured_image_callback = [&](std::shared_ptr &&img, bool frame_captured) -> bool { while (encode_session_ctx_queue.peek()) { auto encode_session_ctx = encode_session_ctx_queue.pop(); if (!encode_session_ctx) { - return nullptr; + return false; } synced_session_ctxs.emplace_back(std::make_unique(std::move(*encode_session_ctx))); @@ -1495,7 +1575,7 @@ namespace video { auto encode_session = make_synced_session(disp.get(), encoder, *img, *synced_session_ctxs.back()); if (!encode_session) { ec = platf::capture_e::error; - return nullptr; + return false; } synced_sessions.emplace_back(std::move(*encode_session)); @@ -1514,7 +1594,7 @@ namespace video { })); if (synced_sessions.empty()) { - return nullptr; + return false; } continue; @@ -1534,7 +1614,12 @@ namespace video { continue; } - if (encode(ctx->frame_nr++, pos->session, frame, ctx->packets, ctx->channel_data)) { + std::optional frame_timestamp; + if (img) { + frame_timestamp = img->frame_timestamp; + } + + if (encode(ctx->frame_nr++, pos->session, frame, ctx->packets, ctx->channel_data, frame_timestamp)) { BOOST_LOG(error) << "Could not encode video packet"sv; ctx->shutdown_event->raise(true); @@ -1551,18 +1636,25 @@ namespace video { ec = platf::capture_e::reinit; display_p = std::clamp(*switch_display_event->pop(), 0, (int) display_names.size() - 1); - return nullptr; + return false; } - return img; + return true; }; - auto status = disp->capture(std::move(snapshot_cb), img, &display_cursor); + auto pull_free_image_callback = [&img](std::shared_ptr &img_out) -> bool { + img_out = img; + img_out->frame_timestamp.reset(); + return true; + }; + + auto status = disp->capture(push_captured_image_callback, pull_free_image_callback, &display_cursor); switch (status) { case platf::capture_e::reinit: case platf::capture_e::error: case platf::capture_e::ok: case platf::capture_e::timeout: + case platf::capture_e::interrupted: return ec != platf::capture_e::ok ? ec : status; } } @@ -1646,7 +1738,7 @@ namespace video { display = ref->display_wp->lock(); } - auto &encoder = encoders.front(); + auto &encoder = *chosen_encoder; auto pix_fmt = config.dynamicRange == 0 ? map_pix_fmt(encoder.static_pix_fmt) : map_pix_fmt(encoder.dynamic_pix_fmt); auto hwdevice = display->make_hwdevice(pix_fmt); if (!hwdevice) { @@ -1682,7 +1774,7 @@ namespace video { auto idr_events = mail->event(mail::idr); idr_events->raise(true); - if (encoders.front().flags & PARALLEL_ENCODING) { + if (chosen_encoder->flags & PARALLEL_ENCODING) { capture_async(std::move(mail), config, channel_data); } else { @@ -1707,7 +1799,6 @@ namespace video { enum validate_flag_e { VUI_PARAMS = 0x01, - NALU_PREFIX_5b = 0x02, }; int @@ -1728,12 +1819,12 @@ namespace video { return -1; } - auto img = disp->alloc_img(); - if (!img || disp->dummy_img(img.get())) { - return -1; - } - if (session->device->convert(*img)) { - return -1; + { + // Image buffers are large, so we use a separate scope to free it immediately after convert() + auto img = disp->alloc_img(); + if (!img || disp->dummy_img(img.get()) || session->device->convert(*img)) { + return -1; + } } auto frame = session->device->frame; @@ -1742,7 +1833,7 @@ namespace video { auto packets = mail::man->queue(mail::video_packets); while (!packets->peek()) { - if (encode(1, *session, frame, packets, nullptr)) { + if (encode(1, *session, frame, packets, nullptr, {})) { return -1; } } @@ -1760,17 +1851,11 @@ namespace video { flag |= VUI_PARAMS; } - auto nalu_prefix = config.videoFormat ? hevc_nalu : h264_nalu; - std::string_view payload { (char *) av_packet->data, (std::size_t) av_packet->size }; - if (std::search(std::begin(payload), std::end(payload), std::begin(nalu_prefix), std::end(nalu_prefix)) != std::end(payload)) { - flag |= NALU_PREFIX_5b; - } - return flag; } bool - validate_encoder(encoder_t &encoder) { + validate_encoder(encoder_t &encoder, bool expect_failure) { std::shared_ptr disp; BOOST_LOG(info) << "Trying encoder ["sv << encoder.name << ']'; @@ -1778,23 +1863,22 @@ namespace video { BOOST_LOG(info) << "Encoder ["sv << encoder.name << "] failed"sv; }); - auto force_hevc = config::video.hevc_mode >= 2; - auto test_hevc = force_hevc || (config::video.hevc_mode == 0 && !(encoder.flags & H264_ONLY)); + auto force_hevc = active_hevc_mode >= 2; + auto test_hevc = force_hevc || (active_hevc_mode == 0 && !(encoder.flags & H264_ONLY)); encoder.h264.capabilities.set(); encoder.hevc.capabilities.set(); - encoder.hevc[encoder_t::PASSED] = test_hevc; - // First, test encoder viability config_t config_max_ref_frames { 1920, 1080, 60, 1000, 1, 1, 1, 0, 0 }; config_t config_autoselect { 1920, 1080, 60, 1000, 1, 0, 1, 0, 0 }; retry: - auto max_ref_frames_h264 = validate_config(disp, encoder, config_max_ref_frames); - auto autoselect_h264 = validate_config(disp, encoder, config_autoselect); - - if (max_ref_frames_h264 < 0 && autoselect_h264 < 0) { + // If we're expecting failure, use the autoselect ref config first since that will always succeed + // if the encoder is available. + auto max_ref_frames_h264 = expect_failure ? -1 : validate_config(disp, encoder, config_max_ref_frames); + auto autoselect_h264 = max_ref_frames_h264 >= 0 ? max_ref_frames_h264 : validate_config(disp, encoder, config_autoselect); + if (autoselect_h264 < 0) { if (encoder.h264.qp && encoder.h264[encoder_t::CBR]) { // It's possible the encoder isn't accepting Constant Bit Rate. Turn off CBR and make another attempt encoder.h264.capabilities.set(); @@ -1803,10 +1887,13 @@ namespace video { } return false; } + else if (expect_failure) { + // We expected failure, but actually succeeded. Do the max_ref_frames probe we skipped. + max_ref_frames_h264 = validate_config(disp, encoder, config_max_ref_frames); + } std::vector> packet_deficiencies { { VUI_PARAMS, encoder_t::VUI_PARAMETERS }, - { NALU_PREFIX_5b, encoder_t::NALU_PREFIX_5b }, }; for (auto [validate_flag, encoder_flag] : packet_deficiencies) { @@ -1814,20 +1901,16 @@ namespace video { } encoder.h264[encoder_t::REF_FRAMES_RESTRICT] = max_ref_frames_h264 >= 0; - encoder.h264[encoder_t::REF_FRAMES_AUTOSELECT] = autoselect_h264 >= 0; encoder.h264[encoder_t::PASSED] = true; - encoder.h264[encoder_t::SLICE] = validate_config(disp, encoder, config_max_ref_frames); if (test_hevc) { config_max_ref_frames.videoFormat = 1; config_autoselect.videoFormat = 1; retry_hevc: auto max_ref_frames_hevc = validate_config(disp, encoder, config_max_ref_frames); - auto autoselect_hevc = validate_config(disp, encoder, config_autoselect); - - // If HEVC must be supported, but it is not supported - if (max_ref_frames_hevc < 0 && autoselect_hevc < 0) { + auto autoselect_hevc = max_ref_frames_hevc >= 0 ? max_ref_frames_hevc : validate_config(disp, encoder, config_autoselect); + if (autoselect_hevc < 0) { if (encoder.hevc.qp && encoder.hevc[encoder_t::CBR]) { // It's possible the encoder isn't accepting Constant Bit Rate. Turn off CBR and make another attempt encoder.hevc.capabilities.set(); @@ -1835,6 +1918,7 @@ namespace video { goto retry_hevc; } + // If HEVC must be supported, but it is not supported if (force_hevc) { return false; } @@ -1845,20 +1929,17 @@ namespace video { } encoder.hevc[encoder_t::REF_FRAMES_RESTRICT] = max_ref_frames_hevc >= 0; - encoder.hevc[encoder_t::REF_FRAMES_AUTOSELECT] = autoselect_hevc >= 0; - encoder.hevc[encoder_t::PASSED] = max_ref_frames_hevc >= 0 || autoselect_hevc >= 0; } + else { + // Clear all cap bits for HEVC if we didn't probe it + encoder.hevc.capabilities.reset(); + } std::vector> configs { { encoder_t::DYNAMIC_RANGE, { 1920, 1080, 60, 1000, 1, 0, 3, 1, 1 } }, }; - if (!(encoder.flags & SINGLE_SLICE_ONLY)) { - configs.emplace_back( - std::pair { encoder_t::SLICE, { 1920, 1080, 60, 1000, 2, 1, 1, 0, 0 } }); - } - for (auto &[flag, config] : configs) { auto h264 = config; auto hevc = config; @@ -1866,17 +1947,14 @@ namespace video { h264.videoFormat = 0; hevc.videoFormat = 1; - encoder.h264[flag] = validate_config(disp, encoder, h264) >= 0; + // HDR is not supported with H.264. Don't bother even trying it. + encoder.h264[flag] = flag != encoder_t::DYNAMIC_RANGE && validate_config(disp, encoder, h264) >= 0; + if (encoder.hevc[encoder_t::PASSED]) { encoder.hevc[flag] = validate_config(disp, encoder, hevc) >= 0; } } - if (encoder.flags & SINGLE_SLICE_ONLY) { - encoder.h264.capabilities[encoder_t::SLICE] = false; - encoder.hevc.capabilities[encoder_t::SLICE] = false; - } - encoder.h264[encoder_t::VUI_PARAMETERS] = encoder.h264[encoder_t::VUI_PARAMETERS] && !config::sunshine.flags[config::flag::FORCE_VIDEO_HEADER_REPLACE]; encoder.hevc[encoder_t::VUI_PARAMETERS] = encoder.hevc[encoder_t::VUI_PARAMETERS] && !config::sunshine.flags[config::flag::FORCE_VIDEO_HEADER_REPLACE]; @@ -1887,97 +1965,104 @@ namespace video { BOOST_LOG(warning) << encoder.name << ": hevc missing sps->vui parameters"sv; } - if (!encoder.h264[encoder_t::NALU_PREFIX_5b]) { - BOOST_LOG(warning) << encoder.name << ": h264: replacing nalu prefix data"sv; - } - if (encoder.hevc[encoder_t::PASSED] && !encoder.hevc[encoder_t::NALU_PREFIX_5b]) { - BOOST_LOG(warning) << encoder.name << ": hevc: replacing nalu prefix data"sv; - } - fg.disable(); return true; } + /** + * This is called once at startup and each time a stream is launched to + * ensure the best encoder is selected. Encoder availablility can change + * at runtime due to all sorts of things from driver updates to eGPUs. + * + * This is only safe to call when there is no client actively streaming. + */ int - init() { - bool encoder_found = false; + probe_encoders() { + auto encoder_list = encoders; + + // Restart encoder selection + auto previous_encoder = chosen_encoder; + chosen_encoder = nullptr; + active_hevc_mode = config::video.hevc_mode; + if (!config::video.encoder.empty()) { // If there is a specific encoder specified, use it if it passes validation - KITTY_WHILE_LOOP(auto pos = std::begin(encoders), pos != std::end(encoders), { + KITTY_WHILE_LOOP(auto pos = std::begin(encoder_list), pos != std::end(encoder_list), { auto encoder = *pos; - if (encoder.name == config::video.encoder) { + if (encoder->name == config::video.encoder) { // Remove the encoder from the list entirely if it fails validation - if (!validate_encoder(encoder)) { - pos = encoders.erase(pos); + if (!validate_encoder(*encoder, previous_encoder && previous_encoder != encoder)) { + pos = encoder_list.erase(pos); break; } // If we can't satisfy both the encoder and HDR requirement, prefer the encoder over HDR support - if (config::video.hevc_mode == 3 && !encoder.hevc[encoder_t::DYNAMIC_RANGE]) { + if (active_hevc_mode == 3 && !encoder->hevc[encoder_t::DYNAMIC_RANGE]) { BOOST_LOG(warning) << "Encoder ["sv << config::video.encoder << "] does not support HDR on this system"sv; - config::video.hevc_mode = 0; + active_hevc_mode = 0; } - encoders.clear(); - encoders.emplace_back(encoder); - encoder_found = true; + chosen_encoder = encoder; break; } pos++; }); - if (!encoder_found) { + if (chosen_encoder == nullptr) { BOOST_LOG(error) << "Couldn't find any working encoder matching ["sv << config::video.encoder << ']'; - config::video.encoder.clear(); } } BOOST_LOG(info) << "// Testing for available encoders, this may generate errors. You can safely ignore those errors. //"sv; // If we haven't found an encoder yet, but we want one with HDR support, search for that now. - if (!encoder_found && config::video.hevc_mode == 3) { - KITTY_WHILE_LOOP(auto pos = std::begin(encoders), pos != std::end(encoders), { + if (chosen_encoder == nullptr && active_hevc_mode == 3) { + KITTY_WHILE_LOOP(auto pos = std::begin(encoder_list), pos != std::end(encoder_list), { auto encoder = *pos; // Remove the encoder from the list entirely if it fails validation - if (!validate_encoder(encoder)) { - pos = encoders.erase(pos); + if (!validate_encoder(*encoder, previous_encoder && previous_encoder != encoder)) { + pos = encoder_list.erase(pos); continue; } // Skip it if it doesn't support HDR - if (!encoder.hevc[encoder_t::DYNAMIC_RANGE]) { + if (!encoder->hevc[encoder_t::DYNAMIC_RANGE]) { pos++; continue; } - encoders.clear(); - encoders.emplace_back(encoder); - encoder_found = true; + chosen_encoder = encoder; break; }); - if (!encoder_found) { + if (chosen_encoder == nullptr) { BOOST_LOG(error) << "Couldn't find any working HDR-capable encoder"sv; } } // If no encoder was specified or the specified encoder was unusable, keep trying // the remaining encoders until we find one that passes validation. - if (!encoder_found) { - KITTY_WHILE_LOOP(auto pos = std::begin(encoders), pos != std::end(encoders), { - if (!validate_encoder(*pos)) { - pos = encoders.erase(pos); + if (chosen_encoder == nullptr) { + KITTY_WHILE_LOOP(auto pos = std::begin(encoder_list), pos != std::end(encoder_list), { + auto encoder = *pos; + + // If we've used a previous encoder and it's not this one, we expect this encoder to + // fail to validate. It will use a slightly different order of checks to more quickly + // eliminate failing encoders. + if (!validate_encoder(*encoder, previous_encoder && previous_encoder != encoder)) { + pos = encoder_list.erase(pos); continue; } + chosen_encoder = encoder; break; }); } - if (encoders.empty()) { + if (chosen_encoder == nullptr) { BOOST_LOG(fatal) << "Couldn't find any working encoder"sv; return -1; } @@ -1986,7 +2071,7 @@ namespace video { BOOST_LOG(info) << "// Ignore any errors mentioned above, they are not relevant. //"sv; BOOST_LOG(info); - auto &encoder = encoders.front(); + auto &encoder = *chosen_encoder; BOOST_LOG(debug) << "------ h264 ------"sv; for (int x = 0; x < encoder_t::MAX_FLAGS; ++x) { @@ -2009,8 +2094,8 @@ namespace video { BOOST_LOG(info) << "Found encoder "sv << encoder.name << ": ["sv << encoder.h264.name << ']'; } - if (config::video.hevc_mode == 0) { - config::video.hevc_mode = encoder.hevc[encoder_t::PASSED] ? (encoder.hevc[encoder_t::DYNAMIC_RANGE] ? 3 : 2) : 1; + if (active_hevc_mode == 0) { + active_hevc_mode = encoder.hevc[encoder_t::PASSED] ? (encoder.hevc[encoder_t::DYNAMIC_RANGE] ? 3 : 2) : 1; } return 0; @@ -2118,7 +2203,7 @@ namespace video { int start_capture_async(capture_thread_async_ctx_t &capture_thread_ctx) { - capture_thread_ctx.encoder_p = &encoders.front(); + capture_thread_ctx.encoder_p = chosen_encoder; capture_thread_ctx.reinit_event.reset(); capture_thread_ctx.capture_ctx_queue = std::make_shared>(30); diff --git a/src/video.h b/src/video.h index 03bf218b038..d906a2f93b5 100644 --- a/src/video.h +++ b/src/video.h @@ -1,7 +1,8 @@ -// Created by loki on 6/9/19. - -#ifndef SUNSHINE_VIDEO_H -#define SUNSHINE_VIDEO_H +/** + * @file src/video.h + * @brief todo + */ +#pragma once #include "input.h" #include "platform/common.h" @@ -32,7 +33,7 @@ namespace video { } ~packet_raw_t() { - av_packet_unref(this->av_packet); + av_packet_free(&this->av_packet); } struct replace_t { @@ -48,6 +49,8 @@ namespace video { AVPacket *av_packet; std::vector *replacements; void *channel_data; + + std::optional frame_timestamp; }; using packet_t = std::unique_ptr; @@ -90,6 +93,8 @@ namespace video { extern color_t colors[6]; + extern int active_hevc_mode; + void capture( safe::mail_t mail, @@ -97,7 +102,5 @@ namespace video { void *channel_data); int - init(); + probe_encoders(); } // namespace video - -#endif // SUNSHINE_VIDEO_H diff --git a/src_assets/common/assets/web/apps.html b/src_assets/common/assets/web/apps.html index 8908c50c509..f3243036f96 100644 --- a/src_assets/common/assets/web/apps.html +++ b/src_assets/common/assets/web/apps.html @@ -15,9 +15,11 @@

Applications

{{app.name}} - + @@ -56,29 +58,51 @@

Applications

-
-
- - -
- Enable/Disable the execution of Global Prep Commands for this application. -
+
+ + +
+ Enable/Disable the execution of Global Prep Commands for this + application.
+
+
- A list of commands to be run before/after this application.
- If any of the prep-commands fail, starting the application is aborted + A list of commands to be run before/after this application.
+ If any of the prep-commands fail, starting the application is aborted.
- +
+ +
+
- - - + + + + + + - + +
DoUndo
Do Command Undo Command + Run as Admin +
Applications v-model="c.undo" /> +
+ + +
+
+
-
@@ -170,9 +205,28 @@

Applications

v-model="editForm['working-dir']" />
- The working directory that should be passed to the process. - For example, some applications use the working directory to search for configuration files. - If not set, Sunshine will default to the parent directory of the command + The working directory that should be passed to the process. For + example, some applications use the working directory to search for + configuration files. If not set, Sunshine will default to the parent + directory of the command +
+
+ +
+ + +
+ This can be necessary for some applications that require administrator + permissions to run properly.
@@ -180,36 +234,60 @@

Applications

- -