mirror of
https://github.com/LizardByte/Sunshine.git
synced 2025-08-10 00:52:16 +00:00
Compare commits
372 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4c9c292e5 | ||
|
|
fe7a7f4d77 | ||
|
|
cb0b32f90a | ||
|
|
9e93bb2dd8 | ||
|
|
0e6c41f823 | ||
|
|
725212b8a4 | ||
|
|
a275ee6b65 | ||
|
|
997095ce39 | ||
|
|
8b9cd51134 | ||
|
|
e7cbfb3ee9 | ||
|
|
1f7bdb1b2a | ||
|
|
9f14b2278d | ||
|
|
4177b02064 | ||
|
|
847d7b6980 | ||
|
|
ef1114512f | ||
|
|
38c36c00c9 | ||
|
|
846820f4ec | ||
|
|
c39e90b9b0 | ||
|
|
096d7587c3 | ||
|
|
27f5bb60e5 | ||
|
|
70ed5ce829 | ||
|
|
e58dc8e446 | ||
|
|
60e3538adc | ||
|
|
e2fb02323c | ||
|
|
57c7945847 | ||
|
|
68c723e135 | ||
|
|
0241414dfb | ||
|
|
c5a356f3e7 | ||
|
|
deecd19af2 | ||
|
|
f4cb13aa0e | ||
|
|
4385014049 | ||
|
|
fcb84132f4 | ||
|
|
50bd3094b4 | ||
|
|
d7cb71f877 | ||
|
|
e287404992 | ||
|
|
d332f11101 | ||
|
|
f78a9e2ccf | ||
|
|
bd7294e672 | ||
|
|
b3304a059d | ||
|
|
d0529fb234 | ||
|
|
bb912786bd | ||
|
|
8f47190ffc | ||
|
|
196f1f7471 | ||
|
|
e3cc25f23f | ||
|
|
a963b31c1d | ||
|
|
4d1689d6e9 | ||
|
|
fed329568c | ||
|
|
e3f642ac25 | ||
|
|
c94d922282 | ||
|
|
7563a0fa1f | ||
|
|
f5db0e438b | ||
|
|
9982ae4675 | ||
|
|
69eba9c493 | ||
|
|
2c67d73f08 | ||
|
|
7f4b9cf36c | ||
|
|
9c0ea17ada | ||
|
|
91a6e55f74 | ||
|
|
fea650fbe4 | ||
|
|
35e0497f86 | ||
|
|
fbc3735f44 | ||
|
|
9be80c103e | ||
|
|
640f2b1c55 | ||
|
|
d8df57130a | ||
|
|
3ba0533773 | ||
|
|
b1224032a1 | ||
|
|
1a7ed53559 | ||
|
|
9e9487617a | ||
|
|
d68b8138a5 | ||
|
|
10ca72f934 | ||
|
|
9dbf0df67d | ||
|
|
b072af3082 | ||
|
|
35c8b74bb4 | ||
|
|
7fbe9ba34f | ||
|
|
81c6ca5915 | ||
|
|
d73a4a38e5 | ||
|
|
6309f478a2 | ||
|
|
4ca2c0e740 | ||
|
|
ea9ada8d20 | ||
|
|
302b61090b | ||
|
|
75d224cd67 | ||
|
|
2ff9a129c0 | ||
|
|
a1a4ce1af8 | ||
|
|
44ac873100 | ||
|
|
3d179a869a | ||
|
|
530f2de79e | ||
|
|
512e581d56 | ||
|
|
e5c2ad2069 | ||
|
|
6af961199e | ||
|
|
276aa23a61 | ||
|
|
e33a7ff53b | ||
|
|
d9d50d8943 | ||
|
|
7c753e2289 | ||
|
|
ff81a286bb | ||
|
|
c7c3ac7c9c | ||
|
|
74f673e23c | ||
|
|
7c51fbfd18 | ||
|
|
7839ff8057 | ||
|
|
8b91e168e5 | ||
|
|
08f056bb3f | ||
|
|
808af7fce1 | ||
|
|
090d353f3d | ||
|
|
c4b371ccc9 | ||
|
|
06a1119512 | ||
|
|
b80c4253f0 | ||
|
|
3f306de5e1 | ||
|
|
39f9506446 | ||
|
|
00de30d336 | ||
|
|
b59df48dde | ||
|
|
3840b3c561 | ||
|
|
b5424ec671 | ||
|
|
ec184fb2ab | ||
|
|
4a750c7b16 | ||
|
|
f38bbf90bb | ||
|
|
05dcff4f87 | ||
|
|
b458118e34 | ||
|
|
7a920da06d | ||
|
|
ec84f43b80 | ||
|
|
03d572fe10 | ||
|
|
c41df22c88 | ||
|
|
3b3b9e2bd9 | ||
|
|
ebf9dbe931 | ||
|
|
0b3b78891b | ||
|
|
12af30b75b | ||
|
|
a9775a0686 | ||
|
|
ca9809ca7e | ||
|
|
97e14de63e | ||
|
|
0f4cdc2d21 | ||
|
|
869b6ed89d | ||
|
|
1876de0a68 | ||
|
|
019d064d8e | ||
|
|
d6b45eb825 | ||
|
|
556a6aaf33 | ||
|
|
fc7ec9e538 | ||
|
|
1f239214a1 | ||
|
|
9e224987f7 | ||
|
|
81317ce672 | ||
|
|
fce23c482c | ||
|
|
1d2e042240 | ||
|
|
d852bb82a3 | ||
|
|
fdb7754043 | ||
|
|
62c3faaacb | ||
|
|
b58279beea | ||
|
|
898d62bad9 | ||
|
|
446c8ace82 | ||
|
|
e007ee9976 | ||
|
|
6721155155 | ||
|
|
b80619fb0f | ||
|
|
e75e26467f | ||
|
|
2187f0b198 | ||
|
|
3382a5d03c | ||
|
|
fff9145680 | ||
|
|
13e57bb183 | ||
|
|
948500ae41 | ||
|
|
17bcdd7902 | ||
|
|
03837a9308 | ||
|
|
b8bfc13cf9 | ||
|
|
24403cdd25 | ||
|
|
13d0106feb | ||
|
|
dce64fc487 | ||
|
|
0629fe7846 | ||
|
|
315ec47523 | ||
|
|
9ed2141fc8 | ||
|
|
5b40c5008f | ||
|
|
62db9ae01a | ||
|
|
8c2cd2f60d | ||
|
|
0812f6f3c3 | ||
|
|
065e9e718a | ||
|
|
27f1dc318b | ||
|
|
ac5f439839 | ||
|
|
b4255e22aa | ||
|
|
d15c1af152 | ||
|
|
0140989f3a | ||
|
|
793e329fa5 | ||
|
|
6702802829 | ||
|
|
dae9a67fe2 | ||
|
|
b8e11b1272 | ||
|
|
f08b6abc96 | ||
|
|
965812bc19 | ||
|
|
7f643345ce | ||
|
|
28fecbc50c | ||
|
|
6c11e9f27d | ||
|
|
2af179630a | ||
|
|
c243e82047 | ||
|
|
3de77b1849 | ||
|
|
37d09b0bdb | ||
|
|
9a5d23ebde | ||
|
|
da3ed5ff79 | ||
|
|
2e8b462fe5 | ||
|
|
87b2b708f8 | ||
|
|
7ddf8bbe94 | ||
|
|
a87782b025 | ||
|
|
c39f2b0c1f | ||
|
|
a07ad3e479 | ||
|
|
c58f95b79e | ||
|
|
d9f7952710 | ||
|
|
ab70a056fc | ||
|
|
844ba53f54 | ||
|
|
63b920cd7b | ||
|
|
2a5fd78789 | ||
|
|
bc52fe9b82 | ||
|
|
667c113d5b | ||
|
|
38915859ba | ||
|
|
70d0be4b9a | ||
|
|
dad446ea41 | ||
|
|
d283900e43 | ||
|
|
4b043e31fe | ||
|
|
f65882e42a | ||
|
|
1fda8f6219 | ||
|
|
620c629bb4 | ||
|
|
a1f63da057 | ||
|
|
5ff5942258 | ||
|
|
8c803e6a34 | ||
|
|
a93a640d42 | ||
|
|
62abd1ee19 | ||
|
|
d55c6ee84c | ||
|
|
d3419697a5 | ||
|
|
a7171d77db | ||
|
|
fccb8d080d | ||
|
|
24acfabe07 | ||
|
|
61be0eb051 | ||
|
|
c4b4393177 | ||
|
|
388e4696ff | ||
|
|
43dc7cf7c0 | ||
|
|
e8feb00b33 | ||
|
|
42472bec85 | ||
|
|
769c4c8f99 | ||
|
|
049044f768 | ||
|
|
971c784f14 | ||
|
|
747ba7f23a | ||
|
|
4a3f3622b8 | ||
|
|
7d2705424a | ||
|
|
b228fd371c | ||
|
|
cbd2a8269c | ||
|
|
7f9467d759 | ||
|
|
9cf7c14a26 | ||
|
|
f09aea81fe | ||
|
|
8249d41848 | ||
|
|
d33f742241 | ||
|
|
6d0499b0e3 | ||
|
|
fd7a6d070b | ||
|
|
3d81b0fe7a | ||
|
|
1d37a19468 | ||
|
|
cc3cf60015 | ||
|
|
8d4e55fcbb | ||
|
|
25aa8b41a5 | ||
|
|
667878d2eb | ||
|
|
53a9f365ce | ||
|
|
a587338701 | ||
|
|
a52f547726 | ||
|
|
136e9c43a5 | ||
|
|
616f62fb9f | ||
|
|
9d10eaeef4 | ||
|
|
f23fcee382 | ||
|
|
21b1a4a336 | ||
|
|
b346ac2eb0 | ||
|
|
db5a9363ba | ||
|
|
0a637b2272 | ||
|
|
169a53b568 | ||
|
|
109b48a108 | ||
|
|
118707f37a | ||
|
|
e169259f6f | ||
|
|
355df9a615 | ||
|
|
96cfb1f368 | ||
|
|
545cca792b | ||
|
|
ae04c4afbb | ||
|
|
e8fadd2848 | ||
|
|
beb6bdfadb | ||
|
|
8bf4ade9d8 | ||
|
|
ea928c53b4 | ||
|
|
c1697c8562 | ||
|
|
57c4986f0e | ||
|
|
0cc7e35ed9 | ||
|
|
d6eceaf0dc | ||
|
|
cf7eb14573 | ||
|
|
ed5de34800 | ||
|
|
27d4f6063f | ||
|
|
62662edc8d | ||
|
|
b67600962a | ||
|
|
1eda45a81a | ||
|
|
926e95f527 | ||
|
|
029194cb60 | ||
|
|
7e3abefc2c | ||
|
|
cf9eb961fc | ||
|
|
0a05c28df8 | ||
|
|
0034438c9e | ||
|
|
2691489dab | ||
|
|
9e7ecf8db2 | ||
|
|
32e6054435 | ||
|
|
bd2d846557 | ||
|
|
0932d8bab9 | ||
|
|
82ac3becd8 | ||
|
|
7b86ea9e87 | ||
|
|
63d15333f2 | ||
|
|
ac862f9157 | ||
|
|
23b09e3d41 | ||
|
|
4024378772 | ||
|
|
64e1132579 | ||
|
|
6749b64ed5 | ||
|
|
0c43b83598 | ||
|
|
c47e3cb020 | ||
|
|
f3b07efaf3 | ||
|
|
02360f7aef | ||
|
|
c21301a423 | ||
|
|
64a6c1419b | ||
|
|
6f95d360b5 | ||
|
|
71cef4b700 | ||
|
|
56bb4ca865 | ||
|
|
fa0ff91d23 | ||
|
|
13caabaec8 | ||
|
|
8f2e00f31b | ||
|
|
78244a48d2 | ||
|
|
8be7c563a9 | ||
|
|
3fe134d8a6 | ||
|
|
f4c145136c | ||
|
|
ee10a795bd | ||
|
|
8e32c8e6f4 | ||
|
|
30f7742f51 | ||
|
|
55ce46e6ed | ||
|
|
9e03cdda42 | ||
|
|
76f34be395 | ||
|
|
ee89b211de | ||
|
|
0c5daa44a1 | ||
|
|
5495c8367a | ||
|
|
988bf33b40 | ||
|
|
e53f65c305 | ||
|
|
1afd1b7c94 | ||
|
|
33fbd5f431 | ||
|
|
17d3fcf0d0 | ||
|
|
cf77b301bc | ||
|
|
9d52174d6b | ||
|
|
e2874b40c1 | ||
|
|
914f329eea | ||
|
|
877c739f1b | ||
|
|
61b195e9f4 | ||
|
|
c53564dd31 | ||
|
|
9eb4eadda6 | ||
|
|
44ebc4846b | ||
|
|
3a3d4928f9 | ||
|
|
84c55d6efc | ||
|
|
7f636a25a8 | ||
|
|
825a706139 | ||
|
|
4df57a722e | ||
|
|
45f9ce3a20 | ||
|
|
f2863cceb7 | ||
|
|
6b9ed7fcb3 | ||
|
|
f33a587218 | ||
|
|
bdb9ed9001 | ||
|
|
41911a26fb | ||
|
|
0942f2ee7f | ||
|
|
a05a3b355c | ||
|
|
ff1722bbd1 | ||
|
|
dc281119af | ||
|
|
b848db8f2b | ||
|
|
bc261fddf2 | ||
|
|
bb251d5046 | ||
|
|
ec9481392a | ||
|
|
8961b0462e | ||
|
|
ffb80c5fc3 | ||
|
|
4835366a0c | ||
|
|
c09855f703 | ||
|
|
5761b05f3b | ||
|
|
ff1ea1a63e | ||
|
|
0ea6363172 | ||
|
|
57f444357d | ||
|
|
fe08c241ec | ||
|
|
03236a50e5 | ||
|
|
27a1144217 | ||
|
|
04421d84a3 | ||
|
|
fd8cbf0c7d | ||
|
|
4fe90dcbd6 | ||
|
|
3ec4bf52e1 | ||
|
|
ec44a4391a |
12
.gitmodules
vendored
12
.gitmodules
vendored
@@ -1,9 +1,15 @@
|
||||
[submodule "moonlight-common-c"]
|
||||
path = moonlight-common-c
|
||||
path = third-party/moonlight-common-c
|
||||
url = https://github.com/moonlight-stream/moonlight-common-c.git
|
||||
[submodule "Simple-Web-Server"]
|
||||
path = Simple-Web-Server
|
||||
path = third-party/Simple-Web-Server
|
||||
url = https://github.com/loki-47-6F-64/Simple-Web-Server.git
|
||||
[submodule "ViGEmClient"]
|
||||
path = ViGEmClient
|
||||
path = third-party/ViGEmClient
|
||||
url = https://github.com/ViGEm/ViGEmClient
|
||||
[submodule "third-party/miniupnp"]
|
||||
path = third-party/miniupnp
|
||||
url = https://github.com/miniupnp/miniupnp
|
||||
[submodule "third-party/nv-codec-headers"]
|
||||
path = third-party/nv-codec-headers
|
||||
url = https://github.com/FFmpeg/nv-codec-headers
|
||||
|
||||
1
.prettierrc.json
Normal file
1
.prettierrc.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
267
CMakeLists.txt
267
CMakeLists.txt
@@ -2,9 +2,7 @@ cmake_minimum_required(VERSION 3.0)
|
||||
|
||||
project(Sunshine)
|
||||
|
||||
set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
|
||||
add_subdirectory(Simple-Web-Server)
|
||||
set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake)
|
||||
|
||||
if(WIN32)
|
||||
# Ugly hack to compile with #include <qos2.h>
|
||||
@@ -13,14 +11,26 @@ if(WIN32)
|
||||
PQOS_FLOWID=UINT32*
|
||||
QOS_NON_ADAPTIVE_FLOW=2)
|
||||
endif()
|
||||
add_subdirectory(moonlight-common-c/enet)
|
||||
add_subdirectory(third-party/moonlight-common-c/enet)
|
||||
add_subdirectory(third-party/Simple-Web-Server)
|
||||
|
||||
set(UPNPC_BUILD_SHARED OFF CACHE BOOL "no shared libraries")
|
||||
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)
|
||||
|
||||
find_package(Threads REQUIRED)
|
||||
find_package(OpenSSL REQUIRED)
|
||||
set(Boost_USE_STATIC_LIBS ON)
|
||||
find_package(Boost COMPONENTS log filesystem REQUIRED)
|
||||
|
||||
list(APPEND SUNSHINE_COMPILE_OPTIONS -fPIC -Wall -Wno-missing-braces -Wno-maybe-uninitialized -Wno-sign-compare)
|
||||
list(APPEND SUNSHINE_COMPILE_OPTIONS -Wall -Wno-missing-braces -Wno-maybe-uninitialized -Wno-sign-compare)
|
||||
|
||||
if(WIN32)
|
||||
enable_language(RC)
|
||||
set(CMAKE_RC_COMPILER windres)
|
||||
file(
|
||||
DOWNLOAD "https://github.com/TheElixZammuto/sunshine-prebuilt/releases/download/1.0.0/pre-compiled.zip" "${CMAKE_CURRENT_BINARY_DIR}/pre-compiled.zip"
|
||||
TIMEOUT 60
|
||||
@@ -35,24 +45,33 @@ if(WIN32)
|
||||
set(SUNSHINE_PREPARED_BINARIES "${CMAKE_CURRENT_BINARY_DIR}/pre-compiled/windows")
|
||||
endif()
|
||||
|
||||
add_compile_definitions(SUNSHINE_PLATFORM="windows")
|
||||
add_subdirectory(tools) #This is temporary, only tools for Windows are needed, for now
|
||||
|
||||
list(APPEND SUNSHINE_DEFINITIONS APPS_JSON="apps_windows.json")
|
||||
|
||||
include_directories(ViGEmClient/include)
|
||||
include_directories(third-party/ViGEmClient/include)
|
||||
|
||||
if(NOT DEFINED SUNSHINE_ICON_PATH)
|
||||
set(SUNSHINE_ICON_PATH "${CMAKE_CURRENT_SOURCE_DIR}/sunshine.ico")
|
||||
endif()
|
||||
configure_file(sunshine/platform/windows/windows.rs.in windows.rc @ONLY)
|
||||
set(PLATFORM_TARGET_FILES
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/windows.rc"
|
||||
sunshine/platform/windows/publish.cpp
|
||||
sunshine/platform/windows/misc.h
|
||||
sunshine/platform/windows/misc.cpp
|
||||
sunshine/platform/windows/input.cpp
|
||||
sunshine/platform/windows/display.h
|
||||
sunshine/platform/windows/display_base.cpp
|
||||
sunshine/platform/windows/display_vram.cpp
|
||||
sunshine/platform/windows/display_ram.cpp
|
||||
sunshine/platform/windows/audio.cpp
|
||||
ViGEmClient/src/ViGEmClient.cpp
|
||||
ViGEmClient/include/ViGEm/Client.h
|
||||
ViGEmClient/include/ViGEm/Common.h
|
||||
ViGEmClient/include/ViGEm/Util.h
|
||||
ViGEmClient/include/ViGEm/km/BusShared.h)
|
||||
third-party/ViGEmClient/src/ViGEmClient.cpp
|
||||
third-party/ViGEmClient/include/ViGEm/Client.h
|
||||
third-party/ViGEmClient/include/ViGEm/Common.h
|
||||
third-party/ViGEmClient/include/ViGEm/Util.h
|
||||
third-party/ViGEmClient/include/ViGEm/km/BusShared.h)
|
||||
|
||||
set(OPENSSL_LIBRARIES
|
||||
libssl.a
|
||||
@@ -78,62 +97,177 @@ if(WIN32)
|
||||
libstdc++.a
|
||||
libwinpthread.a
|
||||
libssp.a
|
||||
Qwave
|
||||
winmm
|
||||
ksuser
|
||||
wsock32
|
||||
ws2_32
|
||||
iphlpapi
|
||||
d3d11 dxgi D3DCompiler
|
||||
setupapi
|
||||
)
|
||||
|
||||
set_source_files_properties(ViGEmClient/src/ViGEmClient.cpp PROPERTIES COMPILE_DEFINITIONS "UNICODE=1;ERROR_INVALID_DEVICE_OBJECT_PARAMETER=650")
|
||||
set_source_files_properties(ViGEmClient/src/ViGEmClient.cpp PROPERTIES COMPILE_FLAGS "-Wno-unknown-pragmas -Wno-misleading-indentation -Wno-class-memaccess")
|
||||
set_source_files_properties(third-party/ViGEmClient/src/ViGEmClient.cpp PROPERTIES COMPILE_DEFINITIONS "UNICODE=1;ERROR_INVALID_DEVICE_OBJECT_PARAMETER=650")
|
||||
set_source_files_properties(third-party/ViGEmClient/src/ViGEmClient.cpp PROPERTIES COMPILE_FLAGS "-Wno-unknown-pragmas -Wno-misleading-indentation -Wno-class-memaccess")
|
||||
else()
|
||||
add_compile_definitions(SUNSHINE_PLATFORM="linux")
|
||||
list(APPEND SUNSHINE_DEFINITIONS APPS_JSON="apps_linux.json")
|
||||
|
||||
find_package(X11 REQUIRED)
|
||||
find_package(FFmpeg REQUIRED)
|
||||
set(PLATFORM_TARGET_FILES
|
||||
sunshine/platform/linux/display.cpp
|
||||
option(SUNSHINE_ENABLE_DRM "Enable KMS grab if available" ON)
|
||||
option(SUNSHINE_ENABLE_X11 "Enable X11 grab if available" ON)
|
||||
option(SUNSHINE_ENABLE_WAYLAND "Enable building wayland specific code" ON)
|
||||
option(SUNSHINE_ENABLE_CUDA "Enable cuda specific code" ON)
|
||||
|
||||
if(${SUNSHINE_ENABLE_X11})
|
||||
find_package(X11)
|
||||
else()
|
||||
set(X11_FOUND OFF)
|
||||
endif()
|
||||
|
||||
set(CUDA_FOUND OFF)
|
||||
if(${SUNSHINE_ENABLE_CUDA})
|
||||
include(CheckLanguage)
|
||||
check_language(CUDA)
|
||||
|
||||
if(CMAKE_CUDA_COMPILER)
|
||||
if(NOT DEFINED CMAKE_CUDA_ARCHITECTURES)
|
||||
set(CMAKE_CUDA_ARCHITECTURES 35)
|
||||
endif()
|
||||
|
||||
set(CUDA_FOUND ON)
|
||||
enable_language(CUDA)
|
||||
endif()
|
||||
endif()
|
||||
if(${SUNSHINE_ENABLE_DRM})
|
||||
find_package(LIBDRM)
|
||||
find_package(LIBCAP)
|
||||
else()
|
||||
set(LIBDRM_FOUND OFF)
|
||||
set(LIBCAP_FOUND OFF)
|
||||
endif()
|
||||
if(${SUNSHINE_ENABLE_WAYLAND})
|
||||
find_package(Wayland)
|
||||
else()
|
||||
set(WAYLAND_FOUND OFF)
|
||||
endif()
|
||||
|
||||
find_package(FFMPEG REQUIRED)
|
||||
|
||||
if(X11_FOUND)
|
||||
add_compile_definitions(SUNSHINE_BUILD_X11)
|
||||
include_directories(${X11_INCLUDE_DIR})
|
||||
list(APPEND PLATFORM_TARGET_FILES sunshine/platform/linux/x11grab.cpp)
|
||||
endif()
|
||||
|
||||
if(CUDA_FOUND)
|
||||
include_directories(third-party/nvfbc)
|
||||
list(APPEND PLATFORM_TARGET_FILES
|
||||
sunshine/platform/linux/cuda.cu
|
||||
sunshine/platform/linux/cuda.cpp
|
||||
third-party/nvfbc/NvFBC.h)
|
||||
|
||||
add_compile_definitions(SUNSHINE_BUILD_CUDA)
|
||||
endif()
|
||||
|
||||
if(LIBDRM_FOUND AND LIBCAP_FOUND)
|
||||
add_compile_definitions(SUNSHINE_BUILD_DRM)
|
||||
include_directories(${LIBDRM_INCLUDE_DIRS} ${LIBCAP_INCLUDE_DIRS})
|
||||
list(APPEND PLATFORM_LIBRARIES ${LIBDRM_LIBRARIES} ${LIBCAP_LIBRARIES})
|
||||
list(APPEND PLATFORM_TARGET_FILES sunshine/platform/linux/kmsgrab.cpp)
|
||||
list(APPEND SUNSHINE_DEFINITIONS EGL_NO_X11=1)
|
||||
elseif(LIBDRM_FOUND)
|
||||
message(WARNING "Found libdrm, yet there is no libcap")
|
||||
elseif(LIBDRM_FOUND)
|
||||
message(WARNING "Found libcap, yet there is no libdrm")
|
||||
endif()
|
||||
|
||||
if(WAYLAND_FOUND)
|
||||
add_compile_definitions(SUNSHINE_BUILD_WAYLAND)
|
||||
macro(genWayland FILENAME)
|
||||
make_directory(${CMAKE_BINARY_DIR}/generated-src)
|
||||
|
||||
message("wayland-scanner private-code ${CMAKE_SOURCE_DIR}/third-party/wayland-protocols/${FILENAME}.xml ${CMAKE_BINARY_DIR}/generated-src/${FILENAME}.c")
|
||||
message("wayland-scanner client-header ${CMAKE_SOURCE_DIR}/third-party/wayland-protocols/${FILENAME}.xml ${CMAKE_BINARY_DIR}/generated-src/${FILENAME}.h")
|
||||
execute_process(
|
||||
COMMAND wayland-scanner private-code ${CMAKE_SOURCE_DIR}/third-party/wayland-protocols/${FILENAME}.xml ${CMAKE_BINARY_DIR}/generated-src/${FILENAME}.c
|
||||
COMMAND wayland-scanner client-header ${CMAKE_SOURCE_DIR}/third-party/wayland-protocols/${FILENAME}.xml ${CMAKE_BINARY_DIR}/generated-src/${FILENAME}.h
|
||||
|
||||
RESULT_VARIABLE EXIT_INT
|
||||
)
|
||||
|
||||
if(NOT ${EXIT_INT} EQUAL 0)
|
||||
message(FATAL_ERROR "wayland-scanner failed")
|
||||
endif()
|
||||
|
||||
list(APPEND PLATFORM_TARGET_FILES
|
||||
${CMAKE_BINARY_DIR}/generated-src/${FILENAME}.c
|
||||
${CMAKE_BINARY_DIR}/generated-src/${FILENAME}.h
|
||||
)
|
||||
endmacro()
|
||||
|
||||
genWayland(xdg-output-unstable-v1)
|
||||
genWayland(wlr-export-dmabuf-unstable-v1)
|
||||
|
||||
include_directories(
|
||||
${WAYLAND_INCLUDE_DIRS}
|
||||
${CMAKE_BINARY_DIR}/generated-src
|
||||
)
|
||||
|
||||
list(APPEND PLATFORM_LIBRARIES ${WAYLAND_LIBRARIES})
|
||||
list(APPEND PLATFORM_TARGET_FILES
|
||||
sunshine/platform/linux/wlgrab.cpp
|
||||
sunshine/platform/linux/wayland.cpp)
|
||||
endif()
|
||||
if(NOT ${X11_FOUND} AND NOT (${LIBDRM_FOUND} AND ${LIBCAP_FOUND}) AND NOT ${WAYLAND_FOUND} AND NOT ${})
|
||||
message(FATAL_ERROR "Couldn't find either x11, wayland, cuda or (libdrm and libcap)")
|
||||
endif()
|
||||
|
||||
list(APPEND PLATFORM_TARGET_FILES
|
||||
sunshine/platform/linux/publish.cpp
|
||||
sunshine/platform/linux/vaapi.h
|
||||
sunshine/platform/linux/vaapi.cpp
|
||||
sunshine/platform/linux/cuda.h
|
||||
sunshine/platform/linux/graphics.h
|
||||
sunshine/platform/linux/graphics.cpp
|
||||
sunshine/platform/linux/misc.h
|
||||
sunshine/platform/linux/misc.cpp
|
||||
sunshine/platform/linux/audio.cpp
|
||||
sunshine/platform/linux/input.cpp)
|
||||
|
||||
set(PLATFORM_LIBRARIES
|
||||
Xfixes
|
||||
Xtst
|
||||
xcb
|
||||
xcb-shm
|
||||
xcb-xfixes
|
||||
Xrandr
|
||||
${X11_LIBRARIES}
|
||||
sunshine/platform/linux/input.cpp
|
||||
sunshine/platform/linux/x11grab.h
|
||||
sunshine/platform/linux/wayland.h
|
||||
third-party/glad/src/egl.c
|
||||
third-party/glad/src/gl.c
|
||||
third-party/glad/include/EGL/eglplatform.h
|
||||
third-party/glad/include/KHR/khrplatform.h
|
||||
third-party/glad/include/glad/gl.h
|
||||
third-party/glad/include/glad/egl.h)
|
||||
|
||||
list(APPEND PLATFORM_LIBRARIES
|
||||
dl
|
||||
evdev
|
||||
pulse
|
||||
pulse-simple
|
||||
)
|
||||
|
||||
set(PLATFORM_INCLUDE_DIRS
|
||||
${X11_INCLUDE_DIR}
|
||||
/usr/include/libevdev-1.0)
|
||||
include_directories(
|
||||
/usr/include/libevdev-1.0
|
||||
third-party/nv-codec-headers/include
|
||||
third-party/glad/include)
|
||||
|
||||
if(NOT DEFINED SUNSHINE_EXECUTABLE_PATH)
|
||||
set(SUNSHINE_EXECUTABLE_PATH "${CMAKE_CURRENT_BINARY_DIR}/sunshine")
|
||||
set(SUNSHINE_EXECUTABLE_PATH "sunshine")
|
||||
endif()
|
||||
configure_file(gen-deb.in gen-deb @ONLY)
|
||||
configure_file(sunshine.service.in sunshine.service @ONLY)
|
||||
endif()
|
||||
|
||||
set(Boost_USE_STATIC_LIBS ON)
|
||||
find_package(Boost COMPONENTS log filesystem REQUIRED)
|
||||
|
||||
set(SUNSHINE_TARGET_FILES
|
||||
moonlight-common-c/reedsolomon/rs.c
|
||||
moonlight-common-c/reedsolomon/rs.h
|
||||
moonlight-common-c/src/Input.h
|
||||
moonlight-common-c/src/Rtsp.h
|
||||
moonlight-common-c/src/RtspParser.c
|
||||
moonlight-common-c/src/Video.h
|
||||
third-party/moonlight-common-c/reedsolomon/rs.c
|
||||
third-party/moonlight-common-c/reedsolomon/rs.h
|
||||
third-party/moonlight-common-c/src/Input.h
|
||||
third-party/moonlight-common-c/src/Rtsp.h
|
||||
third-party/moonlight-common-c/src/RtspParser.c
|
||||
third-party/moonlight-common-c/src/Video.h
|
||||
sunshine/upnp.cpp
|
||||
sunshine/upnp.h
|
||||
sunshine/cbs.cpp
|
||||
sunshine/utility.h
|
||||
sunshine/uuid.h
|
||||
sunshine/config.h
|
||||
@@ -144,6 +278,10 @@ set(SUNSHINE_TARGET_FILES
|
||||
sunshine/crypto.h
|
||||
sunshine/nvhttp.cpp
|
||||
sunshine/nvhttp.h
|
||||
sunshine/httpcommon.cpp
|
||||
sunshine/httpcommon.h
|
||||
sunshine/confighttp.cpp
|
||||
sunshine/confighttp.h
|
||||
sunshine/rtsp.cpp
|
||||
sunshine/rtsp.h
|
||||
sunshine/stream.cpp
|
||||
@@ -167,18 +305,23 @@ set(SUNSHINE_TARGET_FILES
|
||||
sunshine/round_robin.h
|
||||
${PLATFORM_TARGET_FILES})
|
||||
|
||||
set_source_files_properties(sunshine/upnp.cpp PROPERTIES COMPILE_FLAGS -Wno-pedantic)
|
||||
|
||||
include_directories(
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Simple-Web-Server
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/moonlight-common-c/enet/include
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/moonlight-common-c/reedsolomon
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/third-party
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/third-party/cbs/include
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/third-party/moonlight-common-c/enet/include
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/third-party/moonlight-common-c/reedsolomon
|
||||
${FFMPEG_INCLUDE_DIRS}
|
||||
${PLATFORM_INCLUDE_DIRS}
|
||||
)
|
||||
|
||||
add_subdirectory(third-party/cbs)
|
||||
|
||||
string(TOUPPER "x${CMAKE_BUILD_TYPE}" BUILD_TYPE)
|
||||
if("${BUILD_TYPE}" STREQUAL "XDEBUG")
|
||||
list(APPEND SUNSHINE_COMPILE_OPTIONS -O0 -pedantic -ggdb3)
|
||||
list(APPEND SUNSHINE_COMPILE_OPTIONS -O0 -ggdb3)
|
||||
if(WIN32)
|
||||
set_source_files_properties(sunshine/nvhttp.cpp PROPERTIES COMPILE_FLAGS -O2)
|
||||
endif()
|
||||
@@ -191,7 +334,20 @@ if(NOT SUNSHINE_ASSETS_DIR)
|
||||
set(SUNSHINE_ASSETS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/assets")
|
||||
endif()
|
||||
|
||||
if(NOT SUNSHINE_CONFIG_DIR)
|
||||
set(SUNSHINE_CONFIG_DIR "${SUNSHINE_ASSETS_DIR}")
|
||||
endif()
|
||||
|
||||
if(NOT SUNSHINE_DEFAULT_DIR)
|
||||
set(SUNSHINE_DEFAULT_DIR "${SUNSHINE_ASSETS_DIR}")
|
||||
endif()
|
||||
|
||||
list(APPEND CBS_EXTERNAL_LIBRARIES
|
||||
cbs)
|
||||
|
||||
list(APPEND SUNSHINE_EXTERNAL_LIBRARIES
|
||||
libminiupnpc-static
|
||||
${CBS_EXTERNAL_LIBRARIES}
|
||||
${CMAKE_THREAD_LIBS_INIT}
|
||||
stdc++fs
|
||||
enet
|
||||
@@ -201,10 +357,25 @@ list(APPEND SUNSHINE_EXTERNAL_LIBRARIES
|
||||
${OPENSSL_LIBRARIES}
|
||||
${PLATFORM_LIBRARIES})
|
||||
|
||||
if (NOT WIN32)
|
||||
list(APPEND SUNSHINE_EXTERNAL_LIBRARIES Boost::log)
|
||||
endif()
|
||||
|
||||
list(APPEND SUNSHINE_DEFINITIONS SUNSHINE_ASSETS_DIR="${SUNSHINE_ASSETS_DIR}")
|
||||
list(APPEND SUNSHINE_DEFINITIONS SUNSHINE_CONFIG_DIR="${SUNSHINE_CONFIG_DIR}")
|
||||
list(APPEND SUNSHINE_DEFINITIONS SUNSHINE_DEFAULT_DIR="${SUNSHINE_DEFAULT_DIR}")
|
||||
add_executable(sunshine ${SUNSHINE_TARGET_FILES})
|
||||
target_link_libraries(sunshine ${SUNSHINE_EXTERNAL_LIBRARIES})
|
||||
target_compile_definitions(sunshine PUBLIC ${SUNSHINE_DEFINITIONS})
|
||||
set_target_properties(sunshine PROPERTIES CXX_STANDARD 17)
|
||||
|
||||
target_compile_options(sunshine PRIVATE ${SUNSHINE_COMPILE_OPTIONS})
|
||||
if(NOT DEFINED CMAKE_CUDA_STANDARD)
|
||||
set(CMAKE_CUDA_STANDARD 17)
|
||||
set(CMAKE_CUDA_STANDARD_REQUIRED ON)
|
||||
endif()
|
||||
|
||||
foreach(flag IN LISTS SUNSHINE_COMPILE_OPTIONS)
|
||||
list(APPEND SUNSHINE_COMPILE_OPTIONS_CUDA "$<$<COMPILE_LANGUAGE:CUDA>:--compiler-options=${flag}>")
|
||||
endforeach()
|
||||
|
||||
target_compile_options(sunshine PRIVATE $<$<COMPILE_LANGUAGE:CXX>:${SUNSHINE_COMPILE_OPTIONS}>;$<$<COMPILE_LANGUAGE:CUDA>:${SUNSHINE_COMPILE_OPTIONS_CUDA};-std=c++17>)
|
||||
|
||||
94
README.md
94
README.md
@@ -1,3 +1,4 @@
|
||||

|
||||
# Introduction
|
||||
Sunshine is a Gamestream host for Moonlight
|
||||
|
||||
@@ -13,29 +14,62 @@ Sunshine is a Gamestream host for Moonlight
|
||||
|
||||
## Linux
|
||||
|
||||
If you do not wish to clutter your PC with development files, yet you want the very latest version...
|
||||
You can use these [build scripts](scripts/README.md)
|
||||
They make use of docker to handle building Sunshine automatically
|
||||
|
||||
### Requirements:
|
||||
|
||||
Ubuntu 20.04:
|
||||
Install the following
|
||||
Install the following:
|
||||
|
||||
#### Common
|
||||
```
|
||||
sudo apt install cmake libssl-dev libavdevice-dev libboost-thread-dev libboost-filesystem-dev libboost-log-dev libpulse-dev libopus-dev libxtst-dev libx11-dev libxrandr-dev libxfixes-dev libevdev-dev libxcb1-dev libxcb-shm0-dev libxcb-xfixes0-dev
|
||||
sudo apt install cmake gcc-10 g++-10 libssl-dev libavdevice-dev libboost-thread-dev libboost-filesystem-dev libboost-log-dev libpulse-dev libopus-dev libevdev-dev
|
||||
```
|
||||
#### X11
|
||||
```
|
||||
sudo apt install libxtst-dev libx11-dev libxrandr-dev libxfixes-dev libxcb1-dev libxcb-shm0-dev libxcb-xfixes0-dev
|
||||
```
|
||||
|
||||
#### KMS
|
||||
This requires additional [setup](README.md#Setup).
|
||||
```
|
||||
sudo apt install libdrm-dev libcap-dev
|
||||
```
|
||||
|
||||
#### Wayland
|
||||
This is for wlroots based compositores, such as Sway
|
||||
```
|
||||
sudo apt install libwayland-dev
|
||||
```
|
||||
|
||||
#### Cuda + NvFBC
|
||||
This requires proprietary software
|
||||
On Ubuntu 20.04, the cuda compiler will fail since it's version is too old, it's recommended you compile the sources with the [build scripts](scripts/README.md)
|
||||
```
|
||||
sudo apt install nvidia-cuda-dev nvidia-cuda-toolkit
|
||||
```
|
||||
|
||||
#### Warning:
|
||||
You might require ffmpeg version >= 4.3. Check the troubleshooting section for more information.
|
||||
|
||||
### Compilation:
|
||||
- `git clone https://github.com/loki-47-6F-64/sunshine.git --recurse-submodules`
|
||||
- `cd sunshine && mkdir build && cd build`
|
||||
- `cmake ..`
|
||||
- `cmake -DCMAKE_C_COMPILER=gcc-10 -DCMAKE_CXX_COMPILER=g++-10 ..`
|
||||
- `make -j ${nproc}`
|
||||
|
||||
|
||||
### Setup:
|
||||
sunshine needs access to uinput to create mouse and gamepad events:
|
||||
|
||||
- Add user to group 'input':
|
||||
`usermod -a -G input $USER`
|
||||
- Create udev rules:
|
||||
- Run the following command:
|
||||
`nano /etc/udev/rules.d/85-sunshine-input.rules`
|
||||
- Input the following contents:
|
||||
`KERNEL=="uinput", GROUP="input", mode="0660"`
|
||||
`KERNEL=="uinput", GROUP="input", MODE="0660"`
|
||||
- Save the file and exit
|
||||
1. `CTRL+X` to start exit
|
||||
2. `Y` to save modifications
|
||||
@@ -52,26 +86,41 @@ sunshine needs access to uinput to create mouse and gamepad events:
|
||||
|
||||
- `assets/apps.json` is an [example](README.md#application-list) of a list of applications that are started just before running a stream
|
||||
|
||||
#### Additional Setup for KMS:
|
||||
Please note that `cap_sys_admin` may as well be root, except you don't need to be root to run it.
|
||||
It's necessary to allow Sunshine to use KMS
|
||||
- `sudo setcap cap_sys_admin+p sunshine`
|
||||
|
||||
### Trouleshooting:
|
||||
- If you get "Could not create Sunshine Gamepad: Permission Denied", ensure you are part of the group "input":
|
||||
- `groups $USER`
|
||||
|
||||
- If Sunshine sends audio from the microphone instead of the speaker, try the following steps:
|
||||
1. pacmd list-sources | grep "name:"
|
||||
1. Check whether you're using Pulseaudio or Pipewire
|
||||
- Pulseaudio: Use `pacmd list-sources | grep "name:"`
|
||||
- Pipewire: Use `pactl info | grep Source`. In some causes you'd need to use the `sink` device. Try `pactl info | grep Sink`, if _Source_ doesn't work.
|
||||
2. Copy the name to the configuration option "audio_sink"
|
||||
3. restart sunshine
|
||||
3. Restart sunshine
|
||||
|
||||
- If you get "Error: Failed to create client: Daemon not running", ensure that your avahi-daemon is running:
|
||||
- `systemctl status avahi-daemon`
|
||||
|
||||
- If you use hardware acceleration on Linux using an Intel or an AMD GPU (with VAAPI), you will get tons of [graphical issues](https://github.com/loki-47-6F-64/sunshine/issues/228) if your ffmpeg version is < 4.3. If it is not available in your distribution's repositories, consider using a newer version of your distribution.
|
||||
- Ubuntu started to ship ffmpeg 4.3 starting with groovy (20.10). If you're using an older version, you could use [this PPA](https://launchpad.net/%7Esavoury1/+archive/ubuntu/ffmpeg4) instead of upgrading. **Using PPAs is dangerous and may break your system. Use it at your own risk.**
|
||||
|
||||
## Windows 10
|
||||
|
||||
### Requirements:
|
||||
|
||||
mingw-w64-x86_64-openssl mingw-w64-x86_64-cmake mingw-w64-x86_64-toolchain mingw-w64-x86_64-opus mingw-w64-x86_64-x265 mingw-w64-x86_64-boost git yasm nasm diffutils make
|
||||
First you need to install [MSYS2](https://www.msys2.org), then startup "MSYS2 MinGW 64-bit" and install the following packages using `pacman -S`:
|
||||
|
||||
mingw-w64-x86_64-binutils mingw-w64-x86_64-openssl mingw-w64-x86_64-cmake mingw-w64-x86_64-toolchain mingw-w64-x86_64-opus mingw-w64-x86_64-x265 mingw-w64-x86_64-boost git mingw-w64-x86_64-make cmake make gcc
|
||||
|
||||
### Compilation:
|
||||
- `git clone https://github.com/loki-47-6F-64/sunshine.git --recursive`
|
||||
- `cd sunshine && mkdir build && cd build`
|
||||
- `cmake -G"Unix Makefiles" ..`
|
||||
- `make`
|
||||
- `mingw32-make`
|
||||
|
||||
### Setup:
|
||||
- **OPTIONAL** Gamepad support: Download and run 'ViGEmBus_Setup_1.16.116.exe' from [https://github.com/ViGEm/ViGEmBus/releases]
|
||||
@@ -82,32 +131,33 @@ sunshine needs access to uinput to create mouse and gamepad events:
|
||||
|
||||
## Usage:
|
||||
- run "sunshine path/to/sunshine.conf"
|
||||
- If running for the first time, make sure to note the username and password Sunshine showed to you, since you **cannot get back later**!
|
||||
- In Moonlight: Add PC manually
|
||||
- When Moonlight request you insert the correct pin on sunshine, either:
|
||||
- Type in the URL bar of your browser: `xxx.xxx.xxx.xxx:47989/pin/####`
|
||||
- `wget xxx.xxx.xxx.xxx:47989/pin/####`
|
||||
- The x's are the IP of your instance, `####` is the pin
|
||||
- When Moonlight request you insert the correct pin on sunshine:
|
||||
- Type in the URL bar of your browser: `https://xxx.xxx.xxx.xxx:47990` where `xxx.xxx.xxx.xxx` is the IP address of your computer
|
||||
- Ignore any warning given by your browser about "insecure website"
|
||||
- You should compile the next page with a new username and a password, needed to login into the next step
|
||||
- Press "Save" and log in using the credentials given above
|
||||
- Go to "PIN" in the Header
|
||||
- Type in your PIN and press Enter, you should get a Success Message
|
||||
- Click on one of the Applications listed
|
||||
- Have fun :)
|
||||
|
||||
## Shortcuts:
|
||||
|
||||
## Note:
|
||||
- The Windows key is not passed through by Moonlight, therefore Sunshine maps Right-Alt key to the Windows key
|
||||
- If you set Video Bitrate to 0.5Mb/s:
|
||||
- Sunshine will use CRF or QP to controll the quality of the stream. (See example configuration file for more details)
|
||||
- This is less CPU intensive and it has lower average bandwith requirements compared to manually setting bitrate to acceptable quality
|
||||
- However, it has higher peak bitrates, forcing Sunshine to drop entire frames when streaming 1080P due to their size.
|
||||
- When this happens, the video portion of the stream appears to be frozen.
|
||||
- This is rare enough that using this for the desktop environment is tolerable (in my opinion), however for gaming not so much.
|
||||
|
||||
All shortcuts start with CTRL + ALT + SHIFT, just like Moonlight
|
||||
- CTRL + ALT + SHIFT + N --> Hide/Unhide the cursor (This may be usefull for Remote Desktop Mode for Moonlight)
|
||||
- CTRL + ALT + SHIFT + F1/F13 --> Switch to different monitor for Streaming
|
||||
|
||||
## Credits:
|
||||
- [Simple-Web-Server](https://gitlab.com/eidheim/Simple-Web-Server)
|
||||
- [Moonlight](https://github.com/moonlight-stream)
|
||||
- [Looking-Glass](https://github.com/gnif/LookingGlass) (For showing me how to properly capture frames on Windows, saving me a lot of time :)
|
||||
- [Eretik](http://eretik.omegahg.com/) (For creating PolicyConfig.h, allowing me to change the default audio device on Windows programmatically)
|
||||
- [Twitter emoji](https://github.com/twitter/twemoji/blob/master/LICENSE-GRAPHICS) (Sunshine's icon is made of twemoji)
|
||||
|
||||
## Application List:
|
||||
**Note:** You can change the Application List in the "Apps" section of the User Interface `https://xxx.xxx.xxx.xxx:47990/`
|
||||
- You can use Environment variables in place of values
|
||||
- $(HOME) will be replaced by the value of $HOME
|
||||
- $$ will be replaced by $ --> $$(HOME) will be replaced by $(HOME)
|
||||
|
||||
Submodule Simple-Web-Server deleted from f37a41d48b
Submodule ViGEmClient deleted from 52682b59c4
38
appveyor.yml
38
appveyor.yml
@@ -1,38 +1,44 @@
|
||||
image:
|
||||
- Ubuntu2004
|
||||
- Visual Studio 2019
|
||||
services:
|
||||
- docker
|
||||
|
||||
environment:
|
||||
matrix:
|
||||
- BUILD_TYPE: Debug
|
||||
- BUILD_TYPE: Release
|
||||
- APPVEYOR_BUILD_WORKER_IMAGE: Ubuntu2004
|
||||
DOCKERFILE: Dockerfile-2004
|
||||
- APPVEYOR_BUILD_WORKER_IMAGE: Ubuntu2004
|
||||
DOCKERFILE: Dockerfile-2104
|
||||
- APPVEYOR_BUILD_WORKER_IMAGE: Ubuntu2004
|
||||
DOCKERFILE: Dockerfile-debian
|
||||
- APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019
|
||||
BUILD_TYPE: Release
|
||||
|
||||
install:
|
||||
- sh: sudo apt update --ignore-missing
|
||||
- sh: sudo apt install -y build-essential fakeroot gcc-10 g++-10 cmake libssl-dev libavdevice-dev libboost-thread-dev libboost-filesystem-dev libboost-log-dev libpulse-dev libopus-dev libxtst-dev libx11-dev libxrandr-dev libxfixes-dev libevdev-dev libxcb1-dev libxcb-shm0-dev libxcb-xfixes0-dev
|
||||
- cmd: C:\msys64\usr\bin\bash -lc "pacman --needed --noconfirm -S mingw-w64-x86_64-openssl mingw-w64-x86_64-cmake mingw-w64-x86_64-toolchain mingw-w64-x86_64-opus mingw-w64-x86_64-x265 mingw-w64-x86_64-boost git yasm nasm diffutils make"
|
||||
- cmd: C:\msys64\usr\bin\bash -lc "pacman --needed --noconfirm -S mingw-w64-x86_64-binutils mingw-w64-x86_64-openssl mingw-w64-x86_64-cmake mingw-w64-x86_64-toolchain mingw-w64-x86_64-opus mingw-w64-x86_64-x265 mingw-w64-x86_64-boost git yasm nasm diffutils make"
|
||||
|
||||
before_build:
|
||||
- git submodule update --init --recursive
|
||||
- mkdir build
|
||||
- cd build
|
||||
- cmd: git submodule update --init --recursive
|
||||
- cmd: mkdir build
|
||||
- cmd: cd build
|
||||
- sh: cd scripts
|
||||
- sh: ./build-container.sh -f $DOCKERFILE
|
||||
|
||||
build_script:
|
||||
- cmd: set OLDPATH=%PATH%
|
||||
- cmd: set PATH=C:\msys64\mingw64\bin
|
||||
- sh: cmake -DCMAKE_C_COMPILER=gcc-10 -DCMAKE_CXX_COMPILER=g++-10 -DCMAKE_BUILD_TYPE=$BUILD_TYPE -DSUNSHINE_EXECUTABLE_PATH=sunshine -DSUNSHINE_ASSETS_DIR=/etc/sunshine ..
|
||||
- cmd: cmake -DCMAKE_BUILD_TYPE=$BUILD_TYPE -DSUNSHINE_ASSETS_DIR=assets -G "MinGW Makefiles" ..
|
||||
- sh: make -j$(nproc)
|
||||
- cmd: mingw32-make -j2
|
||||
- cmd: set PATH=%OLDPATH%
|
||||
- sh: ./build-sunshine.sh -pu
|
||||
|
||||
after_build:
|
||||
- sh: ./gen-deb
|
||||
- cmd: Del ..\assets\apps_linux.json
|
||||
- cmd: 7z a Sunshine-Windows.zip ..\assets
|
||||
- cmd: 7z a Sunshine-Windows.zip sunshine.exe
|
||||
- cmd: 7z a Sunshine-Windows.zip tools\dxgi-info.exe
|
||||
- cmd: 7z a Sunshine-Windows.zip tools\audio-info.exe
|
||||
- cmd: 7z a Sunshine-Windows.zip tools\sunshinesvc.exe
|
||||
- cmd: 7z a Sunshine-Windows.zip ..\tools\install-service.bat
|
||||
- cmd: 7z a Sunshine-Windows.zip ..\tools\uninstall-service.bat
|
||||
- cmd: appveyor PushArtifact Sunshine-Windows.zip
|
||||
- sh: appveyor PushArtifact package-deb/sunshine.deb
|
||||
- sh: appveyor PushArtifact sunshine.service
|
||||
- sh: appveyor PushArtifact sunshine-build/sunshine.deb
|
||||
|
||||
|
||||
35
assets/shaders/opengl/ConvertUV.frag
Normal file
35
assets/shaders/opengl/ConvertUV.frag
Normal file
@@ -0,0 +1,35 @@
|
||||
#version 300 es
|
||||
|
||||
#ifdef GL_ES
|
||||
precision lowp float;
|
||||
#endif
|
||||
|
||||
uniform sampler2D image;
|
||||
|
||||
layout(shared) uniform ColorMatrix {
|
||||
vec4 color_vec_y;
|
||||
vec4 color_vec_u;
|
||||
vec4 color_vec_v;
|
||||
vec2 range_y;
|
||||
vec2 range_uv;
|
||||
};
|
||||
|
||||
in vec3 uuv;
|
||||
layout(location = 0) out vec2 color;
|
||||
|
||||
//--------------------------------------------------------------------------------------
|
||||
// Pixel Shader
|
||||
//--------------------------------------------------------------------------------------
|
||||
void main() {
|
||||
vec3 rgb_left = texture(image, uuv.xz).rgb;
|
||||
vec3 rgb_right = texture(image, uuv.yz).rgb;
|
||||
vec3 rgb = (rgb_left + rgb_right) * 0.5;
|
||||
|
||||
float u = dot(color_vec_u.xyz, rgb) + color_vec_u.w;
|
||||
float v = dot(color_vec_v.xyz, rgb) + color_vec_v.w;
|
||||
|
||||
u = u * range_uv.x + range_uv.y;
|
||||
v = v * range_uv.x + range_uv.y;
|
||||
|
||||
color = vec2(u, v * 224.0f / 256.0f + 0.0625);
|
||||
}
|
||||
27
assets/shaders/opengl/ConvertUV.vert
Normal file
27
assets/shaders/opengl/ConvertUV.vert
Normal file
@@ -0,0 +1,27 @@
|
||||
#version 300 es
|
||||
|
||||
#ifdef GL_ES
|
||||
precision mediump float;
|
||||
#endif
|
||||
|
||||
uniform float width_i;
|
||||
|
||||
out vec3 uuv;
|
||||
//--------------------------------------------------------------------------------------
|
||||
// Vertex Shader
|
||||
//--------------------------------------------------------------------------------------
|
||||
void main()
|
||||
{
|
||||
float idHigh = float(gl_VertexID >> 1);
|
||||
float idLow = float(gl_VertexID & int(1));
|
||||
|
||||
float x = idHigh * 4.0 - 1.0;
|
||||
float y = idLow * 4.0 - 1.0;
|
||||
|
||||
float u_right = idHigh * 2.0;
|
||||
float u_left = u_right - width_i;
|
||||
float v = idLow * 2.0;
|
||||
|
||||
uuv = vec3(u_left, u_right, v);
|
||||
gl_Position = vec4(x, y, 0.0, 1.0);
|
||||
}
|
||||
26
assets/shaders/opengl/ConvertY.frag
Normal file
26
assets/shaders/opengl/ConvertY.frag
Normal file
@@ -0,0 +1,26 @@
|
||||
#version 300 es
|
||||
|
||||
#ifdef GL_ES
|
||||
precision lowp float;
|
||||
#endif
|
||||
|
||||
uniform sampler2D image;
|
||||
|
||||
layout(shared) uniform ColorMatrix {
|
||||
vec4 color_vec_y;
|
||||
vec4 color_vec_u;
|
||||
vec4 color_vec_v;
|
||||
vec2 range_y;
|
||||
vec2 range_uv;
|
||||
};
|
||||
|
||||
in vec2 tex;
|
||||
layout(location = 0) out float color;
|
||||
|
||||
void main()
|
||||
{
|
||||
vec3 rgb = texture(image, tex).rgb;
|
||||
float y = dot(color_vec_y.xyz, rgb);
|
||||
|
||||
color = y * range_y.x + range_y.y;
|
||||
}
|
||||
14
assets/shaders/opengl/Scene.frag
Normal file
14
assets/shaders/opengl/Scene.frag
Normal file
@@ -0,0 +1,14 @@
|
||||
#version 300 es
|
||||
|
||||
#ifdef GL_ES
|
||||
precision lowp float;
|
||||
#endif
|
||||
|
||||
uniform sampler2D image;
|
||||
|
||||
in vec2 tex;
|
||||
layout(location = 0) out vec4 color;
|
||||
void main()
|
||||
{
|
||||
color = texture(image, tex);
|
||||
}
|
||||
22
assets/shaders/opengl/Scene.vert
Normal file
22
assets/shaders/opengl/Scene.vert
Normal file
@@ -0,0 +1,22 @@
|
||||
#version 300 es
|
||||
|
||||
#ifdef GL_ES
|
||||
precision mediump float;
|
||||
#endif
|
||||
|
||||
out vec2 tex;
|
||||
|
||||
void main()
|
||||
{
|
||||
float idHigh = float(gl_VertexID >> 1);
|
||||
float idLow = float(gl_VertexID & int(1));
|
||||
|
||||
float x = idHigh * 4.0 - 1.0;
|
||||
float y = idLow * 4.0 - 1.0;
|
||||
|
||||
float u = idHigh * 2.0;
|
||||
float v = idLow * 2.0;
|
||||
|
||||
gl_Position = vec4(x, y, 0.0, 1.0);
|
||||
tex = vec2(u, v);
|
||||
}
|
||||
@@ -1,233 +1,286 @@
|
||||
# If no external IP address is given, the local IP address is used
|
||||
# external_ip = 123.456.789.12
|
||||
|
||||
# The private key must be 2048 bits
|
||||
# pkey = /dir/pkey.pem
|
||||
|
||||
# The certificate must be signed with a 2048 bit key
|
||||
# cert = /dir/cert.pem
|
||||
|
||||
# The name displayed by Moonlight
|
||||
# If not specified, the PC's hostname is used
|
||||
# sunshine_name = Sunshine
|
||||
|
||||
# The minimum log level printed to standard out
|
||||
#
|
||||
# none -> no logs are printed to standard out
|
||||
#
|
||||
# verbose = [0]
|
||||
# debug = [1]
|
||||
# info = [2]
|
||||
# warning = [3]
|
||||
# error = [4]
|
||||
# fatal = [5]
|
||||
# none = [6]
|
||||
#
|
||||
# min_log_level = info
|
||||
|
||||
# The origin of the remote endpoint address that is not denied for HTTP method /pin
|
||||
# Could be any of the following values:
|
||||
# pc|lan|wan
|
||||
# pc: Only localhost may access /pin
|
||||
# lan: Only those in LAN may access /pin
|
||||
# wan: Anyone may access /pin
|
||||
#
|
||||
# origin_pin_allowed = lan
|
||||
|
||||
# The file where current state of Sunshine is stored
|
||||
# file_state = sunshine_state.json
|
||||
|
||||
# The display modes advertised by Sunshine
|
||||
#
|
||||
# Some versions of Moonlight, such as Moonlight-nx (Switch),
|
||||
# rely on this list to ensure that the requested resolutions and fps
|
||||
# are supported.
|
||||
#
|
||||
# fps = [10, 30, 60, 90, 120]
|
||||
# resolutions = [
|
||||
# 352x240,
|
||||
# 480x360,
|
||||
# 858x480,
|
||||
# 1280x720,
|
||||
# 1920x1080,
|
||||
# 2560x1080,
|
||||
# 3440x1440,
|
||||
# 1920x1200,
|
||||
# 3860x2160,
|
||||
# 3840x1600,
|
||||
# ]
|
||||
|
||||
# How long to wait in milliseconds for data from moonlight before shutting down the stream
|
||||
# ping_timeout = 2000
|
||||
|
||||
# The file where configuration for the different applications that Sunshine can run during a stream
|
||||
# file_apps = apps.json
|
||||
|
||||
# How much error correcting packets must be send for every video
|
||||
# This is just some random number, don't know the optimal value
|
||||
# The higher fec_percentage, the lower space for the actual data to send per frame there is
|
||||
#
|
||||
# The value must be greater than 0 and lower than or equal to 100
|
||||
# fec_percentage = 10
|
||||
|
||||
# When multicasting, it could be usefull to have different configurations for each connected Client.
|
||||
# For example:
|
||||
# Clients connected through WAN and LAN have different bitrate contstraints.
|
||||
# Decoders may require different settings for color
|
||||
#
|
||||
# Unlike simply broadcasting to multiple Client, this will generate distinct video streams.
|
||||
# Note, CPU usage increases for each distinct video stream generated
|
||||
# channels = 1
|
||||
|
||||
# The back/select button on the controller
|
||||
# On the Shield, the home and powerbutton are not passed to Moonlight
|
||||
# If, after the timeout, the back button is still pressed down, Home/Guide button press is emulated.
|
||||
# If back_button_timeout < 0, then the Home/Guide button will not be emulated
|
||||
# back_button_timeout = 2000
|
||||
|
||||
# !! Windows only !!
|
||||
# Control how fast keys will repeat themselves
|
||||
# The initial delay in milliseconds before repeating keys
|
||||
# key_repeat_delay = 500
|
||||
#
|
||||
# How often keys repeat every second
|
||||
# This configurable option supports decimals
|
||||
# key_repeat_frequency = 24.9
|
||||
|
||||
# The name of the audio sink used for Audio Loopback
|
||||
# If you do not specify this variable, pulseaudio will select the default monitor device.
|
||||
#
|
||||
# You can find the name of the audio sink using the following command:
|
||||
# !! Linux only !!
|
||||
# pacmd list-sinks | grep "name:"
|
||||
# audio_sink = alsa_output.pci-0000_09_00.3.analog-stereo
|
||||
#
|
||||
# !! Windows only !!
|
||||
# tools\audio-info.exe
|
||||
# audio_sink = {0.0.0.00000000}.{FD47D9CC-4218-4135-9CE2-0C195C87405B}
|
||||
#
|
||||
# The virtual sink, is the audio device that's virtual (Like Steam Streaming Speakers), it allows Sunshine
|
||||
# to stream audio, while muting the speakers.
|
||||
# virtual_sink = {0.0.0.00000000}.{8edba70c-1125-467c-b89c-15da389bc1d4}
|
||||
|
||||
# !! Windows only !!
|
||||
# You can select the video card you want to stream:
|
||||
# The appropriate values can be found using the following command:
|
||||
# tools\dxgi-info.exe
|
||||
# adapter_name = Radeon RX 580 Series
|
||||
# output_name = \\.\DISPLAY1
|
||||
|
||||
# !! Linux only !!
|
||||
# Set the display number to stream. I have no idea how they are numbered. They start from 0, usually.
|
||||
# output_name = 0
|
||||
|
||||
###############################################
|
||||
# FFmpeg software encoding parameters
|
||||
# Honestly, I have no idea what the optimal values would be.
|
||||
# Play around with this :)
|
||||
|
||||
# Constant Rate Factor. Between 1 and 52. It allows QP to go up during motion and down with still image, resulting in constant perceived quality
|
||||
# Higher value means more compression, but less quality
|
||||
# If crf == 0, then use QP directly instead
|
||||
# crf = 0
|
||||
|
||||
# Quantitization Parameter
|
||||
# Higher value means more compression, but less quality
|
||||
# If crf != 0, then this parameter is ignored
|
||||
# qp = 28
|
||||
|
||||
# Minimum number of threads used by ffmpeg to encode the video.
|
||||
# Increasing the value slightly reduces encoding efficiency, but the tradeoff is usually
|
||||
# worth it to gain the use of more CPU cores for encoding. The ideal value is the lowest
|
||||
# value that can reliably encode at your desired streaming settings on your hardware.
|
||||
# min_threads = 1
|
||||
|
||||
# Allows the client to request HEVC Main or HEVC Main10 video streams.
|
||||
# HEVC is more CPU-intensive to encode, so enabling this may reduce performance when using software encoding.
|
||||
# If set to 0 (default), Sunshine will specify support for HEVC based on encoder
|
||||
# If set to 1, Sunshine will not advertise support for HEVC
|
||||
# If set to 2, Sunshine will advertise support for HEVC Main profile
|
||||
# If set to 3, Sunshine will advertise support for HEVC Main and Main10 (HDR) profiles
|
||||
# hevc_mode = 2
|
||||
|
||||
# Force a specific encoder, otherwise Sunshine will use the first encoder that is available
|
||||
# supported encoders:
|
||||
# nvenc
|
||||
# amdvce # NOTE: alpha stage. The cursor is not yet displayed
|
||||
# software
|
||||
#
|
||||
# encoder = nvenc
|
||||
##################################### Software #####################################
|
||||
# See x264 --fullhelp for the different presets
|
||||
# sw_preset = superfast
|
||||
# sw_tune = zerolatency
|
||||
#
|
||||
|
||||
##################################### NVENC #####################################
|
||||
###### presets ###########
|
||||
# default
|
||||
# hp -- high performance
|
||||
# hq -- high quality
|
||||
# slow -- hq 2 passes
|
||||
# medium -- hq 1 pass
|
||||
# fast -- hp 1 pass
|
||||
# bd
|
||||
# ll -- low latency
|
||||
# llhq
|
||||
# llhp
|
||||
# lossless
|
||||
# losslesshp
|
||||
##########################
|
||||
# nv_preset = llhq
|
||||
#
|
||||
####### rate control #####
|
||||
# auto -- let ffmpeg decide rate control
|
||||
# constqp -- constant QP mode
|
||||
# vbr -- variable bitrate
|
||||
# cbr -- constant bitrate
|
||||
# cbr_hq -- cbr high quality
|
||||
# cbr_ld_hq -- cbr low delay high quality
|
||||
# vbr_hq -- vbr high quality
|
||||
##########################
|
||||
# nv_rc = auto
|
||||
|
||||
###### h264 entropy ######
|
||||
# auto -- let ffmpeg nvenc decide the entropy encoding
|
||||
# cabac
|
||||
# cavlc
|
||||
##########################
|
||||
# nv_coder = auto
|
||||
|
||||
##################################### AMD #####################################
|
||||
###### presets ###########
|
||||
# default
|
||||
# speed
|
||||
# balanced
|
||||
##########################
|
||||
# amd_preset = balanced
|
||||
#
|
||||
####### rate control #####
|
||||
# auto -- let ffmpeg decide rate control
|
||||
# constqp -- constant QP mode
|
||||
# vbr_latency -- Latency Constrained Variable Bitrate
|
||||
# vbr_peak -- Peak Contrained Variable Bitrate
|
||||
# cbr -- constant bitrate
|
||||
##########################
|
||||
# amd_rc = auto
|
||||
|
||||
###### h264 entropy ######
|
||||
# auto -- let ffmpeg nvenc decide the entropy encoding
|
||||
# cabac
|
||||
# cavlc
|
||||
##########################
|
||||
# amd_coder = auto
|
||||
|
||||
##############################################
|
||||
# Some configurable parameters, are merely toggles for specific features
|
||||
# The first occurrence turns it on, the second occurence turns it off, the third occurence turns it on again, etc, etc
|
||||
# Here, you change the default state of any flag
|
||||
#
|
||||
# To set the initial state of flags -0 and -1 to on, set the following flags:
|
||||
# flags = 01
|
||||
#
|
||||
# See: sunshine --help for all options under the header: flags
|
||||
# If no external IP address is given, Sunshine will attempt to automatically detect external ip-address
|
||||
# external_ip = 123.456.789.12
|
||||
|
||||
# Set the familly of ports used by Sunshine
|
||||
# port = 47989
|
||||
|
||||
# The private key must be 2048 bits
|
||||
# pkey = /dir/pkey.pem
|
||||
|
||||
# The certificate must be signed with a 2048 bit key
|
||||
# cert = /dir/cert.pem
|
||||
|
||||
# The name displayed by Moonlight
|
||||
# If not specified, the PC's hostname is used
|
||||
# sunshine_name = Sunshine
|
||||
|
||||
# The minimum log level printed to standard out
|
||||
#
|
||||
# none -> no logs are printed to standard out
|
||||
#
|
||||
# verbose = [0]
|
||||
# debug = [1]
|
||||
# info = [2]
|
||||
# warning = [3]
|
||||
# error = [4]
|
||||
# fatal = [5]
|
||||
# none = [6]
|
||||
#
|
||||
# min_log_level = info
|
||||
|
||||
# The origin of the remote endpoint address that is not denied for HTTP method /pin
|
||||
# Could be any of the following values:
|
||||
# pc|lan|wan
|
||||
# pc: Only localhost may access /pin
|
||||
# lan: Only those in LAN may access /pin
|
||||
# wan: Anyone may access /pin
|
||||
#
|
||||
# origin_pin_allowed = pc
|
||||
|
||||
# The origin of the remote endpoint address that is not denied for HTTPS Web UI
|
||||
# Could be any of the following values:
|
||||
# pc|lan|wan
|
||||
# pc: Only localhost may access the Web Manager
|
||||
# lan: Only those in LAN may access the Web Manager
|
||||
# wan: Anyone may access the Web Manager
|
||||
#
|
||||
# origin_web_ui_allowed = lan
|
||||
|
||||
# If UPnP is enabled, Sunshine will attempt to open ports for streaming over the internet
|
||||
# To enable it, uncomment the following line:
|
||||
# upnp = on
|
||||
|
||||
# The file where current state of Sunshine is stored
|
||||
# file_state = sunshine_state.json
|
||||
|
||||
# The file where user credentials for the UI are stored
|
||||
# By default, credentials are stored in `file_state`
|
||||
# credentials_file = sunshine_state.json
|
||||
|
||||
# The display modes advertised by Sunshine
|
||||
#
|
||||
# Some versions of Moonlight, such as Moonlight-nx (Switch),
|
||||
# rely on this list to ensure that the requested resolutions and fps
|
||||
# are supported.
|
||||
#
|
||||
# fps = [10, 30, 60, 90, 120]
|
||||
# resolutions = [
|
||||
# 352x240,
|
||||
# 480x360,
|
||||
# 858x480,
|
||||
# 1280x720,
|
||||
# 1920x1080,
|
||||
# 2560x1080,
|
||||
# 3440x1440,
|
||||
# 1920x1200,
|
||||
# 3860x2160,
|
||||
# 3840x1600,
|
||||
# ]
|
||||
|
||||
# Sometimes it may be usefull to map keybindings.
|
||||
# Wayland won't allow clients to capture the Win Key for example
|
||||
#
|
||||
# See https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
|
||||
#
|
||||
# Note:
|
||||
# keybindings needs to have a multiple of two elements
|
||||
# keybindings = [
|
||||
# 0x10, 0xA0,
|
||||
# 0x11, 0xA2,
|
||||
# 0x12, 0xA4,
|
||||
# 0x4A, 0x4B
|
||||
# ]
|
||||
|
||||
# How long to wait in milliseconds for data from moonlight before shutting down the stream
|
||||
# ping_timeout = 10000
|
||||
|
||||
# The file where configuration for the different applications that Sunshine can run during a stream
|
||||
# file_apps = apps.json
|
||||
|
||||
# Percentage of error correcting packets per data packet in each video frame
|
||||
# Higher values can correct for more network packet loss, but at the cost of increasing bandwidth usage
|
||||
# The default value of 20 is what GeForce Experience uses
|
||||
#
|
||||
# The value must be greater than 0 and lower than or equal to 255
|
||||
# fec_percentage = 20
|
||||
|
||||
# When multicasting, it could be usefull to have different configurations for each connected Client.
|
||||
# For example:
|
||||
# Clients connected through WAN and LAN have different bitrate contstraints.
|
||||
# Decoders may require different settings for color
|
||||
#
|
||||
# Unlike simply broadcasting to multiple Client, this will generate distinct video streams.
|
||||
# Note, CPU usage increases for each distinct video stream generated
|
||||
# channels = 1
|
||||
|
||||
# The back/select button on the controller
|
||||
# On the Shield, the home and powerbutton are not passed to Moonlight
|
||||
# If, after the timeout, the back button is still pressed down, Home/Guide button press is emulated.
|
||||
# If back_button_timeout < 0, then the Home/Guide button will not be emulated
|
||||
# back_button_timeout = 2000
|
||||
|
||||
# !! Windows only !!
|
||||
# Gamepads supported by Sunshine
|
||||
# Possible values:
|
||||
# x360 -- xbox 360 controller
|
||||
# ds4 -- dualshock controller (PS4)
|
||||
# gamepad = x360
|
||||
|
||||
# Control how fast keys will repeat themselves
|
||||
# The initial delay in milliseconds before repeating keys
|
||||
# key_repeat_delay = 500
|
||||
#
|
||||
# How often keys repeat every second
|
||||
# This configurable option supports decimals
|
||||
# key_repeat_frequency = 24.9
|
||||
|
||||
# The name of the audio sink used for Audio Loopback
|
||||
# If you do not specify this variable, pulseaudio will select the default monitor device.
|
||||
#
|
||||
# You can find the name of the audio sink using the following command:
|
||||
# !! Linux only !!
|
||||
# pacmd list-sinks | grep "name:" if running vanilla pulseaudio
|
||||
# pPipewire: Use `pactl info | grep Source`. In some causes you'd need to use the `sink` device. Try `pactl info | grep Sink`, if _Source_ doesn't work
|
||||
# audio_sink = alsa_output.pci-0000_09_00.3.analog-stereo
|
||||
#
|
||||
# !! Windows only !!
|
||||
# tools\audio-info.exe
|
||||
# audio_sink = {0.0.0.00000000}.{FD47D9CC-4218-4135-9CE2-0C195C87405B}
|
||||
#
|
||||
# The virtual sink, is the audio device that's virtual (Like Steam Streaming Speakers), it allows Sunshine
|
||||
# to stream audio, while muting the speakers.
|
||||
# virtual_sink = {0.0.0.00000000}.{8edba70c-1125-467c-b89c-15da389bc1d4}
|
||||
|
||||
# !! Windows only !!
|
||||
# You can select the video card you want to stream:
|
||||
# The appropriate values can be found using the following command:
|
||||
# tools\dxgi-info.exe
|
||||
# adapter_name = Radeon RX 580 Series
|
||||
# output_name = \\.\DISPLAY1
|
||||
|
||||
# !! Linux only !!
|
||||
# Set the display number to stream.
|
||||
# You can find them by the following command:
|
||||
# xrandr --listmonitors
|
||||
# Example output: "0: +HDMI-1 1920/518x1200/324+0+0 HDMI-1"
|
||||
# ^ <-- You need this.
|
||||
# output_name = 0
|
||||
|
||||
###############################################
|
||||
# FFmpeg software encoding parameters
|
||||
# Honestly, I have no idea what the optimal values would be.
|
||||
# Play around with this :)
|
||||
|
||||
# Quantitization Parameter
|
||||
# Some devices don't support Constant Bit Rate. For those devices, QP is used instead
|
||||
# Higher value means more compression, but less quality
|
||||
# qp = 28
|
||||
|
||||
# Minimum number of threads used by ffmpeg to encode the video.
|
||||
# Increasing the value slightly reduces encoding efficiency, but the tradeoff is usually
|
||||
# worth it to gain the use of more CPU cores for encoding. The ideal value is the lowest
|
||||
# value that can reliably encode at your desired streaming settings on your hardware.
|
||||
# min_threads = 1
|
||||
|
||||
# Allows the client to request HEVC Main or HEVC Main10 video streams.
|
||||
# HEVC is more CPU-intensive to encode, so enabling this may reduce performance when using software encoding.
|
||||
# If set to 0 (default), Sunshine will specify support for HEVC based on encoder
|
||||
# If set to 1, Sunshine will not advertise support for HEVC
|
||||
# If set to 2, Sunshine will advertise support for HEVC Main profile
|
||||
# If set to 3, Sunshine will advertise support for HEVC Main and Main10 (HDR) profiles
|
||||
# hevc_mode = 2
|
||||
|
||||
# Force a specific encoder, otherwise Sunshine will use the first encoder that is available
|
||||
# supported encoders:
|
||||
# nvenc
|
||||
# amdvce # NOTE: alpha stage. The cursor is not yet displayed
|
||||
# software
|
||||
#
|
||||
# encoder = nvenc
|
||||
##################################### Software #####################################
|
||||
# See x264 --fullhelp for the different presets
|
||||
# sw_preset = superfast
|
||||
# sw_tune = zerolatency
|
||||
#
|
||||
|
||||
##################################### NVENC #####################################
|
||||
###### presets ###########
|
||||
# default
|
||||
# hp -- high performance
|
||||
# hq -- high quality
|
||||
# slow -- hq 2 passes
|
||||
# medium -- hq 1 pass
|
||||
# fast -- hp 1 pass
|
||||
# bd
|
||||
# ll -- low latency
|
||||
# llhq
|
||||
# llhp
|
||||
# lossless
|
||||
# losslesshp
|
||||
##########################
|
||||
# nv_preset = llhq
|
||||
#
|
||||
####### rate control #####
|
||||
# auto -- let ffmpeg decide rate control
|
||||
# constqp -- constant QP mode
|
||||
# vbr -- variable bitrate
|
||||
# cbr -- constant bitrate
|
||||
# cbr_hq -- cbr high quality
|
||||
# cbr_ld_hq -- cbr low delay high quality
|
||||
# vbr_hq -- vbr high quality
|
||||
##########################
|
||||
# nv_rc = auto
|
||||
|
||||
###### h264 entropy ######
|
||||
# auto -- let ffmpeg nvenc decide the entropy encoding
|
||||
# cabac
|
||||
# cavlc
|
||||
##########################
|
||||
# nv_coder = auto
|
||||
|
||||
##################################### AMD #####################################
|
||||
###### presets ###########
|
||||
# default
|
||||
# speed
|
||||
# balanced
|
||||
##########################
|
||||
# amd_preset = balanced
|
||||
#
|
||||
####### rate control #####
|
||||
# auto -- let ffmpeg decide rate control
|
||||
# constqp -- constant QP mode
|
||||
# vbr_latency -- Latency Constrained Variable Bitrate
|
||||
# vbr_peak -- Peak Contrained Variable Bitrate
|
||||
# cbr -- constant bitrate
|
||||
##########################
|
||||
# amd_rc = auto
|
||||
|
||||
###### h264 entropy ######
|
||||
# auto -- let ffmpeg nvenc decide the entropy encoding
|
||||
# cabac
|
||||
# cavlc
|
||||
##########################
|
||||
# amd_coder = auto
|
||||
|
||||
#################################### VAAPI ###################################
|
||||
####### adapter ##########
|
||||
# Unlike with `amdvce` and `nvenc`, it doesn't matter if video encoding is done
|
||||
# on a different GPU.
|
||||
# Run the following commands:
|
||||
# 1. ls /dev/dri/renderD*
|
||||
# to find all devices capable of VAAPI
|
||||
# 2. vainfo --display drm --device /dev/dri/renderD129 | grep -E "((VAProfileH264High|VAProfileHEVCMain|VAProfileHEVCMain10).*VAEntrypointEncSlice)|Driver version"
|
||||
# Lists the name and capabilities of the device.
|
||||
# To be supported by Sunshine, it needs to have at the very minimum:
|
||||
# VAProfileH264High : VAEntrypointEncSlice
|
||||
# adapter_name = /dev/dri/renderD128
|
||||
|
||||
##############################################
|
||||
# Some configurable parameters, are merely toggles for specific features
|
||||
# The first occurrence turns it on, the second occurence turns it off, the third occurence turns it on again, etc, etc
|
||||
# Here, you change the default state of any flag
|
||||
#
|
||||
# To set the initial state of flags -0 and -1 to on, set the following flags:
|
||||
# flags = 012
|
||||
#
|
||||
# See: sunshine --help for all options under the header: flags
|
||||
|
||||
260
assets/web/apps.html
Normal file
260
assets/web/apps.html
Normal file
@@ -0,0 +1,260 @@
|
||||
<div id="app" class="container">
|
||||
<div class="my-4">
|
||||
<h1>Applications</h1>
|
||||
<div>Applications are refreshed only when Client is restarted</div>
|
||||
</div>
|
||||
<div class="card p-4">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(app,i) in apps" :key="i">
|
||||
<td>{{app.name}}</td>
|
||||
<td>
|
||||
<button class="btn btn-primary" @click="editApp(i)">Edit</button>
|
||||
<button class="btn btn-danger" @click="showDeleteForm(i)">
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="edit-form card mt-2" v-if="showEditForm">
|
||||
<div class="p-4">
|
||||
<!--name-->
|
||||
<div class="mb-3">
|
||||
<label for="appName" class="form-label">Application Name</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="appName"
|
||||
aria-describedby="appNameHelp"
|
||||
v-model="editForm.name"
|
||||
/>
|
||||
<div id="appNameHelp" class="form-text">
|
||||
Application Name, as shown on Moonlight
|
||||
</div>
|
||||
</div>
|
||||
<!--output-->
|
||||
<div class="mb-3">
|
||||
<label for="appOutput" class="form-label">Output</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control monospace"
|
||||
id="appOutput"
|
||||
aria-describedby="appOutputHelp"
|
||||
v-model="editForm.output"
|
||||
/>
|
||||
<div id="appOutputHelp" class="form-text">
|
||||
The file where the output of the command is stored, if it is not
|
||||
specified, the output is ignored
|
||||
</div>
|
||||
</div>
|
||||
<!--prep-cmd-->
|
||||
<div class="mb-3 d-flex flex-column">
|
||||
<label for="appName" class="form-label">Command Preparations</label>
|
||||
<div class="form-text">
|
||||
A list of commands to be run before/after the application. <br />
|
||||
If any of the prep-commands fail, starting the application is aborted
|
||||
</div>
|
||||
<table v-if="editForm['prep-cmd'].length > 0">
|
||||
<thead>
|
||||
<th class="precmd-head">Do</th>
|
||||
<th class="precmd-head">Undo</th>
|
||||
<th style="width: 48px"></th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(c,i) in editForm['prep-cmd']">
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control monospace"
|
||||
v-model="c.do"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control monospace"
|
||||
v-model="c.undo"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
class="btn btn-danger"
|
||||
@click="editForm['prep-cmd'].splice(i,1)"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<button
|
||||
class="mt-2 btn btn-success"
|
||||
style="margin: 0 auto"
|
||||
@click="addPrepCmd"
|
||||
>
|
||||
+ Add
|
||||
</button>
|
||||
</div>
|
||||
<!--detatched-->
|
||||
<div class="mb-3">
|
||||
<label for="appName" class="form-label">Detached Commands</label>
|
||||
<div
|
||||
v-for="(c,i) in editForm.detached"
|
||||
class="d-flex justify-content-between my-2"
|
||||
>
|
||||
<pre>{{c}}</pre>
|
||||
<button
|
||||
class="btn btn-danger mx-2"
|
||||
@click="editForm.detached.splice(i,1)"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control monospace"
|
||||
v-model="detachedCmd"
|
||||
/>
|
||||
<button
|
||||
class="btn btn-success mx-2"
|
||||
@click="editForm.detached.push(detachedCmd);detachedCmd = '';"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-text">
|
||||
A list of commands to be run and forgotten about
|
||||
</div>
|
||||
</div>
|
||||
<!--command-->
|
||||
<div class="mb-3">
|
||||
<label for="appCmd" class="form-label">Command</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control monospace"
|
||||
id="appCmd"
|
||||
aria-describedby="appCmdHelp"
|
||||
v-model="editForm.cmd"
|
||||
/>
|
||||
<div id="appCmdHelp" class="form-text">
|
||||
The main application, if it is not specified, a processs is started
|
||||
that sleeps indefinitely
|
||||
</div>
|
||||
</div>
|
||||
<!--working dir-->
|
||||
<div class="mb-3">
|
||||
<label for="appWorkingDir" class="form-label">Working Directory</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control monospace"
|
||||
id="appWorkingDir"
|
||||
aria-describedby="appWorkingDirHelp"
|
||||
v-model="editForm['working-dir']"
|
||||
/>
|
||||
<div id="appWorkingDirHelp" class="form-text">
|
||||
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
|
||||
</div>
|
||||
</div>
|
||||
<!--buttons-->
|
||||
<div class="d-flex">
|
||||
<button @click="showEditForm = false" class="btn btn-secondary m-2">
|
||||
Cancel
|
||||
</button>
|
||||
<button class="btn btn-primary m-2" @click="save">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2" v-else>
|
||||
<button class="btn btn-primary" @click="newApp">+ Add New</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
new Vue({
|
||||
el: "#app",
|
||||
data() {
|
||||
return {
|
||||
apps: [],
|
||||
showEditForm: false,
|
||||
editForm: null,
|
||||
detachedCmd: "",
|
||||
};
|
||||
},
|
||||
created() {
|
||||
fetch("/api/apps")
|
||||
.then((r) => r.json())
|
||||
.then((r) => {
|
||||
console.log(r);
|
||||
this.apps = r.apps;
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
newApp() {
|
||||
this.editForm = {
|
||||
name: "",
|
||||
output: "",
|
||||
cmd: [],
|
||||
index: -1,
|
||||
"prep-cmd": [],
|
||||
detached: [],
|
||||
};
|
||||
this.editForm.index = -1;
|
||||
this.showEditForm = true;
|
||||
},
|
||||
editApp(id) {
|
||||
this.editForm = JSON.parse(JSON.stringify(this.apps[id]));
|
||||
this.$set(this.editForm, "index", id);
|
||||
if (this.editForm["prep-cmd"] === undefined)
|
||||
this.$set(this.editForm, "prep-cmd", []);
|
||||
if (this.editForm["detached"] === undefined)
|
||||
this.$set(this.editForm, "detached", []);
|
||||
this.showEditForm = true;
|
||||
},
|
||||
showDeleteForm(id) {
|
||||
let resp = confirm(
|
||||
"Are you sure to delete " + this.apps[id].name + "?"
|
||||
);
|
||||
if (resp) {
|
||||
fetch("/api/apps/" + id, { method: "DELETE" }).then((r) => {
|
||||
if (r.status == 200) document.location.reload();
|
||||
});
|
||||
}
|
||||
},
|
||||
addPrepCmd() {
|
||||
this.editForm["prep-cmd"].push({
|
||||
do: "",
|
||||
undo: "",
|
||||
});
|
||||
},
|
||||
save() {
|
||||
fetch("/api/apps", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(this.editForm),
|
||||
}).then((r) => {
|
||||
if (r.status == 200) document.location.reload();
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.precmd-head {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.monospace {
|
||||
font-family: monospace;
|
||||
}
|
||||
</style>
|
||||
3
assets/web/clients.html
Normal file
3
assets/web/clients.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<div id="content" class="container">
|
||||
<h1>Clients</h1>
|
||||
</div>
|
||||
886
assets/web/config.html
Normal file
886
assets/web/config.html
Normal file
@@ -0,0 +1,886 @@
|
||||
<div id="app" class="container">
|
||||
<h1 class="my-4">Configuration</h1>
|
||||
<div class="form" v-if="config">
|
||||
<!--Header-->
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item" v-for="tab in tabs" :key="tab.id">
|
||||
<a
|
||||
class="nav-link"
|
||||
:class="{'active': tab.id === currentTab}"
|
||||
href="#"
|
||||
@click="currentTab = tab.id"
|
||||
>{{tab.name}}</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
<!--General Tab-->
|
||||
<div v-if="currentTab === 'general'" class="config-page">
|
||||
<!--Sunshine Name-->
|
||||
<div class="mb-3">
|
||||
<label for="sunshine_name" class="form-label">Sunshine Name</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="sunshine_name"
|
||||
placeholder="Sunshine"
|
||||
v-model="config.sunshine_name"
|
||||
/>
|
||||
<div class="form-text">
|
||||
The name displayed by Moonlight. If not specified, the PC's hostname
|
||||
is used
|
||||
</div>
|
||||
</div>
|
||||
<!--Log Level-->
|
||||
<div class="mb-3">
|
||||
<label for="min_log_level" class="form-label">Log Level</label>
|
||||
<select
|
||||
id="min_log_level"
|
||||
class="form-select"
|
||||
v-model="config.min_log_level"
|
||||
>
|
||||
<option :value="0">Verbose</option>
|
||||
<option :value="1">Debug</option>
|
||||
<option :value="2">Info</option>
|
||||
<option :value="3">Warning</option>
|
||||
<option :value="4">Error</option>
|
||||
<option :value="5">Fatal</option>
|
||||
<option :value="6">None</option>
|
||||
</select>
|
||||
<div class="form-text">
|
||||
The minimum log level printed to standard out
|
||||
</div>
|
||||
</div>
|
||||
<!--Origin Web UI Allowed-->
|
||||
<div class="mb-3">
|
||||
<label for="origin_web_ui_allowed" class="form-label"
|
||||
>Origin Web UI Allowed</label
|
||||
>
|
||||
<select
|
||||
id="origin_web_ui_allowed"
|
||||
class="form-select"
|
||||
v-model="config.origin_web_ui_allowed"
|
||||
>
|
||||
<option value="pc">Only localhost may access Web UI</option>
|
||||
<option value="lan">Only those in LAN may access Web UI</option>
|
||||
<option value="wan">Anyone may access Web UI</option>
|
||||
</select>
|
||||
<div class="form-text">
|
||||
The origin of the remote endpoint address that is not denied access to
|
||||
Web UI
|
||||
</div>
|
||||
</div>
|
||||
<!--UPnP-->
|
||||
<div class="mb-3">
|
||||
<label for="upnp" class="form-label">UPnP</label>
|
||||
<select id="upnp" class="form-select" v-model="config.upnp">
|
||||
<option value="disabled">Disabled</option>
|
||||
<option value="enabled">Enabled</option>
|
||||
</select>
|
||||
<div class="form-text">Automatically configure port forwarding</div>
|
||||
</div>
|
||||
<!--Gamepads-->
|
||||
<div class="mb-3" v-if="platform === 'windows'">
|
||||
<label for="gamepad" class="form-label">Gamepads</label>
|
||||
<select id="gamepad" class="form-select" v-model="config.gamepad">
|
||||
<option value="ds4">DS4 (PS4)</option>
|
||||
<option value="x360">X360 (Xbox 360)</option>
|
||||
</select>
|
||||
<div class="form-text">Automatically configure port forwarding</div>
|
||||
</div>
|
||||
<!--Ping Timeout-->
|
||||
<div class="mb-3">
|
||||
<label for="ping_timeout" class="form-label">Ping Timeout</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="ping_timeout"
|
||||
placeholder="10000"
|
||||
v-model="config.ping_timeout"
|
||||
/>
|
||||
<div class="form-text">
|
||||
How long to wait in milliseconds for data from moonlight before
|
||||
shutting down the stream
|
||||
</div>
|
||||
</div>
|
||||
<!--Advertised FPS and Resolutions-->
|
||||
<div class="mb-3">
|
||||
<label for="ping_timeout" class="form-label"
|
||||
>Advertised Resolutions and FPS</label
|
||||
>
|
||||
<div class="resolutions-container">
|
||||
<label>Resolutions</label>
|
||||
<div class="resolutions d-flex flex-wrap">
|
||||
<div
|
||||
class="p-2 ms-item m-2 d-flex justify-content-between"
|
||||
v-for="(r,i) in resolutions"
|
||||
:key="r"
|
||||
>
|
||||
<span class="px-2">{{r}}</span>
|
||||
<span style="cursor: pointer" @click="resolutions.splice(i,1)"
|
||||
>×</span
|
||||
>
|
||||
</div>
|
||||
<form
|
||||
@submit.prevent="resolutions.push(resIn);resIn = '';"
|
||||
class="d-flex align-items-center"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
v-model="resIn"
|
||||
required
|
||||
pattern="[0-9]+x[0-9]+"
|
||||
style="
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
"
|
||||
class="form-control"
|
||||
/>
|
||||
<button
|
||||
style="border-top-left-radius: 0; border-bottom-left-radius: 0"
|
||||
class="btn btn-success"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fps-container">
|
||||
<label>FPS</label>
|
||||
<div class="fps d-flex flex-wrap">
|
||||
<div
|
||||
class="p-2 ms-item m-2 d-flex justify-content-between"
|
||||
v-for="(f,i) in fps"
|
||||
:key="f"
|
||||
>
|
||||
<span class="px-2">{{f}}</span>
|
||||
<span style="cursor: pointer" @click="fps.splice(i,1)"
|
||||
>×</span
|
||||
>
|
||||
</div>
|
||||
<form
|
||||
@submit.prevent="fps.push(fpsIn);fpsIn = '';"
|
||||
class="d-flex align-items-center"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
v-model="fpsIn"
|
||||
required
|
||||
pattern="[0-9]+"
|
||||
style="
|
||||
width: 6ch;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
"
|
||||
class="form-control"
|
||||
/>
|
||||
<button
|
||||
style="border-top-left-radius: 0; border-bottom-left-radius: 0"
|
||||
class="btn btn-success"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-text">
|
||||
The display modes advertised by Sunshine<br />
|
||||
Some versions of Moonlight, such as Moonlight-nx (Switch), rely on
|
||||
this list to ensure that the requested resolutions and fps are
|
||||
supported.
|
||||
</div>
|
||||
</div>
|
||||
<!-- Mapping Key AltRight to Key Windows -->
|
||||
<div class="mb-3">
|
||||
<label for="mapkey" class="form-label"
|
||||
>Map Right Alt key to Windows key</label
|
||||
>
|
||||
<select
|
||||
id="mapkey"
|
||||
class="form-select"
|
||||
v-model="config.key_rightalt_to_key_win"
|
||||
>
|
||||
<option value="disabled">Disabled</option>
|
||||
<option value="enabled">Enabled</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-text">
|
||||
It may be possible that you cannot send the Windows Key from Moonlight
|
||||
directly.<br />
|
||||
In those cases it may be usefull to make Sunshine think the Right Alt
|
||||
key is the Windows key
|
||||
</div>
|
||||
</div>
|
||||
<!--Files Tab-->
|
||||
<div v-if="currentTab === 'files'" class="config-page">
|
||||
<!--Private Key-->
|
||||
<div class="mb-3">
|
||||
<label for="pkey" class="form-label">Private Key</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="pkey"
|
||||
placeholder="/dir/pkey.pem"
|
||||
v-model="config.pkey"
|
||||
/>
|
||||
<div class="form-text">The private key must be 2048 bits</div>
|
||||
</div>
|
||||
<!--Cert-->
|
||||
<div class="mb-3">
|
||||
<label for="cert" class="form-label">Cert</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="cert"
|
||||
placeholder="/dir/cert.pem"
|
||||
v-model="config.cert"
|
||||
/>
|
||||
<div class="form-text">
|
||||
The certificate must be signed with a 2048 bit key
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--State File-->
|
||||
<div class="mb-3">
|
||||
<label for="file_state" class="form-label">State File</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="file_state"
|
||||
placeholder="sunshine_state.json"
|
||||
v-model="config.file_state"
|
||||
/>
|
||||
<div class="form-text">
|
||||
The file where current state of Sunshine is stored
|
||||
</div>
|
||||
</div>
|
||||
<!--Apps File-->
|
||||
<div class="mb-3">
|
||||
<label for="file_apps" class="form-label">Apps File</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="file_apps"
|
||||
placeholder="apps.json"
|
||||
v-model="config.file_apps"
|
||||
/>
|
||||
<div class="form-text">
|
||||
The file where current apps of Sunshine are stored
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="currentTab === 'input'" class="config-page">
|
||||
<!--Back Button Timeout-->
|
||||
<div class="mb-3">
|
||||
<label for="back_button_timeout" class="form-label"
|
||||
>Back Button Timeout</label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="back_button_timeout"
|
||||
placeholder="2000"
|
||||
v-model="config.back_button_timeout"
|
||||
/>
|
||||
<div class="form-text">
|
||||
The back/select button on the controller.<br />
|
||||
On the Shield, the home and powerbutton are not passed to
|
||||
Moonlight.<br />
|
||||
If, after the timeout, the back button is still pressed down,
|
||||
Home/Guide button press is emulated.<br />
|
||||
If back_button_timeout < 0, then the Home/Guide button will not be
|
||||
emulated<br />
|
||||
</div>
|
||||
</div>
|
||||
<!-- Key Repeat Delay-->
|
||||
<div class="mb-3" v-if="platform === 'windows'">
|
||||
<label for="key_repeat_delay" class="form-label"
|
||||
>Key Repeat Delay</label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="key_repeat_delay"
|
||||
placeholder="500"
|
||||
v-model="config.key_repeat_delay"
|
||||
/>
|
||||
<div class="form-text">
|
||||
Control how fast keys will repeat themselves<br />
|
||||
The initial delay in milliseconds before repeating keys
|
||||
</div>
|
||||
</div>
|
||||
<!-- Key Repeat Frequency-->
|
||||
<div class="mb-3" v-if="platform === 'windows'">
|
||||
<label for="key_repeat_frequency" class="form-label"
|
||||
>Key Repeat Frequency</label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="key_repeat_frequency"
|
||||
placeholder="24.9"
|
||||
v-model="config.key_repeat_frequency"
|
||||
/>
|
||||
<div class="form-text">
|
||||
How often keys repeat every second<br />
|
||||
This configurable option supports decimals
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--Files Tab-->
|
||||
<div v-if="currentTab === 'av'" class="config-page">
|
||||
<!--Audio Sink-->
|
||||
<div class="mb-3" v-if="platform === 'windows'">
|
||||
<label for="audio_sink" class="form-label">Audio Sink</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="audio_sink"
|
||||
placeholder="{0.0.0.00000000}.{FD47D9CC-4218-4135-9CE2-0C195C87405B}"
|
||||
v-model="config.audio_sink"
|
||||
/>
|
||||
<div class="form-text">
|
||||
The name of the audio sink used for Audio Loopback<br />
|
||||
You can find the name of the audio sink using the following
|
||||
command:<br />
|
||||
<pre>tools\audio-info.exe</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3" v-if="platform === 'linux'">
|
||||
<label for="audio_sink" class="form-label">Audio Sink</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="audio_sink"
|
||||
placeholder="alsa_output.pci-0000_09_00.3.analog-stereo"
|
||||
v-model="config.audio_sink"
|
||||
/>
|
||||
<div class="form-text">
|
||||
The name of the audio sink used for Audio Loopback<br />
|
||||
If you do not specify this variable, pulseaudio will select the
|
||||
default monitor device.<br />
|
||||
<br />
|
||||
You can find the name of the audio sink using either command:<br />
|
||||
<pre>pacmd list-sinks | grep "name:"</pre>
|
||||
<pre>pactl info | grep Source</pre>
|
||||
<br />
|
||||
</div>
|
||||
</div>
|
||||
<!--Virtual Sink-->
|
||||
<div class="mb-3" v-if="platform === 'windows'">
|
||||
<label for="virtual_sink" class="form-label">Virtual Sink</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="virtual_sink"
|
||||
placeholder="{0.0.0.00000000}.{8edba70c-1125-467c-b89c-15da389bc1d4}"
|
||||
v-model="config.virtual_sink"
|
||||
/>
|
||||
<div class="form-text">
|
||||
The virtual sink, is the audio device that's virtual (Like Steam
|
||||
Streaming Speakers), it allows Sunshine to stream audio, while muting
|
||||
the speakers.
|
||||
</div>
|
||||
</div>
|
||||
<!--Adapter Name -->
|
||||
<div class="mb-3" v-if="platform === 'windows'">
|
||||
<label for="adapter_name" class="form-label">Adapter Name</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="adapter_name"
|
||||
placeholder="Radeon RX 580 Series"
|
||||
v-model="config.adapter_name"
|
||||
/>
|
||||
<div class="form-text" v-if="platform === 'windows'">
|
||||
You can select the video card you want to stream:<br />
|
||||
The appropriate values can be found using the following command:<br />
|
||||
<pre>tools\dxgi-info.exe</pre>
|
||||
</div>
|
||||
</div>
|
||||
<!--Output Name -->
|
||||
<div class="mb-3" class="config-page" v-if="platform === 'windows'">
|
||||
<label for="output_name" class="form-label">Output Name</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="output_name"
|
||||
placeholder="\\.\DISPLAY1"
|
||||
v-model="config.output_name"
|
||||
/>
|
||||
<div class="form-text">
|
||||
You can select the video card you want to stream:<br />
|
||||
The appropriate values can be found using the following command:<br />
|
||||
tools\dxgi-info.exe<br /><br />
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3" class="config-page" v-if="platform === 'linux'">
|
||||
<label for="output_name" class="form-label">Monitor number</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="output_name"
|
||||
placeholder="0"
|
||||
v-model="config.output_name"
|
||||
/>
|
||||
<div class="form-text">
|
||||
xrandr --listmonitors<br />
|
||||
Example output:
|
||||
<pre> 0: +HDMI-1 1920/518x1200/324+0+0 HDMI-1</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="currentTab === 'advanced'" class="config-page">
|
||||
<!--Port familly-->
|
||||
<div class="mb-3">
|
||||
<label for="port" class="form-label">Port</label>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="65529"
|
||||
class="form-control"
|
||||
id="port"
|
||||
placeholder="47989"
|
||||
v-model="config.port"
|
||||
/>
|
||||
<div class="form-text">Set the familly of ports used by Sunshine</div>
|
||||
</div>
|
||||
<!-- Quantization Parameter -->
|
||||
<div class="mb-3">
|
||||
<label for="qp" class="form-label">Quantitization Parameter</label>
|
||||
<input
|
||||
type="number"
|
||||
class="form-control"
|
||||
id="qp"
|
||||
placeholder="28"
|
||||
v-model="config.qp"
|
||||
/>
|
||||
<div class="form-text">
|
||||
Quantitization Parameter<br />
|
||||
Some devices may not support Constant Bit Rate.<br />
|
||||
For those devices, QP is used instead.<br />
|
||||
Higher value means more compression, but less quality<br />
|
||||
</div>
|
||||
</div>
|
||||
<!-- Min Threads -->
|
||||
<div class="mb-3">
|
||||
<label for="min_threads" class="form-label"
|
||||
>Minimum number of threads used by ffmpeg to encode the video.</label
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
class="form-control"
|
||||
id="min_threads"
|
||||
placeholder="1"
|
||||
v-model="config.min_threads"
|
||||
/>
|
||||
<div class="form-text">
|
||||
Minimum number of threads used by ffmpeg to encode the video.<br />
|
||||
Increasing the value slightly reduces encoding efficiency, but the
|
||||
tradeoff is usually<br />
|
||||
worth it to gain the use of more CPU cores for encoding. The ideal
|
||||
value is the lowest<br />
|
||||
value that can reliably encode at your desired streaming settings on
|
||||
your hardware.
|
||||
</div>
|
||||
</div>
|
||||
<!--HEVC Suppport -->
|
||||
<div class="mb-3">
|
||||
<label for="hevc_mode" class="form-label">HEVC Support</label>
|
||||
<select id="hevc_mode" class="form-select" v-model="config.hevc_mode">
|
||||
<option value="0">
|
||||
Sunshine will specify support for HEVC based on encoder
|
||||
</option>
|
||||
<option value="1">
|
||||
Sunshine will not advertise support for HEVC
|
||||
</option>
|
||||
<option value="2">
|
||||
Sunshine will advertise support for HEVC Main profile
|
||||
</option>
|
||||
<option value="3">
|
||||
Sunshine will advertise support for HEVC Main and Main10 (HDR)
|
||||
profiles
|
||||
</option>
|
||||
</select>
|
||||
<div class="form-text">
|
||||
Allows the client to request HEVC Main or HEVC Main10 video
|
||||
streams.<br />
|
||||
HEVC is more CPU-intensive to encode, so enabling this may reduce
|
||||
performance when using software encoding.
|
||||
</div>
|
||||
</div>
|
||||
<!--Encoder -->
|
||||
<div class="mb-3">
|
||||
<label for="encoder" class="form-label">Force a Specific Encoder</label>
|
||||
<select id="encoder" class="form-select" v-model="config.encoder">
|
||||
<option :value="''">Autodetect</option>
|
||||
<option value="nvenc">nVidia NVENC</option>
|
||||
<option value="amdvce">AMD AMF/VCE</option>
|
||||
<option value="vaapi">VA-API</option>
|
||||
<option value="software">Software</option>
|
||||
</select>
|
||||
<div class="form-text">
|
||||
Force a specific encoder, otherwise Sunshine will use the first
|
||||
encoder that is available
|
||||
</div>
|
||||
</div>
|
||||
<!--FEC Percentage-->
|
||||
<div class="mb-3">
|
||||
<label for="fec_percentage" class="form-label">FEC Percentage</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="fec_percentage"
|
||||
placeholder="20"
|
||||
v-model="config.fec_percentage"
|
||||
/>
|
||||
<div class="form-text">
|
||||
Percentage of error correcting packets per data packet in each video
|
||||
frame.<br />
|
||||
Higher values can correct for more network packet loss, but at the
|
||||
cost of increasing bandwidth usage.<br />
|
||||
The default value of 20 is what GeForce Experience uses.
|
||||
</div>
|
||||
</div>
|
||||
<!--Channels-->
|
||||
<div class="mb-3">
|
||||
<label for="channels" class="form-label">Channels</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="channels"
|
||||
placeholder="1"
|
||||
v-model="config.channels"
|
||||
/>
|
||||
<div class="form-text">
|
||||
When multicasting, it could be useful to have different configurations
|
||||
for each connected Client. For example:
|
||||
<ul>
|
||||
<li>
|
||||
Clients connected through WAN and LAN have different bitrate
|
||||
contstraints.
|
||||
</li>
|
||||
<li>Decoders may require different settings for color</li>
|
||||
</ul>
|
||||
Unlike simply broadcasting to multiple Client, this will generate
|
||||
distinct video streams.<br />
|
||||
Note, CPU usage increases for each distinct video stream generated
|
||||
</div>
|
||||
</div>
|
||||
<!--Credentials File-->
|
||||
<div class="mb-3">
|
||||
<label for="credentials_file" class="form-label"
|
||||
>Web Manager Credentials File</label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="credentials_file"
|
||||
placeholder="sunshine_state.json"
|
||||
v-model="config.credentials_file"
|
||||
/>
|
||||
<div class="form-text">
|
||||
Store Username/Password seperately from Sunshine's state file.
|
||||
</div>
|
||||
</div>
|
||||
<!--Origin PIN Allowed-->
|
||||
<div class="mb-3">
|
||||
<label for="origin_pin_allowed" class="form-label"
|
||||
>Origin PIN Allowed</label
|
||||
>
|
||||
<select
|
||||
id="origin_pin_allowed"
|
||||
class="form-select"
|
||||
v-model="config.origin_pin_allowed"
|
||||
>
|
||||
<option value="pc">Only localhost may access /pin</option>
|
||||
<option value="lan">Only those in LAN may access /pin</option>
|
||||
<option value="wan">Anyone may access /pin</option>
|
||||
</select>
|
||||
<div class="form-text">
|
||||
The origin of the remote endpoint address that is not denied for HTTP
|
||||
method /pin
|
||||
</div>
|
||||
</div>
|
||||
<!--External IP-->
|
||||
<div class="mb-3">
|
||||
<label for="external_ip" class="form-label">External IP</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="external_ip"
|
||||
placeholder="123.456.789.12"
|
||||
v-model="config.external_ip"
|
||||
/>
|
||||
<div class="form-text">
|
||||
If no external IP address is given, Sunshine will automatically detect
|
||||
external IP
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--Software Settings-->
|
||||
<div v-if="currentTab === 'sw'" class="config-page">
|
||||
<div class="mb-3">
|
||||
<label for="sw_preset" class="form-label">SW Presets</label>
|
||||
<input
|
||||
class="form-control"
|
||||
id="sw_preset"
|
||||
placeholder="superfast"
|
||||
v-model="config.sw_preset"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="sw_tune" class="form-label">SW Tune</label>
|
||||
<input
|
||||
class="form-control"
|
||||
id="sw_tune"
|
||||
placeholder="zerolatency"
|
||||
v-model="config.sw_tune"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!--Nvidia Encoder Settings-->
|
||||
<div v-if="currentTab === 'nv'" class="config-page">
|
||||
<!--NVENC SETTINGS-->
|
||||
<div class="mb-3">
|
||||
<label for="nv_preset" class="form-label">NVEnc Preset</label>
|
||||
<select id="nv_preset" class="form-select" v-model="config.nv_preset">
|
||||
<option value="default">Default</option>
|
||||
<option value="hp">High Performance</option>
|
||||
<option value="hq">High Quality</option>
|
||||
<option value="slow">Slow - hq 2 passes</option>
|
||||
<option value="medium">medium -- hq 1 pass</option>
|
||||
<option value="fast">fast -- hp 1 pass</option>
|
||||
<option value="bd">bd</option>
|
||||
<option value="ll">ll -- low latency</option>
|
||||
<option value="llhq">llhq</option>
|
||||
<option value="llhp">llhp</option>
|
||||
<option value="lossless">lossless</option>
|
||||
<option value="losslesshp">losslesshp</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="nv_rc" class="form-label">NVEnc Rate Control</label>
|
||||
<select id="nv_rc" class="form-select" v-model="config.nv_rc">
|
||||
<option value="auto">auto -- let ffmpeg decide rate control</option>
|
||||
<option value="constqp">constqp -- constant QP mode</option>
|
||||
<option value="vbr">vbr -- variable bitrate</option>
|
||||
<option value="cbr">cbr -- constant bitrate</option>
|
||||
<option value="cbr_hq">cbr_hq -- cbr high quality</option>
|
||||
<option value="cbr_ld_hq">
|
||||
cbr_ld_hq -- cbr low delay high quality
|
||||
</option>
|
||||
<option value="vbr_hq">vbr_hq -- vbr high quality</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="nv_coder" class="form-label">NVEnc Coder</label>
|
||||
<select id="nv_coder" class="form-select" v-model="config.nv_coder">
|
||||
<option value="auto">auto</option>
|
||||
<option value="cabac">cabac</option>
|
||||
<option value="cavlc">cavlc</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<!--AMD Encoder Settings-->
|
||||
<div v-if="currentTab === 'amd'" class="config-page">
|
||||
<!--Presets-->
|
||||
<div class="mb-3">
|
||||
<label for="amd_quality" class="form-label">AMD AMF Quality</label>
|
||||
<select
|
||||
id="amd_quality"
|
||||
class="form-select"
|
||||
v-model="config.amd_quality"
|
||||
>
|
||||
<option value="default">Default</option>
|
||||
<option value="speed">Speed</option>
|
||||
<option value="balanced">Balanced</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="amd_rc" class="form-label">AMD AMF Rate Control</label>
|
||||
<select id="amd_rc" class="form-select" v-model="config.amd_rc">
|
||||
<option value="auto">auto -- let ffmpeg decide rate control</option>
|
||||
<option value="constqp">constqp -- constant QP mode</option>
|
||||
<option value="vbr_latency">
|
||||
vbr_latency -- Latency Constrained Variable Bitrate
|
||||
</option>
|
||||
<option value="vbr_peak">
|
||||
vbr_peak -- Peak Contrained Variable Bitrate
|
||||
</option>
|
||||
<option value="cbr">cbr -- constant bitrate</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="amd_coder" class="form-label">AMD AMF Rate Control</label>
|
||||
<select id="amd_coder" class="form-select" v-model="config.amd_coder">
|
||||
<option value="auto">auto</option>
|
||||
<option value="cabac">cabac</option>
|
||||
<option value="cavlc">cavlc</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="currentTab === 'va-api'" class="config-page">
|
||||
<input
|
||||
class="form-control"
|
||||
id="adapter_name"
|
||||
placeholder="/dev/dri/renderD128"
|
||||
v-model="config.adapter_name"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert alert-success my-4" v-if="success">
|
||||
<b>Success!</b> Restart Sunshine to apply changes
|
||||
</div>
|
||||
<div class="mb-3 buttons">
|
||||
<button class="btn btn-primary" @click="save">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
new Vue({
|
||||
el: "#app",
|
||||
data() {
|
||||
return {
|
||||
platform: "",
|
||||
success: false,
|
||||
config: null,
|
||||
fps: [],
|
||||
resolutions: [],
|
||||
currentTab: "general",
|
||||
resIn: "",
|
||||
fpsIn: "",
|
||||
tabs: [
|
||||
{
|
||||
id: "general",
|
||||
name: "General",
|
||||
},
|
||||
{
|
||||
id: "files",
|
||||
name: "Files",
|
||||
},
|
||||
{
|
||||
id: "input",
|
||||
name: "Input",
|
||||
},
|
||||
{
|
||||
id: "av",
|
||||
name: "Audio/Video",
|
||||
},
|
||||
{
|
||||
id: "advanced",
|
||||
name: "Advanced",
|
||||
},
|
||||
{
|
||||
id: "sw",
|
||||
name: "Software Encoder",
|
||||
},
|
||||
{
|
||||
id: "nv",
|
||||
name: "NVENC Encoder",
|
||||
},
|
||||
{
|
||||
id: "amd",
|
||||
name: "AMF Encoder",
|
||||
},
|
||||
{
|
||||
id: "va-api",
|
||||
name: "VA-API encoder",
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
created() {
|
||||
fetch("/api/config")
|
||||
.then((r) => r.json())
|
||||
.then((r) => {
|
||||
this.config = r;
|
||||
this.platform = this.config.platform;
|
||||
|
||||
var app = document.getElementById("app");
|
||||
if (this.platform == "windows") {
|
||||
this.tabs = this.tabs.filter((el) => {
|
||||
return el.id !== "va-api";
|
||||
});
|
||||
}
|
||||
if (this.platform == "linux") {
|
||||
this.tabs = this.tabs.filter((el) => {
|
||||
return el.id !== "amd";
|
||||
});
|
||||
}
|
||||
|
||||
delete this.config.status;
|
||||
delete this.config.platform;
|
||||
//Populate default values if not present in config
|
||||
this.config.key_rightalt_to_key_win =
|
||||
this.config.key_rightalt_to_key_win || "disabled";
|
||||
this.config.gamepad = this.config.gamepad || "x360";
|
||||
this.config.upnp = this.config.upnp || "disabled";
|
||||
this.config.min_log_level = this.config.min_log_level || 2;
|
||||
this.config.origin_pin_allowed =
|
||||
this.config.origin_pin_allowed || "pc";
|
||||
this.config.origin_web_ui_allowed =
|
||||
this.config.origin_web_manager_allowed || "lan";
|
||||
this.config.hevc_mode = this.config.hevc_mode || 0;
|
||||
this.config.encoder = this.config.encoder || "";
|
||||
this.config.nv_preset = this.config.nv_preset || "default";
|
||||
this.config.nv_rc = this.config.nv_rc || "auto";
|
||||
this.config.nv_coder = this.config.nv_coder || "auto";
|
||||
this.config.amd_quality = this.config.amd_quality || "default";
|
||||
this.config.amd_rc = this.config.amd_rc || "auto";
|
||||
this.config.fps = this.config.fps || "[10, 30, 60, 90, 120]";
|
||||
this.config.resolutions =
|
||||
this.config.resolutions ||
|
||||
"[352x240,480x360,858x480,1280x720,1920x1080,2560x1080,3440x1440,1920x1200,3860x2160,3840x1600]";
|
||||
this.fps = JSON.parse(this.config.fps);
|
||||
//Resolutions should be fixed because are not valid JSON
|
||||
let res = this.config.resolutions.substring(
|
||||
1,
|
||||
this.config.resolutions.length - 1
|
||||
);
|
||||
let resolutions = [];
|
||||
res.split(",").forEach((r) => resolutions.push(r.trim()));
|
||||
this.resolutions = resolutions;
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
save() {
|
||||
this.success = false;
|
||||
let nl = this.config === "windows" ? "\r\n" : "\n";
|
||||
this.config.resolutions =
|
||||
"[" +
|
||||
nl +
|
||||
" " +
|
||||
this.resolutions.join("," + nl + " ") +
|
||||
nl +
|
||||
"]";
|
||||
this.config.fps = JSON.stringify(this.fps);
|
||||
|
||||
fetch("/api/config", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(this.config),
|
||||
}).then((r) => {
|
||||
if (r.status == 200) this.success = true;
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.config-page {
|
||||
padding: 1em;
|
||||
border: 1px solid #dee2e6;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
padding: 1em 0;
|
||||
}
|
||||
|
||||
.ms-item {
|
||||
background-color: #ccc;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
14
assets/web/header-no-nav.html
Normal file
14
assets/web/header-no-nav.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Sunshine</title>
|
||||
<link href="/third_party/bootstrap.min.css" rel="stylesheet" />
|
||||
<script src="/third_party/bootstrap.bundle.min.js"></script>
|
||||
<script src="/third_party/vue.js"></script>
|
||||
</head>
|
||||
|
||||
<body></body>
|
||||
</html>
|
||||
56
assets/web/header.html
Normal file
56
assets/web/header.html
Normal file
@@ -0,0 +1,56 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Sunshine</title>
|
||||
<link href="/third_party/bootstrap.min.css" rel="stylesheet" />
|
||||
<script src="/third_party/bootstrap.bundle.min.js"></script>
|
||||
<script src="/third_party/vue.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<nav
|
||||
class="navbar navbar-expand-lg navbar-light"
|
||||
style="background-color: #ffc400"
|
||||
>
|
||||
<div class="container-fluid">
|
||||
<span class="navbar-brand">Sunshine</span>
|
||||
<button
|
||||
class="navbar-toggler"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#navbarSupportedContent"
|
||||
aria-controls="navbarSupportedContent"
|
||||
aria-expanded="false"
|
||||
aria-label="Toggle navigation"
|
||||
>
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/">Home</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/pin">PIN</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/apps">Applications</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/config">Configuration</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/password">Change Password</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/troubleshooting">Troubleshooting</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</body>
|
||||
</html>
|
||||
11
assets/web/index.html
Normal file
11
assets/web/index.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<div id="content" class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-6 py-4" style="margin: 0 auto">
|
||||
<h1>Hello, Sunshine!</h1>
|
||||
<p>Sunshine is a Gamestream host for Moonlight</p>
|
||||
<a href="https://github.com/loki-47-6F-64/sunshine"
|
||||
>Official GitHub Repository</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
133
assets/web/password.html
Normal file
133
assets/web/password.html
Normal file
@@ -0,0 +1,133 @@
|
||||
<div id="app" class="container">
|
||||
<h1 class="my-4">Password Change</h1>
|
||||
<form @submit.prevent="save">
|
||||
<div class="card d-flex p-4 flex-row">
|
||||
<div class="col-md-6 px-4">
|
||||
<h4>Current Credentials</h4>
|
||||
<div class="mb-3">
|
||||
<label for="currentUsername" class="form-label">Username</label>
|
||||
<input
|
||||
required
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="currentUsername"
|
||||
v-model="passwordData.currentUsername"
|
||||
/>
|
||||
<div class="form-text"> </div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="currentPassword" class="form-label">Password</label>
|
||||
<input
|
||||
autocomplete="current-password"
|
||||
type="password"
|
||||
class="form-control"
|
||||
id="currentPassword"
|
||||
v-model="passwordData.currentPassword"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 px-4">
|
||||
<h4>New Credentials</h4>
|
||||
<div class="mb-3">
|
||||
<label for="newUsername" class="form-label">New Username</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="newUsername"
|
||||
v-model="passwordData.newUsername"
|
||||
/>
|
||||
<div class="form-text">
|
||||
If not specified, the username will not change
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="newPassword" class="form-label">Password</label>
|
||||
<input
|
||||
autocomplete="new-password"
|
||||
required
|
||||
type="password"
|
||||
class="form-control"
|
||||
id="newPassword"
|
||||
v-model="passwordData.newPassword"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="confirmNewPassword" class="form-label"
|
||||
>Confirm Password</label
|
||||
>
|
||||
<input
|
||||
autocomplete="new-password"
|
||||
required
|
||||
type="password"
|
||||
class="form-control"
|
||||
id="confirmNewPassword"
|
||||
v-model="passwordData.confirmNewPassword"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert alert-danger" v-if="error"><b>Error: </b>{{error}}</div>
|
||||
<div class="alert alert-success" v-if="success">
|
||||
<b>Success! </b>This page will reload soon, your browser will ask you for
|
||||
the new credentials
|
||||
</div>
|
||||
<div class="mb-3 buttons">
|
||||
<button class="btn btn-primary">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
new Vue({
|
||||
el: "#app",
|
||||
data() {
|
||||
return {
|
||||
error: null,
|
||||
success: false,
|
||||
passwordData: {
|
||||
currentUsername: "",
|
||||
currentPassword: "",
|
||||
newUsername: "",
|
||||
newPassword: "",
|
||||
confirmNewPassword: "",
|
||||
},
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
save() {
|
||||
this.error = null;
|
||||
fetch("/api/password", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(this.passwordData),
|
||||
}).then((r) => {
|
||||
if (r.status == 200) {
|
||||
r.json().then((rj) => {
|
||||
if (rj.status.toString() === "true") {
|
||||
this.success = true;
|
||||
setTimeout(() => {
|
||||
document.location.reload();
|
||||
}, 5000);
|
||||
} else {
|
||||
this.error = rj.error;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.error = "Internal Server Error";
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.config-page {
|
||||
padding: 1em;
|
||||
border: 1px solid #dee2e6;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
padding: 1em 0;
|
||||
}
|
||||
</style>
|
||||
42
assets/web/pin.html
Normal file
42
assets/web/pin.html
Normal file
@@ -0,0 +1,42 @@
|
||||
<div id="content" class="container">
|
||||
<h1 class="my-4">PIN Pairing</h1>
|
||||
<form action="" class="form d-flex flex-column align-items-center" id="form">
|
||||
<div class="card flex-column d-flex p-4 mb-4">
|
||||
<input
|
||||
type="number"
|
||||
placeholder="PIN"
|
||||
id="pin-input"
|
||||
class="form-control my-4"
|
||||
/>
|
||||
<button class="btn btn-primary">Send</button>
|
||||
</div>
|
||||
<div class="alert alert-warning">
|
||||
<b>Warning!</b> Make sure you have access to the client you are pairing
|
||||
with.<br />
|
||||
This software can give total control to your computer, so be careful!
|
||||
</div>
|
||||
<div id="status"></div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.querySelector("#form").addEventListener("submit", (e) => {
|
||||
e.preventDefault();
|
||||
let pin = document.querySelector("#pin-input").value;
|
||||
document.querySelector("#status").innerHTML = "";
|
||||
let b = JSON.stringify({ pin: pin });
|
||||
fetch("/api/pin", { method: "POST", body: b })
|
||||
.then((response) => response.json())
|
||||
.then((response) => {
|
||||
if (response.status) {
|
||||
document.querySelector(
|
||||
"#status"
|
||||
).innerHTML = `<div class="alert alert-success" role="alert">Success! Please check Moonlight to continue</div>`;
|
||||
} else {
|
||||
document.querySelector(
|
||||
"#status"
|
||||
).innerHTML = `<div class="alert alert-danger" role="alert">PIN does not match, please check if it's typed correctly</div>`;
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
7
assets/web/third_party/bootstrap.bundle.min.js
vendored
Normal file
7
assets/web/third_party/bootstrap.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
7
assets/web/third_party/bootstrap.min.css
vendored
Normal file
7
assets/web/third_party/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
11965
assets/web/third_party/vue.js
vendored
Normal file
11965
assets/web/third_party/vue.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
92
assets/web/troubleshooting.html
Normal file
92
assets/web/troubleshooting.html
Normal file
@@ -0,0 +1,92 @@
|
||||
<div id="app" class="container">
|
||||
<h1 class="my-4">Troubleshooting</h1>
|
||||
<!--Force Close App-->
|
||||
<div class="card p-2 my-4">
|
||||
<div class="card-body">
|
||||
<h2>Force Close</h2>
|
||||
<br />
|
||||
<p>
|
||||
If Moonlight complains about an app currently running, force closing the
|
||||
app should fix the issue
|
||||
</p>
|
||||
<div class="alert alert-success" v-if="closeAppStatus === true">
|
||||
Application Closed Successfuly!
|
||||
</div>
|
||||
<div class="alert alert-danger" v-if="closeAppStatus === false">
|
||||
Error while closing Appplication
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
class="btn btn-warning"
|
||||
:disabled="closeAppPressed"
|
||||
@click="closeApp"
|
||||
>
|
||||
Force Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--Unpair all Clients-->
|
||||
<div class="card p-2 my-4">
|
||||
<div class="card-body">
|
||||
<h2>Unpair All Clients</h2>
|
||||
<br />
|
||||
<p>Remove all your paired devices</p>
|
||||
<div class="alert alert-success" v-if="unpairAllStatus === true">
|
||||
Unpair Successful!
|
||||
</div>
|
||||
<div class="alert alert-danger" v-if="unpairAllStatus === false">
|
||||
Error while unpairing
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
class="btn btn-danger"
|
||||
:disabled="unpairAllPressed"
|
||||
@click="unpairAll"
|
||||
>
|
||||
Unpair All
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
new Vue({
|
||||
el: "#app",
|
||||
data() {
|
||||
return {
|
||||
closeAppPressed: false,
|
||||
closeAppStatus: null,
|
||||
unpairAllPressed: false,
|
||||
unpairAllStatus: null,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
closeApp() {
|
||||
this.closeAppPressed = true;
|
||||
fetch("/api/apps/close", { method: "POST" })
|
||||
.then((r) => r.json())
|
||||
.then((r) => {
|
||||
this.closeAppPressed = false;
|
||||
this.closeAppStatus = r.status.toString() === "true";
|
||||
setTimeout(() => {
|
||||
this.closeAppStatus = null;
|
||||
}, 5000);
|
||||
});
|
||||
},
|
||||
unpairAll() {
|
||||
this.unpairAllPressed = true;
|
||||
fetch("/api/clients/unpair", { method: "POST" })
|
||||
.then((r) => r.json())
|
||||
.then((r) => {
|
||||
this.unpairAllPressed = false;
|
||||
this.unpairAllStatus = r.status.toString() === "true";
|
||||
setTimeout(() => {
|
||||
this.unpairAllStatus = null;
|
||||
}, 5000);
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
105
assets/web/welcome.html
Normal file
105
assets/web/welcome.html
Normal file
@@ -0,0 +1,105 @@
|
||||
<main role="main" id="app" style="max-width: 600px; margin: 0 auto">
|
||||
<header>
|
||||
<h1 class="mb-0">Welcome to Sunshine!</h1>
|
||||
<p class="mb-0 align-self-start">
|
||||
Before Getting Started, write down below these credentials
|
||||
</p>
|
||||
</header>
|
||||
<div class="alert alert-warning">
|
||||
These Credentials down below are needed to access the rest of the
|
||||
application.<br />
|
||||
Keep them safe, since <b>you will never see them again!</b>
|
||||
</div>
|
||||
<form @submit.prevent="save">
|
||||
<div class="mb-2">
|
||||
<label for="usernameInput" class="form-label">Username:</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="usernameInput"
|
||||
autocomplete="username"
|
||||
v-model="passwordData.newUsername"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label for="passwordInput" class="form-label">Password:</label>
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
id="passwordInput"
|
||||
autocomplete="new-password"
|
||||
v-model="passwordData.newPassword"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label for="confirmPasswordInput" class="form-label"
|
||||
>Password (confirm):</label
|
||||
>
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
id="confirmPasswordInput"
|
||||
autocomplete="new-password"
|
||||
v-model="passwordData.confirmNewPassword"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary w-100 mb-2"
|
||||
v-bind:disabled="loading"
|
||||
>
|
||||
Login
|
||||
</button>
|
||||
<div class="alert alert-danger" v-if="error"><b>Error: </b>{{error}}</div>
|
||||
<div class="alert alert-success" v-if="success">
|
||||
<b>Success! </b>This page will reload soon, your browser will ask you for
|
||||
the new credentials
|
||||
</div>
|
||||
</form>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
new Vue({
|
||||
el: "#app",
|
||||
data() {
|
||||
return {
|
||||
error: null,
|
||||
success: false,
|
||||
loading: false,
|
||||
passwordData: {
|
||||
newUsername: "sunshine",
|
||||
newPassword: "",
|
||||
confirmNewPassword: "",
|
||||
},
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
save() {
|
||||
this.error = null;
|
||||
this.loading = true;
|
||||
fetch("/api/password", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(this.passwordData),
|
||||
}).then((r) => {
|
||||
this.loading = false;
|
||||
if (r.status == 200) {
|
||||
r.json().then((rj) => {
|
||||
if (rj.status.toString() === "true") {
|
||||
this.success = true;
|
||||
setTimeout(() => {
|
||||
document.location.reload();
|
||||
}, 5000);
|
||||
} else {
|
||||
this.error = rj.error;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.error = "Internal Server Error";
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
21
cmake/FindLIBCAP.cmake
Normal file
21
cmake/FindLIBCAP.cmake
Normal file
@@ -0,0 +1,21 @@
|
||||
# - Try to find Libcap
|
||||
# Once done this will define
|
||||
#
|
||||
# LIBCAP_FOUND - system has Libcap
|
||||
# LIBCAP_INCLUDE_DIRS - the Libcap include directory
|
||||
# LIBCAP_LIBRARIES - the libraries needed to use Libcap
|
||||
# LIBCAP_DEFINITIONS - Compiler switches required for using Libcap
|
||||
|
||||
# Use pkg-config to get the directories and then use these values
|
||||
# in the find_path() and find_library() calls
|
||||
find_package(PkgConfig)
|
||||
pkg_check_modules(PC_LIBCAP libcap)
|
||||
|
||||
set(LIBCAP_DEFINITIONS ${PC_LIBCAP_CFLAGS})
|
||||
|
||||
find_path(LIBCAP_INCLUDE_DIRS sys/capability.h PATHS ${PC_LIBCAP_INCLUDEDIR} ${PC_LIBCAP_INCLUDE_DIRS})
|
||||
find_library(LIBCAP_LIBRARIES NAMES libcap.so PATHS ${PC_LIBCAP_LIBDIR} ${PC_LIBCAP_LIBRARY_DIRS})
|
||||
mark_as_advanced(LIBCAP_INCLUDE_DIRS LIBCAP_LIBRARIES)
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
find_package_handle_standard_args(LIBCAP REQUIRED_VARS LIBCAP_LIBRARIES LIBCAP_INCLUDE_DIRS)
|
||||
21
cmake/FindLIBDRM.cmake
Normal file
21
cmake/FindLIBDRM.cmake
Normal file
@@ -0,0 +1,21 @@
|
||||
# - Try to find Libdrm
|
||||
# Once done this will define
|
||||
#
|
||||
# LIBDRM_FOUND - system has Libdrm
|
||||
# LIBDRM_INCLUDE_DIRS - the Libdrm include directory
|
||||
# LIBDRM_LIBRARIES - the libraries needed to use Libdrm
|
||||
# LIBDRM_DEFINITIONS - Compiler switches required for using Libdrm
|
||||
|
||||
# Use pkg-config to get the directories and then use these values
|
||||
# in the find_path() and find_library() calls
|
||||
find_package(PkgConfig)
|
||||
pkg_check_modules(PC_LIBDRM libdrm)
|
||||
|
||||
set(LIBDRM_DEFINITIONS ${PC_LIBDRM_CFLAGS})
|
||||
|
||||
find_path(LIBDRM_INCLUDE_DIRS drm.h PATHS ${PC_LIBDRM_INCLUDEDIR} ${PC_LIBDRM_INCLUDE_DIRS} PATH_SUFFIXES libdrm)
|
||||
find_library(LIBDRM_LIBRARIES NAMES libdrm.so PATHS ${PC_LIBDRM_LIBDIR} ${PC_LIBDRM_LIBRARY_DIRS})
|
||||
mark_as_advanced(LIBDRM_INCLUDE_DIRS LIBDRM_LIBRARIES)
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
find_package_handle_standard_args(LIBDRM REQUIRED_VARS LIBDRM_LIBRARIES LIBDRM_INCLUDE_DIRS)
|
||||
78
cmake/FindWayland.cmake
Normal file
78
cmake/FindWayland.cmake
Normal file
@@ -0,0 +1,78 @@
|
||||
# Try to find Wayland on a Unix system
|
||||
#
|
||||
# This will define:
|
||||
#
|
||||
# WAYLAND_FOUND - True if Wayland is found
|
||||
# WAYLAND_LIBRARIES - Link these to use Wayland
|
||||
# WAYLAND_INCLUDE_DIRS - Include directory for Wayland
|
||||
# WAYLAND_DEFINITIONS - Compiler flags for using Wayland
|
||||
#
|
||||
# In addition the following more fine grained variables will be defined:
|
||||
#
|
||||
# Wayland_Client_FOUND WAYLAND_CLIENT_INCLUDE_DIRS WAYLAND_CLIENT_LIBRARIES
|
||||
# Wayland_Server_FOUND WAYLAND_SERVER_INCLUDE_DIRS WAYLAND_SERVER_LIBRARIES
|
||||
# Wayland_EGL_FOUND WAYLAND_EGL_INCLUDE_DIRS WAYLAND_EGL_LIBRARIES
|
||||
# Wayland_Cursor_FOUND WAYLAND_CURSOR_INCLUDE_DIRS WAYLAND_CURSOR_LIBRARIES
|
||||
#
|
||||
# Copyright (c) 2013 Martin Gräßlin <mgraesslin@kde.org>
|
||||
# 2020 Georges Basile Stavracas Neto <georges.stavracas@gmail.com>
|
||||
#
|
||||
# Redistribution and use is allowed according to the terms of the BSD license.
|
||||
# For details see the accompanying COPYING-CMAKE-SCRIPTS file.
|
||||
|
||||
IF (NOT WIN32)
|
||||
|
||||
# Use pkg-config to get the directories and then use these values
|
||||
# in the find_path() and find_library() calls
|
||||
find_package(PkgConfig)
|
||||
PKG_CHECK_MODULES(PKG_WAYLAND QUIET wayland-client wayland-server wayland-egl wayland-cursor)
|
||||
|
||||
set(WAYLAND_DEFINITIONS ${PKG_WAYLAND_CFLAGS})
|
||||
|
||||
find_path(WAYLAND_CLIENT_INCLUDE_DIRS NAMES wayland-client.h HINTS ${PKG_WAYLAND_INCLUDE_DIRS})
|
||||
find_library(WAYLAND_CLIENT_LIBRARIES NAMES wayland-client HINTS ${PKG_WAYLAND_LIBRARY_DIRS})
|
||||
if(WAYLAND_CLIENT_INCLUDE_DIRS AND WAYLAND_CLIENT_LIBRARIES)
|
||||
set(Wayland_Client_FOUND TRUE)
|
||||
else()
|
||||
set(Wayland_Client_FOUND FALSE)
|
||||
endif()
|
||||
mark_as_advanced(WAYLAND_CLIENT_INCLUDE_DIRS WAYLAND_CLIENT_LIBRARIES)
|
||||
|
||||
find_path(WAYLAND_CURSOR_INCLUDE_DIRS NAMES wayland-cursor.h HINTS ${PKG_WAYLAND_INCLUDE_DIRS})
|
||||
find_library(WAYLAND_CURSOR_LIBRARIES NAMES wayland-cursor HINTS ${PKG_WAYLAND_LIBRARY_DIRS})
|
||||
if(WAYLAND_CURSOR_INCLUDE_DIRS AND WAYLAND_CURSOR_LIBRARIES)
|
||||
set(Wayland_Cursor_FOUND TRUE)
|
||||
else()
|
||||
set(Wayland_Cursor_FOUND FALSE)
|
||||
endif()
|
||||
mark_as_advanced(WAYLAND_CURSOR_INCLUDE_DIRS WAYLAND_CURSOR_LIBRARIES)
|
||||
|
||||
find_path(WAYLAND_EGL_INCLUDE_DIRS NAMES wayland-egl.h HINTS ${PKG_WAYLAND_INCLUDE_DIRS})
|
||||
find_library(WAYLAND_EGL_LIBRARIES NAMES wayland-egl HINTS ${PKG_WAYLAND_LIBRARY_DIRS})
|
||||
if(WAYLAND_EGL_INCLUDE_DIRS AND WAYLAND_EGL_LIBRARIES)
|
||||
set(Wayland_EGL_FOUND TRUE)
|
||||
else()
|
||||
set(Wayland_EGL_FOUND FALSE)
|
||||
endif()
|
||||
mark_as_advanced(WAYLAND_EGL_INCLUDE_DIRS WAYLAND_EGL_LIBRARIES)
|
||||
|
||||
find_path(WAYLAND_SERVER_INCLUDE_DIRS NAMES wayland-server.h HINTS ${PKG_WAYLAND_INCLUDE_DIRS})
|
||||
find_library(WAYLAND_SERVER_LIBRARIES NAMES wayland-server HINTS ${PKG_WAYLAND_LIBRARY_DIRS})
|
||||
if(WAYLAND_SERVER_INCLUDE_DIRS AND WAYLAND_SERVER_LIBRARIES)
|
||||
set(Wayland_Server_FOUND TRUE)
|
||||
else()
|
||||
set(Wayland_Server_FOUND FALSE)
|
||||
endif()
|
||||
mark_as_advanced(WAYLAND_SERVER_INCLUDE_DIRS WAYLAND_SERVER_LIBRARIES)
|
||||
|
||||
set(WAYLAND_INCLUDE_DIRS ${WAYLAND_CLIENT_INCLUDE_DIRS} ${WAYLAND_SERVER_INCLUDE_DIRS} ${WAYLAND_EGL_INCLUDE_DIRS} ${WAYLAND_CURSOR_INCLUDE_DIRS})
|
||||
set(WAYLAND_LIBRARIES ${WAYLAND_CLIENT_LIBRARIES} ${WAYLAND_SERVER_LIBRARIES} ${WAYLAND_EGL_LIBRARIES} ${WAYLAND_CURSOR_LIBRARIES})
|
||||
mark_as_advanced(WAYLAND_INCLUDE_DIRS WAYLAND_LIBRARIES)
|
||||
|
||||
list(REMOVE_DUPLICATES WAYLAND_INCLUDE_DIRS)
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
|
||||
find_package_handle_standard_args(Wayland REQUIRED_VARS WAYLAND_LIBRARIES WAYLAND_INCLUDE_DIRS HANDLE_COMPONENTS)
|
||||
|
||||
ENDIF ()
|
||||
BIN
gamepad.png
Normal file
BIN
gamepad.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
61
gen-deb.in
61
gen-deb.in
@@ -10,15 +10,17 @@ if [ -d package-deb ]; then
|
||||
rm -rf package-deb
|
||||
fi
|
||||
|
||||
export DEBIAN=package-deb/sunshine/DEBIAN
|
||||
export RULES=package-deb/sunshine/etc/udev/rules.d
|
||||
export BIN=package-deb/sunshine/usr/bin
|
||||
export ASSETS=package-deb/sunshine/etc/sunshine
|
||||
export DEBIAN=@CMAKE_CURRENT_BINARY_DIR@/package-deb/sunshine/DEBIAN
|
||||
export RULES=@CMAKE_CURRENT_BINARY_DIR@/package-deb/sunshine/etc/udev/rules.d
|
||||
export BIN=@CMAKE_CURRENT_BINARY_DIR@/package-deb/sunshine/usr/bin
|
||||
export SERVICE=@CMAKE_CURRENT_BINARY_DIR@/package-deb/sunshine/usr/lib/systemd/user
|
||||
export ASSETS=@CMAKE_CURRENT_BINARY_DIR@/package-deb/sunshine/etc/sunshine
|
||||
|
||||
mkdir -p $DEBIAN
|
||||
mkdir -p $RULES
|
||||
mkdir -p $BIN
|
||||
mkdir -p $ASSETS
|
||||
mkdir -p $ASSETS/shaders
|
||||
mkdir -p $SERVICE
|
||||
|
||||
if [ ! -f sunshine ]; then
|
||||
echo "Error: Can't find sunshine"
|
||||
@@ -35,11 +37,22 @@ Package: sunshine
|
||||
Architecture: amd64
|
||||
Maintainer: @loki
|
||||
Priority: optional
|
||||
Version: 0.4.1
|
||||
Depends: libssl1.1, libavdevice58, libboost-thread1.71.0, libboost-filesystem1.71.0, libboost-log1.71.0, libpulse0, libopus0, libxcb-shm0, libxcb-xfixes0
|
||||
Version: 0.11.1
|
||||
Depends: libssl1.1, libavdevice58, libboost-thread1.67.0 | libboost-thread1.71.0 | libboost-thread1.74.0, libboost-filesystem1.67.0 | libboost-filesystem1.71.0 | libboost-filesystem1.74.0, libboost-log1.67.0 | libboost-log1.71.0 | libboost-log1.74.0, libpulse0, libopus0, libxcb-shm0, libxcb-xfixes0, libxtst6, libevdev2, libdrm2, libcap2
|
||||
Description: Gamestream host for Moonlight
|
||||
EOF
|
||||
|
||||
cat << 'EOF' > $DEBIAN/preinst
|
||||
#Store backup for old config files to prevent it from being overwritten
|
||||
if [ -f /etc/sunshine/sunshine.conf ]; then
|
||||
cp /etc/sunshine/sunshine.conf /etc/sunshine/sunshine.conf.old
|
||||
fi
|
||||
|
||||
if [ -f /etc/sunshine/apps_linux.json ]; then
|
||||
cp /etc/sunshine/apps_linux.json /etc/sunshine/apps_linux.json.old
|
||||
fi
|
||||
EOF
|
||||
|
||||
cat << 'EOF' > $DEBIAN/postinst
|
||||
#!/bin/sh
|
||||
|
||||
@@ -54,6 +67,34 @@ if [ -f /etc/group ]; then
|
||||
else
|
||||
echo "Warning: /etc/group not found"
|
||||
fi
|
||||
|
||||
if [ -f /etc/sunshine/sunshine.conf.old ]; then
|
||||
echo "Restoring old sunshine.conf"
|
||||
mv /etc/sunshine/sunshine.conf.old /etc/sunshine/sunshine.conf
|
||||
fi
|
||||
|
||||
if [ -f /etc/sunshine/apps_linux.json.old ]; then
|
||||
echo "Restoring old apps_linux.json"
|
||||
mv /etc/sunshine/apps_linux.json.old /etc/sunshine/apps_linux.json
|
||||
fi
|
||||
|
||||
# Update permissions on config files for Web Manager
|
||||
if [ -f /etc/sunshine/apps_linux.json ]; then
|
||||
echo "chmod 666 /etc/sunshine/apps_linux.json"
|
||||
chmod 666 /etc/sunshine/apps_linux.json
|
||||
fi
|
||||
|
||||
if [ -f /etc/sunshine/sunshine.conf ]; then
|
||||
echo "chmod 666 /etc/sunshine/sunshine.conf"
|
||||
chmod 666 /etc/sunshine/sunshine.conf
|
||||
fi
|
||||
|
||||
# Ensure Sunshine can grab images from KMS
|
||||
path_to_setcap=$(which setcap)
|
||||
if [ -x "$path_to_setcap" ] ; then
|
||||
echo "$path_to_setcap cap_sys_admin+p /usr/bin/sunshine"
|
||||
$path_to_setcap cap_sys_admin+p /usr/bin/sunshine
|
||||
fi
|
||||
EOF
|
||||
|
||||
cat << 'EOF' > $RULES/85-sunshine-rules.rules
|
||||
@@ -63,10 +104,16 @@ EOF
|
||||
cp sunshine $BIN/sunshine
|
||||
cp @CMAKE_CURRENT_SOURCE_DIR@/assets/apps_linux.json $ASSETS/apps_linux.json
|
||||
cp @CMAKE_CURRENT_SOURCE_DIR@/assets/sunshine.conf $ASSETS/sunshine.conf
|
||||
cp @CMAKE_CURRENT_BINARY_DIR@/sunshine.service $SERVICE/sunshine.service
|
||||
cp -r @CMAKE_CURRENT_SOURCE_DIR@/assets/web $ASSETS/web
|
||||
cp -r @CMAKE_CURRENT_SOURCE_DIR@/assets/shaders/opengl $ASSETS/shaders/opengl
|
||||
|
||||
chmod 755 $DEBIAN/postinst
|
||||
chmod 755 $DEBIAN/preinst
|
||||
chmod 755 $BIN/sunshine
|
||||
chmod 644 $RULES/85-sunshine-rules.rules
|
||||
chmod 666 $ASSETS/apps_linux.json
|
||||
chmod 666 $ASSETS/sunshine.conf
|
||||
|
||||
cd package-deb
|
||||
if fakeroot dpkg-deb --build sunshine; then
|
||||
|
||||
Submodule moonlight-common-c deleted from 5d09d43b08
18
scripts/Dockerfile-2004
Normal file
18
scripts/Dockerfile-2004
Normal file
@@ -0,0 +1,18 @@
|
||||
FROM ubuntu:20.04 AS sunshine-2004
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
ARG TZ="Europe/London"
|
||||
|
||||
RUN apt-get update -y && \
|
||||
apt-get install -y \
|
||||
git wget gcc-10 g++-10 build-essential cmake libssl-dev libavdevice-dev libboost-thread-dev libboost-filesystem-dev libboost-log-dev libpulse-dev libopus-dev libxtst-dev libx11-dev libxrandr-dev libxfixes-dev libevdev-dev libxcb1-dev libxcb-shm0-dev libxcb-xfixes0-dev libdrm-dev libcap-dev libwayland-dev
|
||||
|
||||
RUN cp /usr/bin/gcc-10 /usr/bin/gcc && cp /usr/bin/g++-10 /usr/bin/gcc-10
|
||||
|
||||
RUN wget https://developer.download.nvidia.com/compute/cuda/11.4.2/local_installers/cuda_11.4.2_470.57.02_linux.run --progress=bar:force:noscroll -q --show-progress -O /root/cuda.run && chmod a+x /root/cuda.run
|
||||
RUN /root/cuda.run --silent --toolkit --toolkitpath=/usr --no-opengl-libs --no-man-page --no-drm && rm /root/cuda.run
|
||||
|
||||
COPY build-private.sh /root/build.sh
|
||||
|
||||
|
||||
ENTRYPOINT ["/root/build.sh"]
|
||||
13
scripts/Dockerfile-2104
Normal file
13
scripts/Dockerfile-2104
Normal file
@@ -0,0 +1,13 @@
|
||||
FROM ubuntu:21.04 AS sunshine-2104
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
ARG TZ="Europe/London"
|
||||
|
||||
RUN apt-get update -y && \
|
||||
apt-get install -y \
|
||||
git build-essential cmake libssl-dev libavdevice-dev libboost-thread-dev libboost-filesystem-dev libboost-log-dev libpulse-dev libopus-dev libxtst-dev libx11-dev libxrandr-dev libxfixes-dev libevdev-dev libxcb1-dev libxcb-shm0-dev libxcb-xfixes0-dev libdrm-dev libcap-dev libwayland-dev nvidia-cuda-dev nvidia-cuda-toolkit
|
||||
|
||||
COPY build-private.sh /root/build.sh
|
||||
|
||||
|
||||
ENTRYPOINT ["/root/build.sh"]
|
||||
14
scripts/Dockerfile-debian
Normal file
14
scripts/Dockerfile-debian
Normal file
@@ -0,0 +1,14 @@
|
||||
FROM debian:bullseye AS sunshine-debian
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
ARG TZ="Europe/London"
|
||||
|
||||
RUN echo deb http://deb.debian.org/debian/ bullseye main contrib non-free | tee /etc/apt/sources.list.d/non-free.list
|
||||
RUN apt-get update -y && \
|
||||
apt-get install -y \
|
||||
git build-essential cmake libssl-dev libavdevice-dev libboost-thread-dev libboost-filesystem-dev libboost-log-dev libpulse-dev libopus-dev libxtst-dev libx11-dev libxrandr-dev libxfixes-dev libevdev-dev libxcb1-dev libxcb-shm0-dev libxcb-xfixes0-dev libdrm-dev libcap-dev libwayland-dev nvidia-cuda-dev nvidia-cuda-toolkit
|
||||
|
||||
COPY build-private.sh /root/build.sh
|
||||
|
||||
|
||||
ENTRYPOINT ["/root/build.sh"]
|
||||
53
scripts/README.md
Normal file
53
scripts/README.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Introduction
|
||||
Sunshine is a Gamestream host for Moonlight
|
||||
|
||||
[](https://ci.appveyor.com/project/loki-47-6F-64/sunshine/branch/master)
|
||||
[](https://github.com/Loki-47-6F-64/sunshine/releases)
|
||||
|
||||
You may wish to simply build sunshine from source, without bloating your OS with development files.
|
||||
These scripts will create a docker images that have the necessary packages. As a result, removing the development files after you're done is a single command away.
|
||||
These scripts use docker under the hood, as such, they can only be used to compile the Linux version
|
||||
|
||||
|
||||
#### Requirements
|
||||
|
||||
```
|
||||
sudo apt install docker
|
||||
```
|
||||
|
||||
#### instructions
|
||||
|
||||
You'll require one of the following Dockerfiles:
|
||||
* Dockerfile-2004 --> Ubuntu 20.04
|
||||
* Dockerfile-2104 --> Ubuntu 21.04
|
||||
* Dockerfile-debian --> Debian Bullseye
|
||||
|
||||
Depending on your system, the build-* scripts may need root privilleges
|
||||
|
||||
First, the docker container needs to be created:
|
||||
```
|
||||
cd scripts
|
||||
./build-container.sh -f Dockerfile-<name>
|
||||
```
|
||||
|
||||
Then, the sources will be compiled and the debian package generated:
|
||||
```
|
||||
./build-sunshine -p -s ..
|
||||
```
|
||||
You can run `build-sunshine -p -s ..` again as long as the docker container exists.
|
||||
|
||||
```
|
||||
git pull
|
||||
./build-sunshine -p -s ..
|
||||
```
|
||||
|
||||
Optionally, the docker container can be removed after you're finished:
|
||||
```
|
||||
./build-container.sh -c delete
|
||||
```
|
||||
|
||||
Finally install the resulting package:
|
||||
```
|
||||
sudo apt install -f sunshine-build/sunshine.deb
|
||||
```
|
||||
|
||||
178
scripts/build-container.sh
Executable file
178
scripts/build-container.sh
Executable file
@@ -0,0 +1,178 @@
|
||||
#/bin/bash -e
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 [OPTIONS]"
|
||||
echo " -c: command --> default [build]"
|
||||
echo " | delete --> Delete the container, Dockerfile isn't mandatory"
|
||||
echo " | build --> Build the container, Dockerfile is mandatory"
|
||||
echo " | compile --> Builds the container, then compiles it. Dockerfile is mandatory"
|
||||
echo ""
|
||||
echo " -s: path: The path to the source for compilation"
|
||||
echo " -n: name: Docker container name --> default [sunshine]"
|
||||
echo " --> all: Build/Compile/Delete all available docker containers"
|
||||
echo " -f: Dockerfile: The name of the docker file"
|
||||
}
|
||||
|
||||
# Attempt to turn relative paths into absolute paths
|
||||
absolute_path() {
|
||||
RELATIVE_PATH=$1
|
||||
if which realpath >/dev/null 2>/dev/null
|
||||
then
|
||||
RELATIVE_PATH=$(realpath $RELATIVE_PATH)
|
||||
else
|
||||
echo "Warning: realpath is not installed on your system, ensure [$1] is absolute"
|
||||
fi
|
||||
|
||||
RETURN=$RELATIVE_PATH
|
||||
}
|
||||
|
||||
CONTAINER_NAME=sunshine
|
||||
COMMAND=BUILD
|
||||
|
||||
build_container() {
|
||||
CONTAINER_NAME=$1
|
||||
DOCKER_FILE=$2
|
||||
|
||||
if [ ! -f "$DOCKER_FILE" ]
|
||||
then
|
||||
echo "Error: $DOCKER_FILE doesn't exist"
|
||||
exit 7
|
||||
fi
|
||||
|
||||
echo "docker build . -t $CONTAINER_NAME -f $DOCKER_FILE"
|
||||
docker build . -t "$CONTAINER_NAME" -f "$DOCKER_FILE"
|
||||
}
|
||||
|
||||
delete() {
|
||||
CONTAINER_NAME_UPPER=$(echo "$CONTAINER_NAME" | tr '[:lower:]' '[:upper:]')
|
||||
if [ "$CONTAINER_NAME_UPPER" = "ALL" ]
|
||||
then
|
||||
shopt -s nullglob
|
||||
for file in $(find . -maxdepth 1 -iname "Dockerfile-*" -type f)
|
||||
do
|
||||
CURRENT_CONTAINER="sunshine-$(echo $file | cut -c 14-)"
|
||||
|
||||
if docker inspect "$CURRENT_CONTAINER" > /dev/null 2> /dev/null
|
||||
then
|
||||
echo "docker rmi $CURRENT_CONTAINER"
|
||||
docker rmi "$CURRENT_CONTAINER"
|
||||
fi
|
||||
done
|
||||
shopt -u nullglob #revert nullglob back to it's normal default state
|
||||
else
|
||||
if docker inspect "$CONTAINER_NAME" > /dev/null 2> /dev/null
|
||||
then
|
||||
echo "docker rmi $CONTAINER_NAME"
|
||||
docker rmi $CONTAINER_NAME
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
build() {
|
||||
CONTAINER_NAME_UPPER=$(echo "$CONTAINER_NAME" | tr '[:lower:]' '[:upper:]')
|
||||
if [ "$CONTAINER_NAME_UPPER" = "ALL" ]
|
||||
then
|
||||
shopt -s nullglob
|
||||
for file in $(find . -maxdepth 1 -iname "Dockerfile-*" -type f)
|
||||
do
|
||||
CURRENT_CONTAINER="sunshine-$(echo $file | cut -c 14-)"
|
||||
build_container "$CURRENT_CONTAINER" "$file"
|
||||
done
|
||||
shopt -u nullglob #revert nullglob back to it's normal default state
|
||||
else
|
||||
if [[ -z "$DOCKER_FILE" ]]
|
||||
then
|
||||
echo "Error: if container name isn't equal to 'all', you need to specify the Dockerfile"
|
||||
exit 6
|
||||
fi
|
||||
|
||||
build_container "$CONTAINER_NAME" "$DOCKER_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
abort() {
|
||||
echo "$1"
|
||||
exit 10
|
||||
}
|
||||
|
||||
compile() {
|
||||
CONTAINER_NAME_UPPER=$(echo "$CONTAINER_NAME" | tr '[:lower:]' '[:upper:]')
|
||||
if [ "$CONTAINER_NAME_UPPER" = "ALL" ]
|
||||
then
|
||||
shopt -s nullglob
|
||||
|
||||
# If any docker container doesn't exist, we cannot compile all of them
|
||||
for file in $(find . -maxdepth 1 -iname "Dockerfile-*" -type f)
|
||||
do
|
||||
CURRENT_CONTAINER="sunshine-$(echo $file | cut -c 14-)"
|
||||
|
||||
# If container doesn't exist --> abort.
|
||||
docker inspect "$CURRENT_CONTAINER" > /dev/null 2> /dev/null || abort "Error: container image [$CURRENT_CONTAINER] doesn't exist"
|
||||
done
|
||||
|
||||
for file in $(find . -maxdepth 1 -iname "Dockerfile-*" -type f)
|
||||
do
|
||||
CURRENT_CONTAINER="sunshine-$(echo $file | cut -c 14-)"
|
||||
|
||||
echo "$PWD/build-sunshine.sh -p -n $CURRENT_CONTAINER $SUNSHINE_SOURCES"
|
||||
"$PWD/build-sunshine.sh" -p -n "$CURRENT_CONTAINER" $SUNSHINE_SOURCES
|
||||
done
|
||||
shopt -u nullglob #revert nullglob back to it's normal default state
|
||||
else
|
||||
# If container exists
|
||||
if docker inspect "$CONTAINER_NAME" > /dev/null 2> /dev/null
|
||||
then
|
||||
echo "$PWD/build-sunshine.sh -p -n $CONTAINER_NAME $SUNSHINE_SOURCES"
|
||||
"$PWD/build-sunshine.sh" -p -n "$CONTAINER_NAME" $SUNSHINE_SOURCES
|
||||
else
|
||||
echo "Error: container image [$CONTAINER_NAME] doesn't exist"
|
||||
exit 9
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
while getopts ":c:hn:f:s:" arg; do
|
||||
case ${arg} in
|
||||
s)
|
||||
SUNSHINE_SOURCES="-s $OPTARG"
|
||||
;;
|
||||
c)
|
||||
COMMAND=$(echo $OPTARG | tr '[:lower:]' '[:upper:]')
|
||||
;;
|
||||
n)
|
||||
echo "Container name: $OPTARG"
|
||||
CONTAINER_NAME="$OPTARG"
|
||||
;;
|
||||
f)
|
||||
echo "Using Dockerfile [$OPTARG]"
|
||||
DOCKER_FILE="$OPTARG"
|
||||
;;
|
||||
h)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo "$0 set to $(echo $COMMAND | tr '[:upper:]' '[:lower:]')"
|
||||
|
||||
if [ "$COMMAND" = "BUILD" ]
|
||||
then
|
||||
echo "Start building..."
|
||||
delete
|
||||
build
|
||||
echo "Done."
|
||||
elif [ "$COMMAND" = "COMPILE" ]
|
||||
then
|
||||
echo "Start compiling..."
|
||||
compile
|
||||
echo "Done."
|
||||
elif [ "$COMMAND" = "DELETE" ]
|
||||
then
|
||||
echo "Start deleting..."
|
||||
delete
|
||||
echo "Done."
|
||||
else
|
||||
echo "Unknown command [$(echo $COMMAND | tr '[:upper:]' '[:lower:]')]"
|
||||
exit 4
|
||||
fi
|
||||
34
scripts/build-private.sh
Executable file
34
scripts/build-private.sh
Executable file
@@ -0,0 +1,34 @@
|
||||
#!/bin/bash -e
|
||||
|
||||
CMAKE_BUILD_TYPE="${CMAKE_BUILD_TYPE:-Release}"
|
||||
SUNSHINE_EXECUTABLE_PATH="${SUNSHINE_EXECUTABLE_PATH:-/usr/bin/sunshine}"
|
||||
SUNSHINE_ASSETS_DIR="${SUNSHINE_ASSETS_DIR:-/etc/sunshine}"
|
||||
|
||||
|
||||
SUNSHINE_ROOT="${SUNSHINE_ROOT:-/root/sunshine}"
|
||||
SUNSHINE_TAG="${SUNSHINE_TAG:-master}"
|
||||
SUNSHINE_GIT_URL="${SUNSHINE_GIT_URL:-https://github.com/loki-47-6F-64/sunshine.git}"
|
||||
|
||||
|
||||
SUNSHINE_ENABLE_WAYLAND=${SUNSHINE_ENABLE_WAYLAND:-ON}
|
||||
SUNSHINE_ENABLE_X11=${SUNSHINE_ENABLE_X11:-ON}
|
||||
SUNSHINE_ENABLE_DRM=${SUNSHINE_ENABLE_DRM:-ON}
|
||||
SUNSHINE_ENABLE_CUDA=${SUNSHINE_ENABLE_CUDA:-ON}
|
||||
|
||||
# For debugging, it would be usefull to have the sources on the host.
|
||||
if [[ ! -d "$SUNSHINE_ROOT" ]]
|
||||
then
|
||||
git clone --depth 1 --branch "$SUNSHINE_TAG" "$SUNSHINE_GIT_URL" --recurse-submodules "$SUNSHINE_ROOT"
|
||||
fi
|
||||
|
||||
if [[ ! -d /root/sunshine-build ]]
|
||||
then
|
||||
mkdir -p /root/sunshine-build
|
||||
fi
|
||||
cd /root/sunshine-build
|
||||
|
||||
cmake "-DCMAKE_BUILD_TYPE=$CMAKE_BUILD_TYPE" "-DSUNSHINE_EXECUTABLE_PATH=$SUNSHINE_EXECUTABLE_PATH" "-DSUNSHINE_ASSETS_DIR=$SUNSHINE_ASSETS_DIR" "-DSUNSHINE_ENABLE_WAYLAND=$SUNSHINE_ENABLE_WAYLAND" "-DSUNSHINE_ENABLE_X11=$SUNSHINE_ENABLE_X11" "-DSUNSHINE_ENABLE_DRM=$SUNSHINE_ENABLE_DRM" "-DSUNSHINE_ENABLE_CUDA=$SUNSHINE_ENABLE_CUDA" "$SUNSHINE_ROOT"
|
||||
|
||||
make -j ${nproc}
|
||||
|
||||
./gen-deb
|
||||
114
scripts/build-sunshine.sh
Executable file
114
scripts/build-sunshine.sh
Executable file
@@ -0,0 +1,114 @@
|
||||
#/bin/bash -e
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0"
|
||||
echo " -d: Generate a debug build"
|
||||
echo " -p: Generate a debian package"
|
||||
echo " -u: The input device is not a TTY"
|
||||
echo " -n name: Docker container name --> default [sunshine]"
|
||||
echo " -s path/to/sources/sunshine: Use local sources instead of a git repository"
|
||||
echo " -c path/to/cmake/binary/dir: Store cmake output on host OS"
|
||||
}
|
||||
|
||||
# Attempt to turn relative paths into absolute paths
|
||||
absolute_path() {
|
||||
RELATIVE_PATH=$1
|
||||
if which realpath >/dev/null 2>/dev/null
|
||||
then
|
||||
RELATIVE_PATH=$(realpath $RELATIVE_PATH)
|
||||
else
|
||||
echo "Warning: realpath is not installed on your system, ensure [$1] is absolute"
|
||||
fi
|
||||
|
||||
RETURN=$RELATIVE_PATH
|
||||
}
|
||||
|
||||
CMAKE_BUILD_TYPE="-e CMAKE_BUILD_TYPE=Release"
|
||||
SUNSHINE_PACKAGE_BUILD=OFF
|
||||
SUNSHINE_GIT_URL=https://github.com/loki-47-6F-64/sunshine.git
|
||||
CONTAINER_NAME=sunshine
|
||||
|
||||
# Docker will fail if ctrl+c is passed through and the input is not a tty
|
||||
DOCKER_INTERACTIVE=-ti
|
||||
|
||||
while getopts ":dpuhc:s:n:" arg; do
|
||||
case ${arg} in
|
||||
u)
|
||||
echo "Input device is not a TTY"
|
||||
USERNAME="$USER"
|
||||
unset DOCKER_INTERACTIVE
|
||||
;;
|
||||
d)
|
||||
echo "Creating debug build"
|
||||
CMAKE_BUILD_TYPE="-e CMAKE_BUILD_TYPE=Debug"
|
||||
;;
|
||||
p)
|
||||
echo "Creating package build"
|
||||
SUNSHINE_PACKAGE_BUILD=ON
|
||||
SUNSHINE_ASSETS_DIR="-e SUNSHINE_ASSETS_DIR=/etc/sunshine"
|
||||
SUNSHINE_EXECUTABLE_PATH="-e SUNSHINE_EXECUTABLE_PATH=/usr/bin/sunshine"
|
||||
;;
|
||||
s)
|
||||
absolute_path "$OPTARG"
|
||||
OPTARG="$RETURN"
|
||||
echo "Using sources from $OPTARG"
|
||||
SUNSHINE_ROOT="-v $OPTARG:/root/sunshine"
|
||||
;;
|
||||
c)
|
||||
[ "$USERNAME" == "" ] && USERNAME=$(logname)
|
||||
|
||||
absolute_path "$OPTARG"
|
||||
OPTARG="$RETURN"
|
||||
|
||||
echo "Using $OPTARG as cmake binary dir"
|
||||
if [[ ! -d $OPTARG ]]
|
||||
then
|
||||
echo "cmake binary dir doesn't exist, a new one will be created."
|
||||
mkdir -p "$OPTARG"
|
||||
[ "$USERNAME" == "$USER"] || chown $USERNAME:$USERNAME "$OPTARG"
|
||||
fi
|
||||
|
||||
CMAKE_ROOT="-v $OPTARG:/root/sunshine-build"
|
||||
;;
|
||||
n)
|
||||
echo "Container name: $OPTARG"
|
||||
CONTAINER_NAME=$OPTARG
|
||||
;;
|
||||
h)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
[ "$USERNAME" = "" ] && USERNAME=$(logname)
|
||||
|
||||
BUILD_DIR="$PWD/$CONTAINER_NAME-build"
|
||||
[ "$SUNSHINE_ASSETS_DIR" = "" ] && SUNSHINE_ASSETS_DIR="-e SUNSHINE_ASSETS_DIR=$BUILD_DIR/assets"
|
||||
[ "$SUNSHINE_EXECUTABLE_PATH" = "" ] && SUNSHINE_EXECUTABLE_PATH="-e SUNSHINE_EXECUTABLE_PATH=$BUILD_DIR/sunshine"
|
||||
|
||||
echo "docker run $DOCKER_INTERACTIVE --privileged $SUNSHINE_ROOT $CMAKE_ROOT $SUNSHINE_ASSETS_DIR $SUNSHINE_EXECUTABLE_PATH $CMAKE_BUILD_TYPE --name $CONTAINER_NAME $CONTAINER_NAME"
|
||||
docker run $DOCKER_INTERACTIVE --privileged $SUNSHINE_ROOT $CMAKE_ROOT $SUNSHINE_ASSETS_DIR $SUNSHINE_EXECUTABLE_PATH $CMAKE_BUILD_TYPE --name $CONTAINER_NAME $CONTAINER_NAME
|
||||
|
||||
exit_code=$?
|
||||
|
||||
if [ $exit_code -eq 0 ]
|
||||
then
|
||||
mkdir -p $BUILD_DIR
|
||||
case $SUNSHINE_PACKAGE_BUILD in
|
||||
ON)
|
||||
echo "Downloading package to: $BUILD_DIR/$CONTAINER_NAME.deb"
|
||||
docker cp $CONTAINER_NAME:/root/sunshine-build/package-deb/sunshine.deb "$BUILD_DIR/$CONTAINER_NAME.deb"
|
||||
;;
|
||||
*)
|
||||
echo "Downloading binary and assets to: $BUILD_DIR"
|
||||
docker cp $CONTAINER_NAME:/root/sunshine/assets "$BUILD_DIR"
|
||||
docker cp $CONTAINER_NAME:/root/sunshine-build/sunshine "$BUILD_DIR"
|
||||
;;
|
||||
esac
|
||||
echo "chown --recursive $USERNAME:$USERNAME $BUILD_DIR"
|
||||
chown --recursive $USERNAME:$USERNAME "$BUILD_DIR"
|
||||
fi
|
||||
|
||||
echo "Removing docker container $CONTAINER_NAME"
|
||||
docker rm $CONTAINER_NAME
|
||||
BIN
sunshine.ico
Normal file
BIN
sunshine.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
@@ -72,7 +72,9 @@ opus_stream_config_t stream_configs[MAX_STREAM_CONFIG] {
|
||||
|
||||
auto control_shared = safe::make_shared<audio_ctx_t>(start_audio_control, stop_audio_control);
|
||||
|
||||
void encodeThread(packet_queue_t packets, sample_queue_t samples, config_t config, void *channel_data) {
|
||||
void encodeThread(sample_queue_t samples, config_t config, void *channel_data) {
|
||||
auto packets = mail::man->queue<packet_t>(mail::audio_packets);
|
||||
|
||||
//FIXME: Pick correct opus_stream_config_t based on config.channels
|
||||
auto stream = &stream_configs[map_stream(config.channels, config.flags[config_t::HIGH_QUALITY])];
|
||||
|
||||
@@ -85,24 +87,39 @@ void encodeThread(packet_queue_t packets, sample_queue_t samples, config_t confi
|
||||
OPUS_APPLICATION_AUDIO,
|
||||
nullptr) };
|
||||
|
||||
// For some reason, audio is crackling when the encoder is set to constant bitstream.
|
||||
// We simulate a constant bitstream with OPUS_SET_BITRATE(OPUS_BITRATE_MAX) -->
|
||||
// which tries to occupy as much space as possible in the packet
|
||||
opus_multistream_encoder_ctl(opus.get(), OPUS_SET_BITRATE(OPUS_BITRATE_MAX));
|
||||
|
||||
auto frame_size = config.packetDuration * stream->sampleRate / 1000;
|
||||
while(auto sample = samples->pop()) {
|
||||
packet_t packet { 16 * 1024 }; // 16KB
|
||||
buffer_t packet { 1400 }; // 1KB
|
||||
|
||||
int bytes = opus_multistream_encode(opus.get(), sample->data(), frame_size, std::begin(packet), packet.size());
|
||||
if(bytes < 0) {
|
||||
BOOST_LOG(error) << opus_strerror(bytes);
|
||||
BOOST_LOG(error) << "Couldn't encode audio: "sv << opus_strerror(bytes);
|
||||
packets->stop();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Even with OPUS_SET_BITRATE(OPUS_BITRATE_MAX), silent packets are smaller than the rest
|
||||
// Drop silent packets to ensure Moonlight won't complain
|
||||
// A packet size of 128 seems a reasonable enough threshold
|
||||
if(bytes < 128) {
|
||||
BOOST_LOG(verbose) << "Dropped silent packet"sv;
|
||||
continue;
|
||||
}
|
||||
|
||||
packet.fake_resize(bytes);
|
||||
packets->raise(channel_data, std::move(packet));
|
||||
}
|
||||
}
|
||||
|
||||
void capture(safe::signal_t *shutdown_event, packet_queue_t packets, config_t config, void *channel_data) {
|
||||
void capture(safe::mail_t mail, config_t config, void *channel_data) {
|
||||
auto shutdown_event = mail->event<bool>(mail::shutdown);
|
||||
|
||||
//FIXME: Pick correct opus_stream_config_t based on config.channels
|
||||
auto stream = &stream_configs[map_stream(config.channels, config.flags[config_t::HIGH_QUALITY])];
|
||||
|
||||
@@ -113,7 +130,7 @@ void capture(safe::signal_t *shutdown_event, packet_queue_t packets, config_t co
|
||||
|
||||
auto &control = ref->control;
|
||||
if(!control) {
|
||||
BOOST_LOG(error) << "Couldn't create audio control"sv;
|
||||
shutdown_event->view();
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -146,7 +163,7 @@ void capture(safe::signal_t *shutdown_event, packet_queue_t packets, config_t co
|
||||
}
|
||||
|
||||
auto samples = std::make_shared<sample_queue_t::element_type>(30);
|
||||
std::thread thread { encodeThread, packets, samples, config, channel_data };
|
||||
std::thread thread { encodeThread, samples, config, channel_data };
|
||||
|
||||
auto fg = util::fail_guard([&]() {
|
||||
samples->stop();
|
||||
@@ -206,23 +223,32 @@ int map_stream(int channels, bool quality) {
|
||||
}
|
||||
|
||||
int start_audio_control(audio_ctx_t &ctx) {
|
||||
auto fg = util::fail_guard([]() {
|
||||
BOOST_LOG(warning) << "There will be no audio"sv;
|
||||
});
|
||||
|
||||
ctx.sink_flag = std::make_unique<std::atomic_bool>(false);
|
||||
|
||||
if(!(ctx.control = platf::audio_control())) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
auto sink = ctx.control->sink_info();
|
||||
if(!sink) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// The default sink has not been replaced yet.
|
||||
ctx.restore_sink = false;
|
||||
|
||||
if(!(ctx.control = platf::audio_control())) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto sink = ctx.control->sink_info();
|
||||
if(!sink) {
|
||||
// Let the calling code know it failed
|
||||
ctx.control.reset();
|
||||
return 0;
|
||||
}
|
||||
|
||||
ctx.sink = std::move(*sink);
|
||||
|
||||
fg.disable();
|
||||
return 0;
|
||||
}
|
||||
|
||||
void stop_audio_control(audio_ctx_t &ctx) {
|
||||
// restore audio-sink if applicable
|
||||
if(!ctx.restore_sink) {
|
||||
|
||||
@@ -37,9 +37,9 @@ struct config_t {
|
||||
std::bitset<MAX_FLAGS> flags;
|
||||
};
|
||||
|
||||
using packet_t = util::buffer_t<std::uint8_t>;
|
||||
using packet_queue_t = std::shared_ptr<safe::queue_t<std::pair<void *, packet_t>>>;
|
||||
void capture(safe::signal_t *shutdown_event, packet_queue_t packets, config_t config, void *channel_data);
|
||||
using buffer_t = util::buffer_t<std::uint8_t>;
|
||||
using packet_t = std::pair<void *, buffer_t>;
|
||||
void capture(safe::mail_t mail, config_t config, void *channel_data);
|
||||
} // namespace audio
|
||||
|
||||
#endif
|
||||
|
||||
300
sunshine/cbs.cpp
Normal file
300
sunshine/cbs.cpp
Normal file
@@ -0,0 +1,300 @@
|
||||
extern "C" {
|
||||
#include <cbs/cbs_h264.h>
|
||||
#include <cbs/cbs_h265.h>
|
||||
#include <cbs/video_levels.h>
|
||||
#include <libavcodec/avcodec.h>
|
||||
#include <libavutil/pixdesc.h>
|
||||
}
|
||||
|
||||
#include "cbs.h"
|
||||
#include "main.h"
|
||||
#include "utility.h"
|
||||
|
||||
using namespace std::literals;
|
||||
namespace cbs {
|
||||
void close(CodedBitstreamContext *c) {
|
||||
ff_cbs_close(&c);
|
||||
}
|
||||
|
||||
using ctx_t = util::safe_ptr<CodedBitstreamContext, close>;
|
||||
|
||||
class frag_t : public CodedBitstreamFragment {
|
||||
public:
|
||||
frag_t(frag_t &&o) {
|
||||
std::copy((std::uint8_t *)&o, (std::uint8_t *)(&o + 1), (std::uint8_t *)this);
|
||||
|
||||
o.data = nullptr;
|
||||
o.units = nullptr;
|
||||
};
|
||||
|
||||
frag_t() {
|
||||
std::fill_n((std::uint8_t *)this, sizeof(*this), 0);
|
||||
}
|
||||
|
||||
frag_t &operator=(frag_t &&o) {
|
||||
std::copy((std::uint8_t *)&o, (std::uint8_t *)(&o + 1), (std::uint8_t *)this);
|
||||
|
||||
o.data = nullptr;
|
||||
o.units = nullptr;
|
||||
|
||||
return *this;
|
||||
};
|
||||
|
||||
|
||||
~frag_t() {
|
||||
if(data || units) {
|
||||
ff_cbs_fragment_free(this);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
util::buffer_t<std::uint8_t> write(const cbs::ctx_t &cbs_ctx, std::uint8_t nal, void *uh, AVCodecID codec_id) {
|
||||
cbs::frag_t frag;
|
||||
auto err = ff_cbs_insert_unit_content(&frag, -1, nal, uh, nullptr);
|
||||
if(err < 0) {
|
||||
char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 };
|
||||
BOOST_LOG(error) << "Could not insert NAL unit SPS: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err);
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
err = ff_cbs_write_fragment_data(cbs_ctx.get(), &frag);
|
||||
if(err < 0) {
|
||||
char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 };
|
||||
BOOST_LOG(error) << "Could not write fragment data: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err);
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
// frag.data_size * 8 - frag.data_bit_padding == bits in fragment
|
||||
util::buffer_t<std::uint8_t> data { frag.data_size };
|
||||
std::copy_n(frag.data, frag.data_size, std::begin(data));
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
util::buffer_t<std::uint8_t> write(std::uint8_t nal, void *uh, AVCodecID codec_id) {
|
||||
cbs::ctx_t cbs_ctx;
|
||||
ff_cbs_init(&cbs_ctx, codec_id, nullptr);
|
||||
|
||||
return write(cbs_ctx, nal, uh, codec_id);
|
||||
}
|
||||
|
||||
util::buffer_t<std::uint8_t> 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 */
|
||||
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;
|
||||
auto mb_height = (FFALIGN(ctx->height, 16) / 16) * 16;
|
||||
|
||||
|
||||
sps.nal_unit_header.nal_ref_idc = 3;
|
||||
sps.nal_unit_header.nal_unit_type = H264_NAL_SPS;
|
||||
|
||||
sps.profile_idc = FF_PROFILE_H264_HIGH & 0xFF;
|
||||
|
||||
sps.constraint_set1_flag = 1;
|
||||
|
||||
if(ctx->level != FF_LEVEL_UNKNOWN) {
|
||||
sps.level_idc = ctx->level;
|
||||
}
|
||||
else {
|
||||
auto framerate = ctx->framerate;
|
||||
|
||||
auto level = ff_h264_guess_level(
|
||||
sps.profile_idc,
|
||||
ctx->bit_rate,
|
||||
framerate.num / framerate.den,
|
||||
mb_width,
|
||||
mb_height,
|
||||
dpb_frame);
|
||||
|
||||
if(!level) {
|
||||
BOOST_LOG(error) << "Could not guess h264 level"sv;
|
||||
|
||||
return {};
|
||||
}
|
||||
sps.level_idc = level->level_idc;
|
||||
}
|
||||
|
||||
sps.seq_parameter_set_id = 0;
|
||||
sps.chroma_format_idc = 1;
|
||||
|
||||
sps.log2_max_frame_num_minus4 = 3; //4;
|
||||
sps.pic_order_cnt_type = 0;
|
||||
sps.log2_max_pic_order_cnt_lsb_minus4 = 0; //4;
|
||||
|
||||
sps.max_num_ref_frames = dpb_frame;
|
||||
|
||||
sps.pic_width_in_mbs_minus1 = mb_width / 16 - 1;
|
||||
sps.pic_height_in_map_units_minus1 = mb_height / 16 - 1;
|
||||
|
||||
sps.frame_mbs_only_flag = 1;
|
||||
sps.direct_8x8_inference_flag = 1;
|
||||
|
||||
if(ctx->width != mb_width || ctx->height != mb_height) {
|
||||
sps.frame_cropping_flag = 1;
|
||||
sps.frame_crop_left_offset = 0;
|
||||
sps.frame_crop_top_offset = 0;
|
||||
sps.frame_crop_right_offset = (mb_width - ctx->width) / 2;
|
||||
sps.frame_crop_bottom_offset = (mb_height - ctx->height) / 2;
|
||||
}
|
||||
|
||||
sps.vui_parameters_present_flag = 1;
|
||||
|
||||
auto &vui = sps.vui;
|
||||
|
||||
vui.video_format = 5;
|
||||
vui.colour_description_present_flag = 1;
|
||||
vui.video_signal_type_present_flag = 1;
|
||||
vui.video_full_range_flag = ctx->color_range == AVCOL_RANGE_JPEG;
|
||||
vui.colour_primaries = ctx->color_primaries;
|
||||
vui.transfer_characteristics = ctx->color_trc;
|
||||
vui.matrix_coefficients = ctx->colorspace;
|
||||
|
||||
vui.low_delay_hrd_flag = 1 - vui.fixed_frame_rate_flag;
|
||||
|
||||
vui.bitstream_restriction_flag = 1;
|
||||
vui.motion_vectors_over_pic_boundaries_flag = 1;
|
||||
vui.log2_max_mv_length_horizontal = 15;
|
||||
vui.log2_max_mv_length_vertical = 15;
|
||||
vui.max_num_reorder_frames = max_b_depth;
|
||||
vui.max_dec_frame_buffering = max_b_depth + 1;
|
||||
|
||||
return write(sps.nal_unit_header.nal_unit_type, (void *)&sps.nal_unit_header, AV_CODEC_ID_H264);
|
||||
}
|
||||
|
||||
hevc_t make_sps_hevc(const AVCodecContext *avctx, const AVPacket *packet) {
|
||||
cbs::ctx_t ctx;
|
||||
if(ff_cbs_init(&ctx, AV_CODEC_ID_H265, nullptr)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
cbs::frag_t frag;
|
||||
|
||||
int err = ff_cbs_read_packet(ctx.get(), &frag, packet);
|
||||
if(err < 0) {
|
||||
char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 };
|
||||
BOOST_LOG(error) << "Couldn't read packet: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err);
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
|
||||
auto vps_p = ((CodedBitstreamH265Context *)ctx->priv_data)->active_vps;
|
||||
auto sps_p = ((CodedBitstreamH265Context *)ctx->priv_data)->active_sps;
|
||||
|
||||
H265RawSPS sps { *sps_p };
|
||||
H265RawVPS vps { *vps_p };
|
||||
|
||||
vps.profile_tier_level.general_profile_compatibility_flag[4] = 1;
|
||||
sps.profile_tier_level.general_profile_compatibility_flag[4] = 1;
|
||||
|
||||
auto &vui = sps.vui;
|
||||
std::memset(&vui, 0, sizeof(vui));
|
||||
|
||||
sps.vui_parameters_present_flag = 1;
|
||||
|
||||
// skip sample aspect ratio
|
||||
|
||||
vui.video_format = 5;
|
||||
vui.colour_description_present_flag = 1;
|
||||
vui.video_signal_type_present_flag = 1;
|
||||
vui.video_full_range_flag = avctx->color_range == AVCOL_RANGE_JPEG;
|
||||
vui.colour_primaries = avctx->color_primaries;
|
||||
vui.transfer_characteristics = avctx->color_trc;
|
||||
vui.matrix_coefficients = avctx->colorspace;
|
||||
|
||||
|
||||
vui.vui_timing_info_present_flag = vps.vps_timing_info_present_flag;
|
||||
vui.vui_num_units_in_tick = vps.vps_num_units_in_tick;
|
||||
vui.vui_time_scale = vps.vps_time_scale;
|
||||
vui.vui_poc_proportional_to_timing_flag = vps.vps_poc_proportional_to_timing_flag;
|
||||
vui.vui_num_ticks_poc_diff_one_minus1 = vps.vps_num_ticks_poc_diff_one_minus1;
|
||||
vui.vui_hrd_parameters_present_flag = 0;
|
||||
|
||||
vui.bitstream_restriction_flag = 1;
|
||||
vui.motion_vectors_over_pic_boundaries_flag = 1;
|
||||
vui.restricted_ref_pic_lists_flag = 1;
|
||||
vui.max_bytes_per_pic_denom = 0;
|
||||
vui.max_bits_per_min_cu_denom = 0;
|
||||
vui.log2_max_mv_length_horizontal = 15;
|
||||
vui.log2_max_mv_length_vertical = 15;
|
||||
|
||||
cbs::ctx_t write_ctx;
|
||||
ff_cbs_init(&write_ctx, AV_CODEC_ID_H265, nullptr);
|
||||
|
||||
|
||||
return hevc_t {
|
||||
nal_t {
|
||||
write(write_ctx, vps.nal_unit_header.nal_unit_type, (void *)&vps.nal_unit_header, AV_CODEC_ID_H265),
|
||||
write(ctx, vps_p->nal_unit_header.nal_unit_type, (void *)&vps_p->nal_unit_header, AV_CODEC_ID_H265),
|
||||
},
|
||||
|
||||
nal_t {
|
||||
write(write_ctx, sps.nal_unit_header.nal_unit_type, (void *)&sps.nal_unit_header, AV_CODEC_ID_H265),
|
||||
write(ctx, sps_p->nal_unit_header.nal_unit_type, (void *)&sps_p->nal_unit_header, AV_CODEC_ID_H265),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
util::buffer_t<std::uint8_t> read_sps_h264(const AVPacket *packet) {
|
||||
cbs::ctx_t ctx;
|
||||
if(ff_cbs_init(&ctx, AV_CODEC_ID_H264, nullptr)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
cbs::frag_t frag;
|
||||
|
||||
int err = ff_cbs_read_packet(ctx.get(), &frag, &*packet);
|
||||
if(err < 0) {
|
||||
char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 };
|
||||
BOOST_LOG(error) << "Couldn't read packet: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err);
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
auto h264 = (H264RawNALUnitHeader *)((CodedBitstreamH264Context *)ctx->priv_data)->active_sps;
|
||||
return write(h264->nal_unit_type, (void *)h264, AV_CODEC_ID_H264);
|
||||
}
|
||||
|
||||
h264_t make_sps_h264(const AVCodecContext *ctx, const AVPacket *packet) {
|
||||
return h264_t {
|
||||
make_sps_h264(ctx),
|
||||
read_sps_h264(packet),
|
||||
};
|
||||
}
|
||||
|
||||
bool validate_sps(const AVPacket *packet, int codec_id) {
|
||||
cbs::ctx_t ctx;
|
||||
if(ff_cbs_init(&ctx, (AVCodecID)codec_id, nullptr)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
cbs::frag_t frag;
|
||||
|
||||
int err = ff_cbs_read_packet(ctx.get(), &frag, packet);
|
||||
if(err < 0) {
|
||||
char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 };
|
||||
BOOST_LOG(error) << "Couldn't read packet: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if(codec_id == AV_CODEC_ID_H264) {
|
||||
auto h264 = (CodedBitstreamH264Context *)ctx->priv_data;
|
||||
|
||||
if(!h264->active_sps->vui_parameters_present_flag) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return ((CodedBitstreamH265Context *)ctx->priv_data)->active_sps->vui_parameters_present_flag;
|
||||
}
|
||||
} // namespace cbs
|
||||
34
sunshine/cbs.h
Normal file
34
sunshine/cbs.h
Normal file
@@ -0,0 +1,34 @@
|
||||
#ifndef SUNSHINE_CBS_H
|
||||
#define SUNSHINE_CBS_H
|
||||
|
||||
#include "utility.h"
|
||||
|
||||
struct AVPacket;
|
||||
struct AVCodecContext;
|
||||
|
||||
namespace cbs {
|
||||
|
||||
struct nal_t {
|
||||
util::buffer_t<std::uint8_t> _new;
|
||||
util::buffer_t<std::uint8_t> old;
|
||||
};
|
||||
|
||||
struct hevc_t {
|
||||
nal_t vps;
|
||||
nal_t sps;
|
||||
};
|
||||
|
||||
struct h264_t {
|
||||
nal_t sps;
|
||||
};
|
||||
|
||||
hevc_t make_sps_hevc(const AVCodecContext *ctx, const AVPacket *packet);
|
||||
h264_t make_sps_h264(const AVCodecContext *ctx, const AVPacket *packet);
|
||||
|
||||
/**
|
||||
* Check if SPS->VUI is present
|
||||
*/
|
||||
bool validate_sps(const AVPacket *packet, int codec_id);
|
||||
} // namespace cbs
|
||||
|
||||
#endif
|
||||
@@ -1,4 +1,5 @@
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <functional>
|
||||
#include <iostream>
|
||||
@@ -7,15 +8,20 @@
|
||||
#include <boost/asio.hpp>
|
||||
|
||||
#include "config.h"
|
||||
#include "main.h"
|
||||
#include "utility.h"
|
||||
|
||||
#include "platform/common.h"
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
using namespace std::literals;
|
||||
|
||||
#define CA_DIR "credentials"
|
||||
#define PRIVATE_KEY_FILE CA_DIR "/cakey.pem"
|
||||
#define CERTIFICATE_FILE CA_DIR "/cacert.pem"
|
||||
|
||||
#define APPS_JSON_PATH SUNSHINE_ASSETS_DIR "/" APPS_JSON
|
||||
#define APPS_JSON_PATH SUNSHINE_CONFIG_DIR "/" APPS_JSON
|
||||
namespace config {
|
||||
using namespace std::literals;
|
||||
|
||||
namespace nv {
|
||||
enum preset_e : int {
|
||||
@@ -93,16 +99,22 @@ enum quality_e : int {
|
||||
_default = 0,
|
||||
speed,
|
||||
balanced,
|
||||
//quality2,
|
||||
};
|
||||
|
||||
enum rc_e : int {
|
||||
enum class rc_hevc_e : int {
|
||||
constqp, /**< Constant QP mode */
|
||||
vbr_latency, /**< Latency Constrained Variable Bitrate */
|
||||
vbr_peak, /**< Peak Contrained Variable Bitrate */
|
||||
cbr, /**< Constant bitrate mode */
|
||||
};
|
||||
|
||||
enum class rc_h264_e : int {
|
||||
constqp, /**< Constant QP mode */
|
||||
cbr, /**< Constant bitrate mode */
|
||||
vbr_peak, /**< Peak Contrained Variable Bitrate */
|
||||
vbr_latency, /**< Latency Constrained Variable Bitrate */
|
||||
};
|
||||
|
||||
enum coder_e : int {
|
||||
_auto = 0,
|
||||
cabac,
|
||||
@@ -114,15 +126,25 @@ std::optional<quality_e> quality_from_view(const std::string_view &quality) {
|
||||
if(quality == #x##sv) return x
|
||||
_CONVERT_(speed);
|
||||
_CONVERT_(balanced);
|
||||
//_CONVERT_(quality2);
|
||||
if(quality == "default"sv) return _default;
|
||||
#undef _CONVERT_
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<rc_e> rc_from_view(const std::string_view &rc) {
|
||||
std::optional<int> rc_h264_from_view(const std::string_view &rc) {
|
||||
#define _CONVERT_(x) \
|
||||
if(rc == #x##sv) return x
|
||||
if(rc == #x##sv) return (int)rc_h264_e::x
|
||||
_CONVERT_(constqp);
|
||||
_CONVERT_(vbr_latency);
|
||||
_CONVERT_(vbr_peak);
|
||||
_CONVERT_(cbr);
|
||||
#undef _CONVERT_
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<int> rc_hevc_from_view(const std::string_view &rc) {
|
||||
#define _CONVERT_(x) \
|
||||
if(rc == #x##sv) return (int)rc_hevc_e::x
|
||||
_CONVERT_(constqp);
|
||||
_CONVERT_(vbr_latency);
|
||||
_CONVERT_(vbr_peak);
|
||||
@@ -141,7 +163,6 @@ int coder_from_view(const std::string_view &coder) {
|
||||
} // namespace amd
|
||||
|
||||
video_t video {
|
||||
0, // crf
|
||||
28, // qp
|
||||
|
||||
0, // hevc_mode
|
||||
@@ -160,6 +181,7 @@ video_t video {
|
||||
{
|
||||
amd::balanced,
|
||||
std::nullopt,
|
||||
std::nullopt,
|
||||
-1 }, // amd
|
||||
|
||||
{}, // encoder
|
||||
@@ -170,16 +192,18 @@ video_t video {
|
||||
audio_t audio {};
|
||||
|
||||
stream_t stream {
|
||||
2s, // ping_timeout
|
||||
10s, // ping_timeout
|
||||
|
||||
APPS_JSON_PATH,
|
||||
|
||||
10, // fecPercentage
|
||||
20, // fecPercentage
|
||||
1 // channels
|
||||
};
|
||||
|
||||
nvhttp_t nvhttp {
|
||||
"lan", // origin_pin
|
||||
"pc", // origin_pin
|
||||
"lan", // origin web manager
|
||||
|
||||
PRIVATE_KEY_FILE,
|
||||
CERTIFICATE_FILE,
|
||||
|
||||
@@ -203,14 +227,31 @@ nvhttp_t nvhttp {
|
||||
};
|
||||
|
||||
input_t input {
|
||||
2s, // back_button_timeout
|
||||
500ms, // key_repeat_delay
|
||||
std::chrono::duration<double> { 1 / 24.9 } // key_repeat_period
|
||||
{
|
||||
{ 0x10, 0xA0 },
|
||||
{ 0x11, 0xA2 },
|
||||
{ 0x12, 0xA4 },
|
||||
},
|
||||
2s, // back_button_timeout
|
||||
500ms, // key_repeat_delay
|
||||
std::chrono::duration<double> { 1 / 24.9 }, // key_repeat_period
|
||||
|
||||
{
|
||||
platf::supported_gamepads().front().data(),
|
||||
platf::supported_gamepads().front().size(),
|
||||
}, // Default gamepad
|
||||
};
|
||||
|
||||
sunshine_t sunshine {
|
||||
2, // min_log_level
|
||||
0 // flags
|
||||
2, // min_log_level
|
||||
0, // flags
|
||||
{}, // User file
|
||||
{}, // Username
|
||||
{}, // Password
|
||||
{}, // Password Salt
|
||||
SUNSHINE_CONFIG_DIR "/sunshine.conf", // config file
|
||||
{}, // cmd args
|
||||
47989,
|
||||
};
|
||||
|
||||
bool endline(char ch) {
|
||||
@@ -293,7 +334,7 @@ parse_option(std::string_view::const_iterator begin, std::string_view::const_ite
|
||||
std::make_pair(to_string(begin, end_name), to_string(begin_val, endl)));
|
||||
}
|
||||
|
||||
std::unordered_map<std::string, std::string> parse_config(std::string_view file_content) {
|
||||
std::unordered_map<std::string, std::string> parse_config(const std::string_view &file_content) {
|
||||
std::unordered_map<std::string, std::string> vars;
|
||||
|
||||
auto pos = std::begin(file_content);
|
||||
@@ -341,6 +382,38 @@ void string_restricted_f(std::unordered_map<std::string, std::string> &vars, con
|
||||
}
|
||||
}
|
||||
|
||||
void path_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, fs::path &input) {
|
||||
// appdata needs to be retrieved once only
|
||||
static auto appdata = platf::appdata();
|
||||
|
||||
std::string temp;
|
||||
string_f(vars, name, temp);
|
||||
|
||||
if(!temp.empty()) {
|
||||
input = temp;
|
||||
}
|
||||
|
||||
if(input.is_relative()) {
|
||||
input = appdata / input;
|
||||
}
|
||||
|
||||
auto dir = input;
|
||||
dir.remove_filename();
|
||||
|
||||
// Ensure the directories exists
|
||||
if(!fs::exists(dir)) {
|
||||
fs::create_directories(dir);
|
||||
}
|
||||
}
|
||||
|
||||
void path_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, std::string &input) {
|
||||
fs::path temp = input;
|
||||
|
||||
path_f(vars, name, temp);
|
||||
|
||||
input = temp.string();
|
||||
}
|
||||
|
||||
void int_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, int &input) {
|
||||
auto it = vars.find(name);
|
||||
|
||||
@@ -348,8 +421,20 @@ void int_f(std::unordered_map<std::string, std::string> &vars, const std::string
|
||||
return;
|
||||
}
|
||||
|
||||
auto &val = it->second;
|
||||
input = util::from_chars(&val[0], &val[0] + val.size());
|
||||
std::string_view val = it->second;
|
||||
|
||||
// If value is something like: "756" instead of 756
|
||||
if(val.size() >= 2 && val[0] == '"') {
|
||||
val = val.substr(1, val.size() - 2);
|
||||
}
|
||||
|
||||
// If that integer is in hexadecimal
|
||||
if(val.size() >= 2 && val.substr(0, 2) == "0x"sv) {
|
||||
input = util::from_hex<int>(val.substr(2));
|
||||
}
|
||||
else {
|
||||
input = util::from_view(val);
|
||||
}
|
||||
|
||||
vars.erase(it);
|
||||
}
|
||||
@@ -361,8 +446,20 @@ void int_f(std::unordered_map<std::string, std::string> &vars, const std::string
|
||||
return;
|
||||
}
|
||||
|
||||
auto &val = it->second;
|
||||
input = util::from_chars(&val[0], &val[0] + val.size());
|
||||
std::string_view val = it->second;
|
||||
|
||||
// If value is something like: "756" instead of 756
|
||||
if(val.size() >= 2 && val[0] == '"') {
|
||||
val = val.substr(1, val.size() - 2);
|
||||
}
|
||||
|
||||
// If that integer is in hexadecimal
|
||||
if(val.size() >= 2 && val.substr(0, 2) == "0x"sv) {
|
||||
input = util::from_hex<int>(val.substr(2));
|
||||
}
|
||||
else {
|
||||
input = util::from_view(val);
|
||||
}
|
||||
|
||||
vars.erase(it);
|
||||
}
|
||||
@@ -402,9 +499,12 @@ bool to_bool(std::string &boolean) {
|
||||
return boolean == "true"sv ||
|
||||
boolean == "yes"sv ||
|
||||
boolean == "enable"sv ||
|
||||
boolean == "enabled"sv ||
|
||||
boolean == "on"sv ||
|
||||
(std::find(std::begin(boolean), std::end(boolean), '1') != std::end(boolean));
|
||||
}
|
||||
void bool_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, int &input) {
|
||||
|
||||
void bool_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, bool &input) {
|
||||
std::string tmp;
|
||||
string_f(vars, name, tmp);
|
||||
|
||||
@@ -412,7 +512,7 @@ void bool_f(std::unordered_map<std::string, std::string> &vars, const std::strin
|
||||
return;
|
||||
}
|
||||
|
||||
input = to_bool(tmp) ? 1 : 0;
|
||||
input = to_bool(tmp);
|
||||
}
|
||||
|
||||
void double_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, double &input) {
|
||||
@@ -491,21 +591,43 @@ void list_int_f(std::unordered_map<std::string, std::string> &vars, const std::s
|
||||
list_string_f(vars, name, list);
|
||||
|
||||
for(auto &el : list) {
|
||||
input.emplace_back(util::from_view(el));
|
||||
std::string_view val = el;
|
||||
|
||||
// If value is something like: "756" instead of 756
|
||||
if(val.size() >= 2 && val[0] == '"') {
|
||||
val = val.substr(1, val.size() - 2);
|
||||
}
|
||||
|
||||
int tmp;
|
||||
|
||||
// If the integer is a hexadecimal
|
||||
if(val.size() >= 2 && val.substr(0, 2) == "0x"sv) {
|
||||
tmp = util::from_hex<int>(val.substr(2));
|
||||
}
|
||||
else {
|
||||
tmp = util::from_view(val);
|
||||
}
|
||||
input.emplace_back(tmp);
|
||||
}
|
||||
}
|
||||
|
||||
void print_help(const char *name) {
|
||||
std::cout
|
||||
<< "Usage: "sv << name << " [options] [/path/to/configuration_file]"sv << std::endl
|
||||
<< " Any configurable option can be overwritten with: \"name=value\""sv << std::endl
|
||||
<< std::endl
|
||||
<< " --help | print help"sv << std::endl
|
||||
<< std::endl
|
||||
<< " flags"sv << std::endl
|
||||
<< " -0 | Read PIN from stdin"sv << std::endl
|
||||
<< " -1 | Do not load previously saved state and do retain any state after shutdown"sv << std::endl
|
||||
<< " | Effectively starting as if for the first time without overwriting any pairings with your devices"sv;
|
||||
void map_int_int_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, std::unordered_map<int, int> &input) {
|
||||
std::vector<int> list;
|
||||
list_int_f(vars, name, list);
|
||||
|
||||
// The list needs to be a multiple of 2
|
||||
if(list.size() % 2) {
|
||||
std::cout << "Warning: expected "sv << name << " to have a multiple of two elements --> not "sv << list.size() << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
int x = 0;
|
||||
while(x < list.size()) {
|
||||
auto key = list[x++];
|
||||
auto val = list[x++];
|
||||
|
||||
input.emplace(key, val);
|
||||
}
|
||||
}
|
||||
|
||||
int apply_flags(const char *line) {
|
||||
@@ -518,8 +640,11 @@ int apply_flags(const char *line) {
|
||||
case '1':
|
||||
config::sunshine.flags[config::flag::FRESH_STATE].flip();
|
||||
break;
|
||||
case '2':
|
||||
config::sunshine.flags[config::flag::FORCE_VIDEO_HEADER_REPLACE].flip();
|
||||
break;
|
||||
case 'p':
|
||||
config::sunshine.flags[config::flag::CONST_PIN].flip();
|
||||
config::sunshine.flags[config::flag::UPNP].flip();
|
||||
break;
|
||||
default:
|
||||
std::cout << "Warning: Unrecognized flag: ["sv << *line << ']' << std::endl;
|
||||
@@ -533,11 +658,14 @@ int apply_flags(const char *line) {
|
||||
}
|
||||
|
||||
void apply_config(std::unordered_map<std::string, std::string> &&vars) {
|
||||
if(!fs::exists(stream.file_apps.c_str())) {
|
||||
fs::copy_file(SUNSHINE_DEFAULT_DIR "/" APPS_JSON, stream.file_apps);
|
||||
}
|
||||
|
||||
for(auto &[name, val] : vars) {
|
||||
std::cout << "["sv << name << "] -- ["sv << val << ']' << std::endl;
|
||||
}
|
||||
|
||||
int_f(vars, "crf", video.crf);
|
||||
int_f(vars, "qp", video.qp);
|
||||
int_f(vars, "min_threads", video.min_threads);
|
||||
int_between_f(vars, "hevc_mode", video.hevc_mode, { 0, 3 });
|
||||
@@ -548,17 +676,29 @@ void apply_config(std::unordered_map<std::string, std::string> &&vars) {
|
||||
int_f(vars, "nv_coder", video.nv.coder, nv::coder_from_view);
|
||||
|
||||
int_f(vars, "amd_quality", video.amd.quality, amd::quality_from_view);
|
||||
int_f(vars, "amd_rc", video.amd.rc, amd::rc_from_view);
|
||||
|
||||
std::string rc;
|
||||
string_f(vars, "amd_rc", rc);
|
||||
int_f(vars, "amd_coder", video.amd.coder, amd::coder_from_view);
|
||||
if(!rc.empty()) {
|
||||
video.amd.rc_h264 = amd::rc_h264_from_view(rc);
|
||||
video.amd.rc_hevc = amd::rc_hevc_from_view(rc);
|
||||
}
|
||||
|
||||
string_f(vars, "encoder", video.encoder);
|
||||
string_f(vars, "adapter_name", video.adapter_name);
|
||||
string_f(vars, "output_name", video.output_name);
|
||||
|
||||
string_f(vars, "pkey", nvhttp.pkey);
|
||||
string_f(vars, "cert", nvhttp.cert);
|
||||
path_f(vars, "pkey", nvhttp.pkey);
|
||||
path_f(vars, "cert", nvhttp.cert);
|
||||
string_f(vars, "sunshine_name", nvhttp.sunshine_name);
|
||||
string_f(vars, "file_state", nvhttp.file_state);
|
||||
|
||||
path_f(vars, "file_state", nvhttp.file_state);
|
||||
|
||||
// Must be run after "file_state"
|
||||
config::sunshine.credentials_file = config::nvhttp.file_state;
|
||||
path_f(vars, "credentials_file", config::sunshine.credentials_file);
|
||||
|
||||
string_f(vars, "external_ip", nvhttp.external_ip);
|
||||
list_string_f(vars, "resolutions"s, nvhttp.resolutions);
|
||||
list_int_f(vars, "fps"s, nvhttp.fps);
|
||||
@@ -567,6 +707,7 @@ void apply_config(std::unordered_map<std::string, std::string> &&vars) {
|
||||
string_f(vars, "virtual_sink", audio.virtual_sink);
|
||||
|
||||
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 });
|
||||
|
||||
int to = -1;
|
||||
int_between_f(vars, "ping_timeout", to, { -1, std::numeric_limits<int>::max() });
|
||||
@@ -576,8 +717,19 @@ void apply_config(std::unordered_map<std::string, std::string> &&vars) {
|
||||
|
||||
int_between_f(vars, "channels", stream.channels, { 1, std::numeric_limits<int>::max() });
|
||||
|
||||
string_f(vars, "file_apps", stream.file_apps);
|
||||
int_between_f(vars, "fec_percentage", stream.fec_percentage, { 1, 100 });
|
||||
path_f(vars, "file_apps", stream.file_apps);
|
||||
int_between_f(vars, "fec_percentage", stream.fec_percentage, { 1, 255 });
|
||||
|
||||
map_int_int_f(vars, "keybindings"s, input.keybindings);
|
||||
|
||||
// This config option will only be used by the UI
|
||||
// When editing in the config file itself, use "keybindings"
|
||||
bool map_rightalt_to_win = false;
|
||||
bool_f(vars, "key_rightalt_to_key_win", map_rightalt_to_win);
|
||||
|
||||
if(map_rightalt_to_win) {
|
||||
input.keybindings.emplace(0xA5, 0x5B);
|
||||
}
|
||||
|
||||
to = std::numeric_limits<int>::min();
|
||||
int_f(vars, "back_button_timeout", to);
|
||||
@@ -599,8 +751,21 @@ void apply_config(std::unordered_map<std::string, std::string> &&vars) {
|
||||
input.key_repeat_delay = std::chrono::milliseconds { to };
|
||||
}
|
||||
|
||||
string_restricted_f(vars, "gamepad"s, input.gamepad, platf::supported_gamepads());
|
||||
|
||||
int port = sunshine.port;
|
||||
int_f(vars, "port"s, port);
|
||||
sunshine.port = (std::uint16_t)port;
|
||||
|
||||
bool upnp = false;
|
||||
bool_f(vars, "upnp"s, upnp);
|
||||
|
||||
if(upnp) {
|
||||
config::sunshine.flags[config::flag::UPNP].flip();
|
||||
}
|
||||
|
||||
std::string log_level_string;
|
||||
string_restricted_f(vars, "min_log_level", log_level_string, { "verbose"sv, "debug"sv, "info"sv, "warning"sv, "error"sv, "fatal"sv, "none"sv });
|
||||
string_f(vars, "min_log_level", log_level_string);
|
||||
|
||||
if(!log_level_string.empty()) {
|
||||
if(log_level_string == "verbose"sv) {
|
||||
@@ -624,6 +789,13 @@ void apply_config(std::unordered_map<std::string, std::string> &&vars) {
|
||||
else if(log_level_string == "none"sv) {
|
||||
sunshine.min_log_level = 6;
|
||||
}
|
||||
else {
|
||||
// accept digit directly
|
||||
auto val = log_level_string[0];
|
||||
if(val >= '0' && val < '7') {
|
||||
sunshine.min_log_level = val - '0';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto it = vars.find("flags"s);
|
||||
@@ -641,11 +813,9 @@ void apply_config(std::unordered_map<std::string, std::string> &&vars) {
|
||||
}
|
||||
|
||||
int parse(int argc, char *argv[]) {
|
||||
const char *config_file = SUNSHINE_ASSETS_DIR "/sunshine.conf";
|
||||
|
||||
std::unordered_map<std::string, std::string> cmd_vars;
|
||||
|
||||
for(auto x = argc - 1; x > 0; --x) {
|
||||
for(auto x = 1; x < argc; ++x) {
|
||||
auto line = argv[x];
|
||||
|
||||
if(line == "--help"sv) {
|
||||
@@ -653,6 +823,13 @@ int parse(int argc, char *argv[]) {
|
||||
return 1;
|
||||
}
|
||||
else if(*line == '-') {
|
||||
if(*(line + 1) == '-') {
|
||||
sunshine.cmd.name = line + 2;
|
||||
sunshine.cmd.argc = argc - x - 1;
|
||||
sunshine.cmd.argv = argv + x + 1;
|
||||
|
||||
break;
|
||||
}
|
||||
if(apply_flags(line + 1)) {
|
||||
print_help(*argv);
|
||||
return -1;
|
||||
@@ -663,7 +840,7 @@ int parse(int argc, char *argv[]) {
|
||||
|
||||
auto pos = std::find(line, line_end, '=');
|
||||
if(pos == line_end) {
|
||||
config_file = line;
|
||||
sunshine.config_file = line;
|
||||
}
|
||||
else {
|
||||
TUPLE_EL(var, 1, parse_option(line, line_end));
|
||||
@@ -672,23 +849,23 @@ int parse(int argc, char *argv[]) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
TUPLE_EL_REF(name, 0, *var);
|
||||
|
||||
auto it = cmd_vars.find(name);
|
||||
if(it != std::end(cmd_vars)) {
|
||||
cmd_vars.erase(it);
|
||||
}
|
||||
|
||||
cmd_vars.emplace(std::move(*var));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::ifstream in { config_file };
|
||||
|
||||
if(!in.is_open()) {
|
||||
std::cout << "Error: Couldn't open "sv << config_file << std::endl;
|
||||
|
||||
return -1;
|
||||
if(!fs::exists(sunshine.config_file)) {
|
||||
fs::copy_file(SUNSHINE_DEFAULT_DIR "/sunshine.conf", sunshine.config_file);
|
||||
}
|
||||
|
||||
auto vars = parse_config(std::string {
|
||||
// Quick and dirty
|
||||
std::istreambuf_iterator<char>(in),
|
||||
std::istreambuf_iterator<char>() });
|
||||
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));
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
#include <chrono>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace config {
|
||||
struct video_t {
|
||||
// ffmpeg params
|
||||
int crf; // higher == more compression and less quality
|
||||
int qp; // higher == more compression and less quality, ignored if crf != 0
|
||||
int qp; // higher == more compression and less quality
|
||||
|
||||
int hevc_mode;
|
||||
|
||||
@@ -29,7 +29,8 @@ struct video_t {
|
||||
|
||||
struct {
|
||||
std::optional<int> quality;
|
||||
std::optional<int> rc;
|
||||
std::optional<int> rc_h264;
|
||||
std::optional<int> rc_hevc;
|
||||
int coder;
|
||||
} amd;
|
||||
|
||||
@@ -58,6 +59,7 @@ struct nvhttp_t {
|
||||
// Could be any of the following values:
|
||||
// pc|lan|wan
|
||||
std::string origin_pin_allowed;
|
||||
std::string origin_web_ui_allowed;
|
||||
|
||||
std::string pkey; // must be 2048 bits
|
||||
std::string cert; // must be signed with a key of 2048 bits
|
||||
@@ -72,24 +74,44 @@ struct nvhttp_t {
|
||||
};
|
||||
|
||||
struct input_t {
|
||||
std::unordered_map<int, int> keybindings;
|
||||
|
||||
std::chrono::milliseconds back_button_timeout;
|
||||
std::chrono::milliseconds key_repeat_delay;
|
||||
std::chrono::duration<double> key_repeat_period;
|
||||
|
||||
std::string gamepad;
|
||||
};
|
||||
|
||||
namespace flag {
|
||||
enum flag_e : std::size_t {
|
||||
PIN_STDIN = 0, // Read PIN from stdin instead of http
|
||||
FRESH_STATE, // Do not load or save state
|
||||
CONST_PIN = 4, // Use "universal" pin
|
||||
PIN_STDIN = 0, // Read PIN from stdin instead of http
|
||||
FRESH_STATE, // Do not load or save state
|
||||
FORCE_VIDEO_HEADER_REPLACE, // force replacing headers inside video data
|
||||
UPNP, // Try Universal Plug 'n Play
|
||||
CONST_PIN, // Use "universal" pin
|
||||
FLAG_SIZE
|
||||
};
|
||||
}
|
||||
|
||||
struct sunshine_t {
|
||||
int min_log_level;
|
||||
|
||||
std::bitset<flag::FLAG_SIZE> flags;
|
||||
std::string credentials_file;
|
||||
|
||||
std::string username;
|
||||
std::string password;
|
||||
std::string salt;
|
||||
|
||||
std::string config_file;
|
||||
|
||||
struct cmd_t {
|
||||
std::string name;
|
||||
int argc;
|
||||
char **argv;
|
||||
} cmd;
|
||||
|
||||
std::uint16_t port;
|
||||
};
|
||||
|
||||
extern video_t video;
|
||||
@@ -100,6 +122,6 @@ extern input_t input;
|
||||
extern sunshine_t sunshine;
|
||||
|
||||
int parse(int argc, char *argv[]);
|
||||
std::unordered_map<std::string, std::string> parse_config(const std::string_view &file_content);
|
||||
} // namespace config
|
||||
|
||||
#endif
|
||||
|
||||
616
sunshine/confighttp.cpp
Normal file
616
sunshine/confighttp.cpp
Normal file
@@ -0,0 +1,616 @@
|
||||
//
|
||||
// Created by TheElixZammuto on 2021-05-09.
|
||||
// TODO: Authentication, better handling of routes common to nvhttp, cleanup
|
||||
|
||||
#define BOOST_BIND_GLOBAL_PLACEHOLDERS
|
||||
|
||||
#include "process.h"
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
#include <boost/property_tree/json_parser.hpp>
|
||||
#include <boost/property_tree/ptree.hpp>
|
||||
#include <boost/property_tree/xml_parser.hpp>
|
||||
|
||||
#include <boost/asio/ssl/context.hpp>
|
||||
|
||||
#include <Simple-Web-Server/crypto.hpp>
|
||||
#include <Simple-Web-Server/server_https.hpp>
|
||||
#include <boost/asio/ssl/context_base.hpp>
|
||||
|
||||
#include "config.h"
|
||||
#include "confighttp.h"
|
||||
#include "crypto.h"
|
||||
#include "httpcommon.h"
|
||||
#include "main.h"
|
||||
#include "network.h"
|
||||
#include "nvhttp.h"
|
||||
#include "platform/common.h"
|
||||
#include "rtsp.h"
|
||||
#include "utility.h"
|
||||
#include "uuid.h"
|
||||
|
||||
using namespace std::literals;
|
||||
|
||||
namespace confighttp {
|
||||
namespace fs = std::filesystem;
|
||||
namespace pt = boost::property_tree;
|
||||
|
||||
using https_server_t = SimpleWeb::Server<SimpleWeb::HTTPS>;
|
||||
|
||||
using args_t = SimpleWeb::CaseInsensitiveMultimap;
|
||||
using resp_https_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTPS>::Response>;
|
||||
using req_https_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTPS>::Request>;
|
||||
|
||||
enum class op_e {
|
||||
ADD,
|
||||
REMOVE
|
||||
};
|
||||
|
||||
void print_req(const req_https_t &request) {
|
||||
BOOST_LOG(debug) << "METHOD :: "sv << request->method;
|
||||
BOOST_LOG(debug) << "DESTINATION :: "sv << request->path;
|
||||
|
||||
for(auto &[name, val] : request->header) {
|
||||
BOOST_LOG(debug) << name << " -- " << val;
|
||||
}
|
||||
|
||||
BOOST_LOG(debug) << " [--] "sv;
|
||||
|
||||
for(auto &[name, val] : request->parse_query_string()) {
|
||||
BOOST_LOG(debug) << name << " -- " << val;
|
||||
}
|
||||
|
||||
BOOST_LOG(debug) << " [--] "sv;
|
||||
}
|
||||
|
||||
void send_unauthorized(resp_https_t response, req_https_t request) {
|
||||
auto address = request->remote_endpoint_address();
|
||||
BOOST_LOG(info) << "Web UI: ["sv << address << "] -- not authorized"sv;
|
||||
const SimpleWeb::CaseInsensitiveMultimap headers {
|
||||
{ "WWW-Authenticate", R"(Basic realm="Sunshine Gamestream Host", charset="UTF-8")" }
|
||||
};
|
||||
response->write(SimpleWeb::StatusCode::client_error_unauthorized, headers);
|
||||
}
|
||||
|
||||
void send_redirect(resp_https_t response, req_https_t request, const char *path) {
|
||||
auto address = request->remote_endpoint_address();
|
||||
BOOST_LOG(info) << "Web UI: ["sv << address << "] -- not authorized"sv;
|
||||
const SimpleWeb::CaseInsensitiveMultimap headers {
|
||||
{ "Location", path }
|
||||
};
|
||||
response->write(SimpleWeb::StatusCode::redirection_temporary_redirect, headers);
|
||||
}
|
||||
|
||||
bool authenticate(resp_https_t response, req_https_t request) {
|
||||
auto address = request->remote_endpoint_address();
|
||||
auto ip_type = net::from_address(address);
|
||||
|
||||
if(ip_type > http::origin_web_ui_allowed) {
|
||||
BOOST_LOG(info) << "Web UI: ["sv << address << "] -- denied"sv;
|
||||
response->write(SimpleWeb::StatusCode::client_error_forbidden);
|
||||
return false;
|
||||
}
|
||||
|
||||
//If credentials are shown, redirect the user to a /welcome page
|
||||
if(config::sunshine.username.empty()){
|
||||
send_redirect(response,request,"/welcome");
|
||||
return false;
|
||||
}
|
||||
|
||||
auto fg = util::fail_guard([&]() {
|
||||
send_unauthorized(response, request);
|
||||
});
|
||||
|
||||
auto auth = request->header.find("authorization");
|
||||
if(auth == request->header.end()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto &rawAuth = auth->second;
|
||||
auto authData = SimpleWeb::Crypto::Base64::decode(rawAuth.substr("Basic "sv.length()));
|
||||
|
||||
int index = authData.find(':');
|
||||
if(index >= authData.size() - 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto username = authData.substr(0, index);
|
||||
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) {
|
||||
return false;
|
||||
}
|
||||
|
||||
fg.disable();
|
||||
return true;
|
||||
}
|
||||
|
||||
void not_found(resp_https_t response, req_https_t request) {
|
||||
pt::ptree tree;
|
||||
tree.put("root.<xmlattr>.status_code", 404);
|
||||
|
||||
std::ostringstream data;
|
||||
|
||||
pt::write_xml(data, tree);
|
||||
response->write(data.str());
|
||||
|
||||
*response << "HTTP/1.1 404 NOT FOUND\r\n"
|
||||
<< data.str();
|
||||
}
|
||||
|
||||
void getIndexPage(resp_https_t response, req_https_t request) {
|
||||
if(!authenticate(response, request)) return;
|
||||
|
||||
print_req(request);
|
||||
|
||||
std::string header = read_file(WEB_DIR "header.html");
|
||||
std::string content = read_file(WEB_DIR "index.html");
|
||||
response->write(header + content);
|
||||
}
|
||||
|
||||
void getPinPage(resp_https_t response, req_https_t request) {
|
||||
if(!authenticate(response, request)) return;
|
||||
|
||||
print_req(request);
|
||||
|
||||
std::string header = read_file(WEB_DIR "header.html");
|
||||
std::string content = read_file(WEB_DIR "pin.html");
|
||||
response->write(header + content);
|
||||
}
|
||||
|
||||
void getAppsPage(resp_https_t response, req_https_t request) {
|
||||
if(!authenticate(response, request)) return;
|
||||
|
||||
print_req(request);
|
||||
|
||||
std::string header = read_file(WEB_DIR "header.html");
|
||||
std::string content = read_file(WEB_DIR "apps.html");
|
||||
response->write(header + content);
|
||||
}
|
||||
|
||||
void getClientsPage(resp_https_t response, req_https_t request) {
|
||||
if(!authenticate(response, request)) return;
|
||||
|
||||
print_req(request);
|
||||
|
||||
std::string header = read_file(WEB_DIR "header.html");
|
||||
std::string content = read_file(WEB_DIR "clients.html");
|
||||
response->write(header + content);
|
||||
}
|
||||
|
||||
void getConfigPage(resp_https_t response, req_https_t request) {
|
||||
if(!authenticate(response, request)) return;
|
||||
|
||||
print_req(request);
|
||||
|
||||
std::string header = read_file(WEB_DIR "header.html");
|
||||
std::string content = read_file(WEB_DIR "config.html");
|
||||
response->write(header + content);
|
||||
}
|
||||
|
||||
void getPasswordPage(resp_https_t response, req_https_t request) {
|
||||
if(!authenticate(response, request)) return;
|
||||
|
||||
print_req(request);
|
||||
|
||||
std::string header = read_file(WEB_DIR "header.html");
|
||||
std::string content = read_file(WEB_DIR "password.html");
|
||||
response->write(header + content);
|
||||
}
|
||||
|
||||
void getWelcomePage(resp_https_t response, req_https_t request) {
|
||||
print_req(request);
|
||||
if(!config::sunshine.username.empty()){
|
||||
send_redirect(response,request,"/");
|
||||
return;
|
||||
}
|
||||
std::string header = read_file(WEB_DIR "header-no-nav.html");
|
||||
std::string content = read_file(WEB_DIR "welcome.html");
|
||||
response->write(header + content);
|
||||
}
|
||||
|
||||
void getTroubleshootingPage(resp_https_t response, req_https_t request) {
|
||||
if(!authenticate(response, request)) return;
|
||||
|
||||
print_req(request);
|
||||
|
||||
std::string header = read_file(WEB_DIR "header.html");
|
||||
std::string content = read_file(WEB_DIR "troubleshooting.html");
|
||||
response->write(header + content);
|
||||
}
|
||||
|
||||
void getBootstrapCss(resp_https_t response, req_https_t request) {
|
||||
print_req(request);
|
||||
|
||||
std::string content = read_file(WEB_DIR "third_party/bootstrap.min.css");
|
||||
response->write(content);
|
||||
}
|
||||
|
||||
void getBootstrapJs(resp_https_t response, req_https_t request) {
|
||||
print_req(request);
|
||||
|
||||
std::string content = read_file(WEB_DIR "third_party/bootstrap.bundle.min.js");
|
||||
response->write(content);
|
||||
}
|
||||
|
||||
void getVueJs(resp_https_t response, req_https_t request) {
|
||||
print_req(request);
|
||||
|
||||
std::string content = read_file(WEB_DIR "third_party/vue.js");
|
||||
response->write(content);
|
||||
}
|
||||
|
||||
void getApps(resp_https_t response, req_https_t request) {
|
||||
if(!authenticate(response, request)) return;
|
||||
|
||||
print_req(request);
|
||||
|
||||
std::string content = read_file(config::stream.file_apps.c_str());
|
||||
response->write(content);
|
||||
}
|
||||
|
||||
void saveApp(resp_https_t response, req_https_t request) {
|
||||
if(!authenticate(response, request)) return;
|
||||
|
||||
print_req(request);
|
||||
|
||||
std::stringstream ss;
|
||||
ss << request->content.rdbuf();
|
||||
|
||||
pt::ptree outputTree;
|
||||
auto g = util::fail_guard([&]() {
|
||||
std::ostringstream data;
|
||||
|
||||
pt::write_json(data, outputTree);
|
||||
response->write(data.str());
|
||||
});
|
||||
|
||||
pt::ptree inputTree, fileTree;
|
||||
|
||||
BOOST_LOG(fatal) << config::stream.file_apps;
|
||||
try {
|
||||
//TODO: Input Validation
|
||||
pt::read_json(ss, inputTree);
|
||||
pt::read_json(config::stream.file_apps, fileTree);
|
||||
|
||||
if(inputTree.get_child("prep-cmd").empty()) {
|
||||
inputTree.erase("prep-cmd");
|
||||
}
|
||||
|
||||
if(inputTree.get_child("detached").empty()) {
|
||||
inputTree.erase("detached");
|
||||
}
|
||||
|
||||
auto &apps_node = fileTree.get_child("apps"s);
|
||||
int index = inputTree.get<int>("index");
|
||||
|
||||
inputTree.erase("index");
|
||||
|
||||
if(index == -1) {
|
||||
apps_node.push_back(std::make_pair("", inputTree));
|
||||
}
|
||||
else {
|
||||
//Unfortuantely Boost PT does not allow to directly edit the array, copy should do the trick
|
||||
pt::ptree newApps;
|
||||
int i = 0;
|
||||
for(const auto &kv : apps_node) {
|
||||
if(i == index) {
|
||||
newApps.push_back(std::make_pair("", inputTree));
|
||||
}
|
||||
else {
|
||||
newApps.push_back(std::make_pair("", kv.second));
|
||||
}
|
||||
i++;
|
||||
}
|
||||
fileTree.erase("apps");
|
||||
fileTree.push_back(std::make_pair("apps", newApps));
|
||||
}
|
||||
pt::write_json(config::stream.file_apps, fileTree);
|
||||
}
|
||||
catch(std::exception &e) {
|
||||
BOOST_LOG(warning) << "SaveApp: "sv << e.what();
|
||||
|
||||
outputTree.put("status", "false");
|
||||
outputTree.put("error", "Invalid Input JSON");
|
||||
return;
|
||||
}
|
||||
|
||||
outputTree.put("status", "true");
|
||||
proc::refresh(config::stream.file_apps);
|
||||
}
|
||||
|
||||
void deleteApp(resp_https_t response, req_https_t request) {
|
||||
if(!authenticate(response, request)) return;
|
||||
|
||||
print_req(request);
|
||||
|
||||
pt::ptree outputTree;
|
||||
auto g = util::fail_guard([&]() {
|
||||
std::ostringstream data;
|
||||
|
||||
pt::write_json(data, outputTree);
|
||||
response->write(data.str());
|
||||
});
|
||||
pt::ptree fileTree;
|
||||
try {
|
||||
pt::read_json(config::stream.file_apps, fileTree);
|
||||
auto &apps_node = fileTree.get_child("apps"s);
|
||||
int index = stoi(request->path_match[1]);
|
||||
|
||||
if(index < 0) {
|
||||
outputTree.put("status", "false");
|
||||
outputTree.put("error", "Invalid Index");
|
||||
return;
|
||||
}
|
||||
else {
|
||||
//Unfortuantely Boost PT does not allow to directly edit the array, copy should do the trick
|
||||
pt::ptree newApps;
|
||||
int i = 0;
|
||||
for(const auto &kv : apps_node) {
|
||||
if(i++ != index) {
|
||||
newApps.push_back(std::make_pair("", kv.second));
|
||||
}
|
||||
}
|
||||
fileTree.erase("apps");
|
||||
fileTree.push_back(std::make_pair("apps", newApps));
|
||||
}
|
||||
pt::write_json(config::stream.file_apps, fileTree);
|
||||
}
|
||||
catch(std::exception &e) {
|
||||
BOOST_LOG(warning) << "DeleteApp: "sv << e.what();
|
||||
outputTree.put("status", "false");
|
||||
outputTree.put("error", "Invalid File JSON");
|
||||
return;
|
||||
}
|
||||
|
||||
outputTree.put("status", "true");
|
||||
proc::refresh(config::stream.file_apps);
|
||||
}
|
||||
|
||||
void getConfig(resp_https_t response, req_https_t request) {
|
||||
if(!authenticate(response, request)) return;
|
||||
|
||||
print_req(request);
|
||||
|
||||
pt::ptree outputTree;
|
||||
auto g = util::fail_guard([&]() {
|
||||
std::ostringstream data;
|
||||
|
||||
pt::write_json(data, outputTree);
|
||||
response->write(data.str());
|
||||
});
|
||||
|
||||
outputTree.put("status", "true");
|
||||
outputTree.put("platform", SUNSHINE_PLATFORM);
|
||||
|
||||
auto vars = config::parse_config(read_file(config::sunshine.config_file.c_str()));
|
||||
|
||||
for(auto &[name, value] : vars) {
|
||||
outputTree.put(std::move(name), std::move(value));
|
||||
}
|
||||
}
|
||||
|
||||
void saveConfig(resp_https_t response, req_https_t request) {
|
||||
if(!authenticate(response, request)) return;
|
||||
|
||||
print_req(request);
|
||||
|
||||
std::stringstream ss;
|
||||
std::stringstream configStream;
|
||||
ss << request->content.rdbuf();
|
||||
pt::ptree outputTree;
|
||||
auto g = util::fail_guard([&]() {
|
||||
std::ostringstream data;
|
||||
|
||||
pt::write_json(data, outputTree);
|
||||
response->write(data.str());
|
||||
});
|
||||
pt::ptree inputTree;
|
||||
try {
|
||||
//TODO: Input Validation
|
||||
pt::read_json(ss, inputTree);
|
||||
for(const auto &kv : inputTree) {
|
||||
std::string value = inputTree.get<std::string>(kv.first);
|
||||
if(value.length() == 0 || value.compare("null") == 0) continue;
|
||||
|
||||
configStream << kv.first << " = " << value << std::endl;
|
||||
}
|
||||
write_file(config::sunshine.config_file.c_str(), configStream.str());
|
||||
}
|
||||
catch(std::exception &e) {
|
||||
BOOST_LOG(warning) << "SaveConfig: "sv << e.what();
|
||||
outputTree.put("status", "false");
|
||||
outputTree.put("error", e.what());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void savePassword(resp_https_t response, req_https_t request) {
|
||||
if(!config::sunshine.username.empty() && !authenticate(response, request)) return;
|
||||
|
||||
print_req(request);
|
||||
|
||||
std::stringstream ss;
|
||||
std::stringstream configStream;
|
||||
ss << request->content.rdbuf();
|
||||
|
||||
pt::ptree inputTree, outputTree;
|
||||
|
||||
auto g = util::fail_guard([&]() {
|
||||
std::ostringstream data;
|
||||
pt::write_json(data, outputTree);
|
||||
response->write(data.str());
|
||||
});
|
||||
|
||||
try {
|
||||
//TODO: Input Validation
|
||||
pt::read_json(ss, inputTree);
|
||||
auto username = inputTree.count("currentUsername") > 0 ? inputTree.get<std::string>("currentUsername") : "";
|
||||
auto newUsername = inputTree.get<std::string>("newUsername");
|
||||
auto password = inputTree.count("currentPassword") > 0 ? inputTree.get<std::string>("currentPassword") : "";
|
||||
auto newPassword = inputTree.count("newPassword") > 0 ? inputTree.get<std::string>("newPassword") : "";
|
||||
auto confirmPassword = inputTree.count("confirmNewPassword") > 0 ? inputTree.get<std::string>("confirmNewPassword") : "";
|
||||
if(newUsername.length() == 0) newUsername = username;
|
||||
if(newUsername.length() == 0){
|
||||
outputTree.put("status", false);
|
||||
outputTree.put("error", "Invalid Username");
|
||||
} 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(newPassword.empty() || newPassword != confirmPassword) {
|
||||
outputTree.put("status", false);
|
||||
outputTree.put("error", "Password Mismatch");
|
||||
} else {
|
||||
http::save_user_creds(config::sunshine.credentials_file, newUsername, newPassword);
|
||||
http::reload_user_creds(config::sunshine.credentials_file);
|
||||
outputTree.put("status", true);
|
||||
}
|
||||
}
|
||||
else {
|
||||
outputTree.put("status", false);
|
||||
outputTree.put("error", "Invalid Current Credentials");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(std::exception &e) {
|
||||
BOOST_LOG(warning) << "SavePassword: "sv << e.what();
|
||||
outputTree.put("status", false);
|
||||
outputTree.put("error", e.what());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void savePin(resp_https_t response, req_https_t request) {
|
||||
if(!authenticate(response, request)) return;
|
||||
|
||||
print_req(request);
|
||||
|
||||
std::stringstream ss;
|
||||
ss << request->content.rdbuf();
|
||||
|
||||
pt::ptree inputTree, outputTree;
|
||||
|
||||
auto g = util::fail_guard([&]() {
|
||||
std::ostringstream data;
|
||||
pt::write_json(data, outputTree);
|
||||
response->write(data.str());
|
||||
});
|
||||
|
||||
try {
|
||||
//TODO: Input Validation
|
||||
pt::read_json(ss, inputTree);
|
||||
std::string pin = inputTree.get<std::string>("pin");
|
||||
outputTree.put("status", nvhttp::pin(pin));
|
||||
}
|
||||
catch(std::exception &e) {
|
||||
BOOST_LOG(warning) << "SavePin: "sv << e.what();
|
||||
outputTree.put("status", false);
|
||||
outputTree.put("error", e.what());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void unpairAll(resp_https_t response, req_https_t request){
|
||||
if(!authenticate(response, request)) return;
|
||||
|
||||
print_req(request);
|
||||
|
||||
pt::ptree outputTree;
|
||||
|
||||
auto g = util::fail_guard([&]() {
|
||||
std::ostringstream data;
|
||||
pt::write_json(data, outputTree);
|
||||
response->write(data.str());
|
||||
});
|
||||
nvhttp::erase_all_clients();
|
||||
outputTree.put("status", true);
|
||||
}
|
||||
|
||||
void closeApp(resp_https_t response, req_https_t request){
|
||||
if(!authenticate(response, request)) return;
|
||||
|
||||
print_req(request);
|
||||
|
||||
pt::ptree outputTree;
|
||||
|
||||
auto g = util::fail_guard([&]() {
|
||||
std::ostringstream data;
|
||||
pt::write_json(data, outputTree);
|
||||
response->write(data.str());
|
||||
});
|
||||
|
||||
proc::proc.terminate();
|
||||
outputTree.put("status", true);
|
||||
}
|
||||
|
||||
void start() {
|
||||
auto shutdown_event = mail::man->event<bool>(mail::shutdown);
|
||||
|
||||
auto port_https = map_port(PORT_HTTPS);
|
||||
|
||||
auto ctx = std::make_shared<boost::asio::ssl::context>(boost::asio::ssl::context::tls);
|
||||
ctx->use_certificate_chain_file(config::nvhttp.cert);
|
||||
ctx->use_private_key_file(config::nvhttp.pkey, boost::asio::ssl::context::pem);
|
||||
https_server_t server { ctx, 0 };
|
||||
server.default_resource = not_found;
|
||||
server.resource["^/$"]["GET"] = getIndexPage;
|
||||
server.resource["^/pin$"]["GET"] = getPinPage;
|
||||
server.resource["^/apps$"]["GET"] = getAppsPage;
|
||||
server.resource["^/clients$"]["GET"] = getClientsPage;
|
||||
server.resource["^/config$"]["GET"] = getConfigPage;
|
||||
server.resource["^/password$"]["GET"] = getPasswordPage;
|
||||
server.resource["^/welcome$"]["GET"] = getWelcomePage;
|
||||
server.resource["^/troubleshooting$"]["GET"] = getTroubleshootingPage;
|
||||
server.resource["^/api/pin"]["POST"] = savePin;
|
||||
server.resource["^/api/apps$"]["GET"] = getApps;
|
||||
server.resource["^/api/apps$"]["POST"] = saveApp;
|
||||
server.resource["^/api/config$"]["GET"] = getConfig;
|
||||
server.resource["^/api/config$"]["POST"] = saveConfig;
|
||||
server.resource["^/api/password$"]["POST"] = savePassword;
|
||||
server.resource["^/api/apps/([0-9]+)$"]["DELETE"] = deleteApp;
|
||||
server.resource["^/api/clients/unpair$"]["POST"] = unpairAll;
|
||||
server.resource["^/api/apps/close"]["POST"] = closeApp;
|
||||
server.resource["^/third_party/bootstrap.min.css$"]["GET"] = getBootstrapCss;
|
||||
server.resource["^/third_party/bootstrap.bundle.min.js$"]["GET"] = getBootstrapJs;
|
||||
server.resource["^/third_party/vue.js$"]["GET"] = getVueJs;
|
||||
server.config.reuse_address = true;
|
||||
server.config.address = "0.0.0.0"s;
|
||||
server.config.port = port_https;
|
||||
|
||||
try {
|
||||
server.bind();
|
||||
BOOST_LOG(info) << "Configuration UI available at [https://localhost:"sv << port_https << "]";
|
||||
}
|
||||
catch(boost::system::system_error &err) {
|
||||
BOOST_LOG(fatal) << "Couldn't bind http server to ports ["sv << port_https << "]: "sv << err.what();
|
||||
|
||||
shutdown_event->raise(true);
|
||||
return;
|
||||
}
|
||||
auto accept_and_run = [&](auto *server) {
|
||||
try {
|
||||
server->accept_and_run();
|
||||
}
|
||||
catch(boost::system::system_error &err) {
|
||||
// It's possible the exception gets thrown after calling server->stop() from a different thread
|
||||
if(shutdown_event->peek()) {
|
||||
return;
|
||||
}
|
||||
|
||||
BOOST_LOG(fatal) << "Couldn't start Configuration HTTPS server to port ["sv << port_https << "]: "sv << err.what();
|
||||
shutdown_event->raise(true);
|
||||
return;
|
||||
}
|
||||
};
|
||||
std::thread tcp { accept_and_run, &server };
|
||||
|
||||
// Wait for any event
|
||||
shutdown_event->view();
|
||||
|
||||
server.stop();
|
||||
|
||||
tcp.join();
|
||||
}
|
||||
} // namespace confighttp
|
||||
21
sunshine/confighttp.h
Normal file
21
sunshine/confighttp.h
Normal file
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// Created by loki on 6/3/19.
|
||||
//
|
||||
|
||||
#ifndef SUNSHINE_CONFIGHTTP_H
|
||||
#define SUNSHINE_CONFIGHTTP_H
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
#include "thread_safe.h"
|
||||
|
||||
#define WEB_DIR SUNSHINE_ASSETS_DIR "/web/"
|
||||
|
||||
|
||||
namespace confighttp {
|
||||
constexpr auto PORT_HTTPS = 1;
|
||||
void start();
|
||||
} // namespace confighttp
|
||||
|
||||
#endif //SUNSHINE_CONFIGHTTP_H
|
||||
@@ -4,8 +4,9 @@
|
||||
|
||||
#include "crypto.h"
|
||||
#include <openssl/pem.h>
|
||||
|
||||
namespace crypto {
|
||||
using big_num_t = util::safe_ptr<BIGNUM, BN_free>;
|
||||
using big_num_t = util::safe_ptr<BIGNUM, BN_free>;
|
||||
//using rsa_t = util::safe_ptr<RSA, RSA_free>;
|
||||
using asn1_string_t = util::safe_ptr<ASN1_STRING, ASN1_STRING_free>;
|
||||
|
||||
@@ -17,6 +18,26 @@ void cert_chain_t::add(x509_t &&cert) {
|
||||
_certs.emplace_back(std::make_pair(std::move(cert), std::move(x509_store)));
|
||||
}
|
||||
|
||||
static int openssl_verify_cb(int ok, X509_STORE_CTX *ctx) {
|
||||
int err_code = X509_STORE_CTX_get_error(ctx);
|
||||
|
||||
switch(err_code) {
|
||||
//FIXME: Checking for X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY is a temporary workaround to get mmonlight-embedded to work on the raspberry pi
|
||||
case X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY:
|
||||
return 1;
|
||||
|
||||
// Expired or not-yet-valid certificates are fine. Sometimes Moonlight is running on embedded devices
|
||||
// that don't have accurate clocks (or haven't yet synchronized by the time Moonlight first runs).
|
||||
// This behavior also matches what GeForce Experience does.
|
||||
case X509_V_ERR_CERT_NOT_YET_VALID:
|
||||
case X509_V_ERR_CERT_HAS_EXPIRED:
|
||||
return 1;
|
||||
|
||||
default:
|
||||
return ok;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 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
|
||||
@@ -31,8 +52,13 @@ const char *cert_chain_t::verify(x509_t::element_type *cert) {
|
||||
X509_STORE_CTX_cleanup(_cert_ctx.get());
|
||||
});
|
||||
|
||||
X509_STORE_CTX_init(_cert_ctx.get(), x509_store.get(), nullptr, nullptr);
|
||||
X509_STORE_CTX_set_cert(_cert_ctx.get(), cert);
|
||||
X509_STORE_CTX_init(_cert_ctx.get(), x509_store.get(), cert, nullptr);
|
||||
X509_STORE_CTX_set_verify_cb(_cert_ctx.get(), openssl_verify_cb);
|
||||
|
||||
// We don't care to validate the entire chain for the purposes of client auth.
|
||||
// Some versions of clients forked from Moonlight Embedded produce client certs
|
||||
// that OpenSSL doesn't detect as self-signed due to some X509v3 extensions.
|
||||
X509_STORE_CTX_set_flags(_cert_ctx.get(), X509_V_FLAG_PARTIAL_CHAIN);
|
||||
|
||||
auto err = X509_verify_cert(_cert_ctx.get());
|
||||
|
||||
@@ -42,10 +68,6 @@ const char *cert_chain_t::verify(x509_t::element_type *cert) {
|
||||
|
||||
err_code = X509_STORE_CTX_get_error(_cert_ctx.get());
|
||||
|
||||
//FIXME: Checking for X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY is a temporary workaround to get mmonlight-embedded to work on the raspberry pi
|
||||
if(err_code == X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY) {
|
||||
return nullptr;
|
||||
}
|
||||
if(err_code != X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT && err_code != X509_V_ERR_INVALID_CA) {
|
||||
return X509_verify_cert_error_string(err_code);
|
||||
}
|
||||
@@ -54,71 +76,91 @@ const char *cert_chain_t::verify(x509_t::element_type *cert) {
|
||||
return X509_verify_cert_error_string(err_code);
|
||||
}
|
||||
|
||||
cipher_t::cipher_t(const crypto::aes_t &key) : ctx { EVP_CIPHER_CTX_new() }, key { key }, padding { true } {}
|
||||
int cipher_t::decrypt(const std::string_view &cipher, std::vector<std::uint8_t> &plaintext) {
|
||||
int len;
|
||||
namespace cipher {
|
||||
|
||||
auto fg = util::fail_guard([this]() {
|
||||
EVP_CIPHER_CTX_reset(ctx.get());
|
||||
});
|
||||
static int init_decrypt_gcm(cipher_ctx_t &ctx, aes_t *key, aes_t *iv, bool padding) {
|
||||
ctx.reset(EVP_CIPHER_CTX_new());
|
||||
|
||||
// Gen 7 servers use 128-bit AES ECB
|
||||
if(EVP_DecryptInit_ex(ctx.get(), EVP_aes_128_ecb(), nullptr, key.data(), nullptr) != 1) {
|
||||
if(!ctx) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
EVP_CIPHER_CTX_set_padding(ctx.get(), padding);
|
||||
|
||||
plaintext.resize((cipher.size() + 15) / 16 * 16);
|
||||
auto size = (int)plaintext.size();
|
||||
// Encrypt into the caller's buffer, leaving room for the auth tag to be prepended
|
||||
if(EVP_DecryptUpdate(ctx.get(), plaintext.data(), &size, (const std::uint8_t *)cipher.data(), cipher.size()) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if(EVP_DecryptFinal_ex(ctx.get(), plaintext.data(), &len) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
plaintext.resize(len + size);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int cipher_t::decrypt_gcm(aes_t &iv, const std::string_view &tagged_cipher,
|
||||
std::vector<std::uint8_t> &plaintext) {
|
||||
auto cipher = tagged_cipher.substr(16);
|
||||
auto tag = tagged_cipher.substr(0, 16);
|
||||
|
||||
auto fg = util::fail_guard([this]() {
|
||||
EVP_CIPHER_CTX_reset(ctx.get());
|
||||
});
|
||||
|
||||
if(EVP_DecryptInit_ex(ctx.get(), EVP_aes_128_gcm(), nullptr, nullptr, nullptr) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if(EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_GCM_SET_IVLEN, iv.size(), nullptr) != 1) {
|
||||
if(EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_GCM_SET_IVLEN, iv->size(), nullptr) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if(EVP_DecryptInit_ex(ctx.get(), nullptr, nullptr, key.data(), iv.data()) != 1) {
|
||||
if(EVP_DecryptInit_ex(ctx.get(), nullptr, nullptr, key->data(), iv->data()) != 1) {
|
||||
return -1;
|
||||
}
|
||||
EVP_CIPHER_CTX_set_padding(ctx.get(), padding);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int init_encrypt_gcm(cipher_ctx_t &ctx, aes_t *key, aes_t *iv, bool padding) {
|
||||
ctx.reset(EVP_CIPHER_CTX_new());
|
||||
|
||||
// Gen 7 servers use 128-bit AES ECB
|
||||
if(EVP_EncryptInit_ex(ctx.get(), EVP_aes_128_gcm(), nullptr, nullptr, nullptr) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if(EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_GCM_SET_IVLEN, iv->size(), nullptr) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if(EVP_EncryptInit_ex(ctx.get(), nullptr, nullptr, key->data(), iv->data()) != 1) {
|
||||
return -1;
|
||||
}
|
||||
EVP_CIPHER_CTX_set_padding(ctx.get(), padding);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int init_encrypt_cbc(cipher_ctx_t &ctx, aes_t *key, aes_t *iv, bool padding) {
|
||||
ctx.reset(EVP_CIPHER_CTX_new());
|
||||
|
||||
// Gen 7 servers use 128-bit AES ECB
|
||||
if(EVP_EncryptInit_ex(ctx.get(), EVP_aes_128_cbc(), nullptr, key->data(), iv->data()) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
EVP_CIPHER_CTX_set_padding(ctx.get(), padding);
|
||||
plaintext.resize((cipher.size() + 15) / 16 * 16);
|
||||
|
||||
int size;
|
||||
if(EVP_DecryptUpdate(ctx.get(), plaintext.data(), &size, (const std::uint8_t *)cipher.data(), cipher.size()) != 1) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int gcm_t::decrypt(const std::string_view &tagged_cipher, std::vector<std::uint8_t> &plaintext, aes_t *iv) {
|
||||
if(!decrypt_ctx && init_decrypt_gcm(decrypt_ctx, &key, iv, padding)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if(EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_GCM_SET_TAG, tag.size(), const_cast<char *>(tag.data())) != 1) {
|
||||
// Calling with cipher == nullptr results in a parameter change
|
||||
// without requiring a reallocation of the internal cipher ctx.
|
||||
if(EVP_DecryptInit_ex(decrypt_ctx.get(), nullptr, nullptr, nullptr, iv->data()) != 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto cipher = tagged_cipher.substr(tag_size);
|
||||
auto tag = tagged_cipher.substr(0, tag_size);
|
||||
|
||||
plaintext.resize((cipher.size() + 15) / 16 * 16);
|
||||
|
||||
int size;
|
||||
if(EVP_DecryptUpdate(decrypt_ctx.get(), plaintext.data(), &size, (const std::uint8_t *)cipher.data(), cipher.size()) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if(EVP_CIPHER_CTX_ctrl(decrypt_ctx.get(), EVP_CTRL_GCM_SET_TAG, tag.size(), const_cast<char *>(tag.data())) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int len = size;
|
||||
if(EVP_DecryptFinal_ex(ctx.get(), plaintext.data() + size, &len) != 1) {
|
||||
if(EVP_DecryptFinal_ex(decrypt_ctx.get(), plaintext.data() + size, &len) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -126,28 +168,92 @@ int cipher_t::decrypt_gcm(aes_t &iv, const std::string_view &tagged_cipher,
|
||||
return 0;
|
||||
}
|
||||
|
||||
int cipher_t::encrypt(const std::string_view &plaintext, std::vector<std::uint8_t> &cipher) {
|
||||
int gcm_t::encrypt(const std::string_view &plaintext, std::uint8_t *tagged_cipher, aes_t *iv) {
|
||||
if(!encrypt_ctx && init_encrypt_gcm(encrypt_ctx, &key, iv, padding)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Calling with cipher == nullptr results in a parameter change
|
||||
// without requiring a reallocation of the internal cipher ctx.
|
||||
if(EVP_EncryptInit_ex(encrypt_ctx.get(), nullptr, nullptr, nullptr, iv->data()) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
auto tag = tagged_cipher;
|
||||
auto cipher = tag + tag_size;
|
||||
|
||||
int len;
|
||||
int size = round_to_pkcs7_padded(plaintext.size());
|
||||
|
||||
// Encrypt into the caller's buffer
|
||||
if(EVP_EncryptUpdate(encrypt_ctx.get(), cipher, &size, (const std::uint8_t *)plaintext.data(), plaintext.size()) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// GCM encryption won't ever fill ciphertext here but we have to call it anyway
|
||||
if(EVP_EncryptFinal_ex(encrypt_ctx.get(), cipher + size, &len) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if(EVP_CIPHER_CTX_ctrl(encrypt_ctx.get(), EVP_CTRL_GCM_GET_TAG, tag_size, tag) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return len + size;
|
||||
}
|
||||
|
||||
int ecb_t::decrypt(const std::string_view &cipher, std::vector<std::uint8_t> &plaintext) {
|
||||
int len;
|
||||
|
||||
auto fg = util::fail_guard([this]() {
|
||||
EVP_CIPHER_CTX_reset(ctx.get());
|
||||
EVP_CIPHER_CTX_reset(decrypt_ctx.get());
|
||||
});
|
||||
|
||||
// Gen 7 servers use 128-bit AES ECB
|
||||
if(EVP_EncryptInit_ex(ctx.get(), EVP_aes_128_ecb(), nullptr, key.data(), nullptr) != 1) {
|
||||
if(EVP_DecryptInit_ex(decrypt_ctx.get(), EVP_aes_128_ecb(), nullptr, key.data(), nullptr) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
EVP_CIPHER_CTX_set_padding(ctx.get(), padding);
|
||||
EVP_CIPHER_CTX_set_padding(decrypt_ctx.get(), padding);
|
||||
|
||||
plaintext.resize((cipher.size() + 15) / 16 * 16);
|
||||
auto size = (int)plaintext.size();
|
||||
// Decrypt into the caller's buffer, leaving room for the auth tag to be prepended
|
||||
if(EVP_DecryptUpdate(decrypt_ctx.get(), plaintext.data(), &size, (const std::uint8_t *)cipher.data(), cipher.size()) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if(EVP_DecryptFinal_ex(decrypt_ctx.get(), plaintext.data(), &len) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
plaintext.resize(len + size);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ecb_t::encrypt(const std::string_view &plaintext, std::vector<std::uint8_t> &cipher) {
|
||||
auto fg = util::fail_guard([this]() {
|
||||
EVP_CIPHER_CTX_reset(encrypt_ctx.get());
|
||||
});
|
||||
|
||||
// Gen 7 servers use 128-bit AES ECB
|
||||
if(EVP_EncryptInit_ex(encrypt_ctx.get(), EVP_aes_128_ecb(), nullptr, key.data(), nullptr) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
EVP_CIPHER_CTX_set_padding(encrypt_ctx.get(), padding);
|
||||
|
||||
int len;
|
||||
|
||||
cipher.resize((plaintext.size() + 15) / 16 * 16);
|
||||
auto size = (int)cipher.size();
|
||||
|
||||
// Encrypt into the caller's buffer
|
||||
if(EVP_EncryptUpdate(ctx.get(), cipher.data(), &size, (const std::uint8_t *)plaintext.data(), plaintext.size()) != 1) {
|
||||
if(EVP_EncryptUpdate(encrypt_ctx.get(), cipher.data(), &size, (const std::uint8_t *)plaintext.data(), plaintext.size()) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if(EVP_EncryptFinal_ex(ctx.get(), cipher.data() + size, &len) != 1) {
|
||||
if(EVP_EncryptFinal_ex(encrypt_ctx.get(), cipher.data() + size, &len) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -155,6 +261,44 @@ int cipher_t::encrypt(const std::string_view &plaintext, std::vector<std::uint8_
|
||||
return 0;
|
||||
}
|
||||
|
||||
int cbc_t::encrypt(const std::string_view &plaintext, std::uint8_t *cipher, aes_t *iv) {
|
||||
if(!encrypt_ctx && init_encrypt_cbc(encrypt_ctx, &key, iv, padding)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Calling with cipher == nullptr results in a parameter change
|
||||
// without requiring a reallocation of the internal cipher ctx.
|
||||
if(EVP_EncryptInit_ex(encrypt_ctx.get(), nullptr, nullptr, nullptr, iv->data()) != 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int len;
|
||||
|
||||
int size = plaintext.size(); //round_to_pkcs7_padded(plaintext.size());
|
||||
|
||||
// Encrypt into the caller's buffer
|
||||
if(EVP_EncryptUpdate(encrypt_ctx.get(), cipher, &size, (const std::uint8_t *)plaintext.data(), plaintext.size()) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if(EVP_EncryptFinal_ex(encrypt_ctx.get(), cipher + size, &len) != 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return size + len;
|
||||
}
|
||||
|
||||
ecb_t::ecb_t(const aes_t &key, bool padding)
|
||||
: cipher_t { EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_new(), key, padding } {}
|
||||
|
||||
cbc_t::cbc_t(const aes_t &key, bool padding)
|
||||
: cipher_t { nullptr, nullptr, key, padding } {}
|
||||
|
||||
gcm_t::gcm_t(const crypto::aes_t &key, bool padding)
|
||||
: cipher_t { nullptr, nullptr, key, padding } {}
|
||||
|
||||
} // namespace cipher
|
||||
|
||||
aes_t gen_aes_key(const std::array<uint8_t, 16> &salt, const std::string_view &pin) {
|
||||
aes_t key;
|
||||
|
||||
@@ -338,4 +482,14 @@ bool verify256(const x509_t &x509, const std::string_view &data, const std::stri
|
||||
void md_ctx_destroy(EVP_MD_CTX *ctx) {
|
||||
EVP_MD_CTX_destroy(ctx);
|
||||
}
|
||||
} // namespace crypto
|
||||
|
||||
std::string rand_alphabet(std::size_t bytes, const std::string_view &alphabet) {
|
||||
auto value = rand(bytes);
|
||||
|
||||
for(std::size_t i = 0; i != value.size(); ++i) {
|
||||
value[i] = alphabet[value[i] % alphabet.length()];
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
} // namespace crypto
|
||||
@@ -6,7 +6,6 @@
|
||||
#define SUNSHINE_CRYPTO_H
|
||||
|
||||
#include <array>
|
||||
#include <cassert>
|
||||
#include <openssl/evp.h>
|
||||
#include <openssl/rand.h>
|
||||
#include <openssl/sha.h>
|
||||
@@ -35,6 +34,7 @@ using bio_t = util::safe_ptr<BIO, BIO_free_all>;
|
||||
using pkey_t = util::safe_ptr<EVP_PKEY, EVP_PKEY_free>;
|
||||
|
||||
sha256_t hash(const std::string_view &plaintext);
|
||||
|
||||
aes_t gen_aes_key(const std::array<uint8_t, 16> &salt, const std::string_view &pin);
|
||||
|
||||
x509_t x509(const std::string_view &x);
|
||||
@@ -50,6 +50,8 @@ creds_t gen_creds(const std::string_view &cn, std::uint32_t key_bits);
|
||||
std::string_view signature(const x509_t &x);
|
||||
|
||||
std::string rand(std::size_t bytes);
|
||||
std::string rand_alphabet(std::size_t bytes,
|
||||
const std::string_view &alphabet = std::string_view { "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!%&()=-" });
|
||||
|
||||
class cert_chain_t {
|
||||
public:
|
||||
@@ -64,24 +66,70 @@ private:
|
||||
x509_store_ctx_t _cert_ctx;
|
||||
};
|
||||
|
||||
namespace cipher {
|
||||
constexpr std::size_t tag_size = 16;
|
||||
constexpr std::size_t round_to_pkcs7_padded(std::size_t size) {
|
||||
return ((size + 15) / 16) * 16;
|
||||
}
|
||||
|
||||
class cipher_t {
|
||||
public:
|
||||
cipher_t(const aes_t &key);
|
||||
cipher_t(cipher_t &&) noexcept = default;
|
||||
cipher_t &operator=(cipher_t &&) noexcept = default;
|
||||
cipher_ctx_t decrypt_ctx;
|
||||
cipher_ctx_t encrypt_ctx;
|
||||
|
||||
int encrypt(const std::string_view &plaintext, std::vector<std::uint8_t> &cipher);
|
||||
|
||||
int decrypt_gcm(aes_t &iv, const std::string_view &cipher, std::vector<std::uint8_t> &plaintext);
|
||||
int decrypt(const std::string_view &cipher, std::vector<std::uint8_t> &plaintext);
|
||||
|
||||
private:
|
||||
cipher_ctx_t ctx;
|
||||
aes_t key;
|
||||
|
||||
public:
|
||||
bool padding;
|
||||
};
|
||||
|
||||
class ecb_t : public cipher_t {
|
||||
public:
|
||||
ecb_t() = default;
|
||||
ecb_t(ecb_t &&) noexcept = default;
|
||||
ecb_t &operator=(ecb_t &&) noexcept = default;
|
||||
|
||||
ecb_t(const aes_t &key, bool padding = true);
|
||||
|
||||
int encrypt(const std::string_view &plaintext, std::vector<std::uint8_t> &cipher);
|
||||
int decrypt(const std::string_view &cipher, std::vector<std::uint8_t> &plaintext);
|
||||
};
|
||||
|
||||
class gcm_t : public cipher_t {
|
||||
public:
|
||||
gcm_t() = default;
|
||||
gcm_t(gcm_t &&) noexcept = default;
|
||||
gcm_t &operator=(gcm_t &&) noexcept = default;
|
||||
|
||||
gcm_t(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
|
||||
*/
|
||||
int encrypt(const std::string_view &plaintext, std::uint8_t *tagged_cipher, aes_t *iv);
|
||||
|
||||
int decrypt(const std::string_view &cipher, std::vector<std::uint8_t> &plaintext, aes_t *iv);
|
||||
};
|
||||
|
||||
class cbc_t : public cipher_t {
|
||||
public:
|
||||
cbc_t() = default;
|
||||
cbc_t(cbc_t &&) noexcept = default;
|
||||
cbc_t &operator=(cbc_t &&) noexcept = default;
|
||||
|
||||
cbc_t(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
|
||||
*/
|
||||
int encrypt(const std::string_view &plaintext, std::uint8_t *cipher, aes_t *iv);
|
||||
};
|
||||
} // namespace cipher
|
||||
} // namespace crypto
|
||||
|
||||
#endif //SUNSHINE_CRYPTO_H
|
||||
|
||||
182
sunshine/httpcommon.cpp
Normal file
182
sunshine/httpcommon.cpp
Normal file
@@ -0,0 +1,182 @@
|
||||
#define BOOST_BIND_GLOBAL_PLACEHOLDERS
|
||||
|
||||
#include "process.h"
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
#include <boost/property_tree/json_parser.hpp>
|
||||
#include <boost/property_tree/ptree.hpp>
|
||||
#include <boost/property_tree/xml_parser.hpp>
|
||||
|
||||
#include <boost/asio/ssl/context.hpp>
|
||||
|
||||
#include <Simple-Web-Server/server_http.hpp>
|
||||
#include <Simple-Web-Server/server_https.hpp>
|
||||
#include <boost/asio/ssl/context_base.hpp>
|
||||
|
||||
#include "config.h"
|
||||
#include "crypto.h"
|
||||
#include "httpcommon.h"
|
||||
#include "main.h"
|
||||
#include "network.h"
|
||||
#include "nvhttp.h"
|
||||
#include "platform/common.h"
|
||||
#include "rtsp.h"
|
||||
#include "utility.h"
|
||||
#include "uuid.h"
|
||||
|
||||
namespace http {
|
||||
using namespace std::literals;
|
||||
namespace fs = std::filesystem;
|
||||
namespace pt = boost::property_tree;
|
||||
|
||||
int reload_user_creds(const std::string &file);
|
||||
bool user_creds_exist(const std::string &file);
|
||||
|
||||
std::string unique_id;
|
||||
net::net_e origin_pin_allowed;
|
||||
net::net_e origin_web_ui_allowed;
|
||||
|
||||
int init() {
|
||||
bool clean_slate = config::sunshine.flags[config::flag::FRESH_STATE];
|
||||
origin_pin_allowed = net::from_enum_string(config::nvhttp.origin_pin_allowed);
|
||||
origin_web_ui_allowed = net::from_enum_string(config::nvhttp.origin_web_ui_allowed);
|
||||
|
||||
if(clean_slate) {
|
||||
unique_id = util::uuid_t::generate().string();
|
||||
auto dir = std::filesystem::temp_directory_path() / "Sushine"sv;
|
||||
config::nvhttp.cert = (dir / ("cert-"s + unique_id)).string();
|
||||
config::nvhttp.pkey = (dir / ("pkey-"s + unique_id)).string();
|
||||
}
|
||||
|
||||
if(!fs::exists(config::nvhttp.pkey) || !fs::exists(config::nvhttp.cert)) {
|
||||
if(create_creds(config::nvhttp.pkey, config::nvhttp.cert)) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
if(user_creds_exist(config::sunshine.credentials_file)) {
|
||||
if(reload_user_creds(config::sunshine.credentials_file)) return -1;
|
||||
} else {
|
||||
BOOST_LOG(info) << "Open the Web UI to set your new username and password and getting started";
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int save_user_creds(const std::string &file, const std::string &username, const std::string &password, bool run_our_mouth) {
|
||||
pt::ptree outputTree;
|
||||
|
||||
if(fs::exists(file)) {
|
||||
try {
|
||||
pt::read_json(file, outputTree);
|
||||
}
|
||||
catch(std::exception &e) {
|
||||
BOOST_LOG(error) << "Couldn't read user credentials: "sv << e.what();
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
auto salt = crypto::rand_alphabet(16);
|
||||
outputTree.put("username", username);
|
||||
outputTree.put("salt", salt);
|
||||
outputTree.put("password", util::hex(crypto::hash(password + salt)).to_string());
|
||||
try {
|
||||
pt::write_json(file, outputTree);
|
||||
}
|
||||
catch(std::exception &e) {
|
||||
BOOST_LOG(error) << "generating user credentials: "sv << e.what();
|
||||
return -1;
|
||||
}
|
||||
|
||||
BOOST_LOG(info) << "New credentials have been created"sv;
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool user_creds_exist(const std::string &file) {
|
||||
if(!fs::exists(file)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
pt::ptree inputTree;
|
||||
try {
|
||||
pt::read_json(file, inputTree);
|
||||
return inputTree.find("username") != inputTree.not_found() &&
|
||||
inputTree.find("password") != inputTree.not_found() &&
|
||||
inputTree.find("salt") != inputTree.not_found();
|
||||
}
|
||||
catch(std::exception &e) {
|
||||
BOOST_LOG(error) << "validating user credentials: "sv << e.what();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
int reload_user_creds(const std::string &file) {
|
||||
pt::ptree inputTree;
|
||||
try {
|
||||
pt::read_json(file, inputTree);
|
||||
config::sunshine.username = inputTree.get<std::string>("username");
|
||||
config::sunshine.password = inputTree.get<std::string>("password");
|
||||
config::sunshine.salt = inputTree.get<std::string>("salt");
|
||||
}
|
||||
catch(std::exception &e) {
|
||||
BOOST_LOG(error) << "loading user credentials: "sv << e.what();
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int create_creds(const std::string &pkey, const std::string &cert) {
|
||||
fs::path pkey_path = pkey;
|
||||
fs::path cert_path = cert;
|
||||
|
||||
auto creds = crypto::gen_creds("Sunshine Gamestream Host"sv, 2048);
|
||||
|
||||
auto pkey_dir = pkey_path;
|
||||
auto cert_dir = cert_path;
|
||||
pkey_dir.remove_filename();
|
||||
cert_dir.remove_filename();
|
||||
|
||||
std::error_code err_code {};
|
||||
fs::create_directories(pkey_dir, err_code);
|
||||
if(err_code) {
|
||||
BOOST_LOG(error) << "Couldn't create directory ["sv << pkey_dir << "] :"sv << err_code.message();
|
||||
return -1;
|
||||
}
|
||||
|
||||
fs::create_directories(cert_dir, err_code);
|
||||
if(err_code) {
|
||||
BOOST_LOG(error) << "Couldn't create directory ["sv << cert_dir << "] :"sv << err_code.message();
|
||||
return -1;
|
||||
}
|
||||
|
||||
if(write_file(pkey.c_str(), creds.pkey)) {
|
||||
BOOST_LOG(error) << "Couldn't open ["sv << config::nvhttp.pkey << ']';
|
||||
return -1;
|
||||
}
|
||||
|
||||
if(write_file(cert.c_str(), creds.x509)) {
|
||||
BOOST_LOG(error) << "Couldn't open ["sv << config::nvhttp.cert << ']';
|
||||
return -1;
|
||||
}
|
||||
|
||||
fs::permissions(pkey_path,
|
||||
fs::perms::owner_read | fs::perms::owner_write,
|
||||
fs::perm_options::replace, err_code);
|
||||
|
||||
if(err_code) {
|
||||
BOOST_LOG(error) << "Couldn't change permissions of ["sv << config::nvhttp.pkey << "] :"sv << err_code.message();
|
||||
return -1;
|
||||
}
|
||||
|
||||
fs::permissions(cert_path,
|
||||
fs::perms::owner_read | fs::perms::group_read | fs::perms::others_read | fs::perms::owner_write,
|
||||
fs::perm_options::replace, err_code);
|
||||
|
||||
if(err_code) {
|
||||
BOOST_LOG(error) << "Couldn't change permissions of ["sv << config::nvhttp.cert << "] :"sv << err_code.message();
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
} // namespace http
|
||||
19
sunshine/httpcommon.h
Normal file
19
sunshine/httpcommon.h
Normal file
@@ -0,0 +1,19 @@
|
||||
#include "network.h"
|
||||
#include "thread_safe.h"
|
||||
|
||||
namespace http {
|
||||
|
||||
int init();
|
||||
int create_creds(const std::string &pkey, const std::string &cert);
|
||||
int save_user_creds(
|
||||
const std::string &file,
|
||||
const std::string &username,
|
||||
const std::string &password,
|
||||
bool run_our_mouth = false);
|
||||
|
||||
int reload_user_creds(const std::string &file);
|
||||
extern std::string unique_id;
|
||||
extern net::net_e origin_pin_allowed;
|
||||
extern net::net_e origin_web_ui_allowed;
|
||||
|
||||
} // namespace http
|
||||
@@ -17,12 +17,23 @@ extern "C" {
|
||||
#include "thread_pool.h"
|
||||
#include "utility.h"
|
||||
|
||||
using namespace std::literals;
|
||||
namespace input {
|
||||
|
||||
constexpr auto MAX_GAMEPADS = std::min((std::size_t)platf::MAX_GAMEPADS, sizeof(std::int16_t) * 8);
|
||||
#define DISABLE_LEFT_BUTTON_DELAY ((util::ThreadPool::task_id_t)0x01)
|
||||
#define ENABLE_LEFT_BUTTON_DELAY nullptr
|
||||
|
||||
constexpr auto VKEY_SHIFT = 0x10;
|
||||
constexpr auto VKEY_LSHIFT = 0xA0;
|
||||
constexpr auto VKEY_RSHIFT = 0xA1;
|
||||
constexpr auto VKEY_CONTROL = 0x11;
|
||||
constexpr auto VKEY_LCONTROL = 0xA2;
|
||||
constexpr auto VKEY_RCONTROL = 0xA3;
|
||||
constexpr auto VKEY_MENU = 0x12;
|
||||
constexpr auto VKEY_LMENU = 0xA4;
|
||||
constexpr auto VKEY_RMENU = 0xA5;
|
||||
|
||||
enum class button_state_e {
|
||||
NONE,
|
||||
DOWN,
|
||||
@@ -46,12 +57,7 @@ void free_id(std::bitset<N> &gamepad_mask, int id) {
|
||||
gamepad_mask[id] = false;
|
||||
}
|
||||
|
||||
touch_port_event_t touch_port_event;
|
||||
platf::touch_port_t touch_port {
|
||||
0, 0, 0, 0
|
||||
};
|
||||
|
||||
static util::TaskPool::task_id_t task_id {};
|
||||
static util::TaskPool::task_id_t key_press_repeat_id {};
|
||||
static std::unordered_map<short, bool> key_press {};
|
||||
static std::array<std::uint8_t, 5> mouse_press {};
|
||||
|
||||
@@ -89,15 +95,65 @@ struct gamepad_t {
|
||||
};
|
||||
|
||||
struct input_t {
|
||||
input_t() : active_gamepad_state {}, gamepads(MAX_GAMEPADS), mouse_left_button_timeout {} {}
|
||||
enum shortkey_e {
|
||||
CTRL = 0x1,
|
||||
ALT = 0x2,
|
||||
SHIFT = 0x4,
|
||||
|
||||
SHORTCUT = CTRL | ALT | SHIFT
|
||||
};
|
||||
|
||||
input_t(
|
||||
safe::mail_raw_t::event_t<input::touch_port_t> touch_port_event,
|
||||
platf::rumble_queue_t rumble_queue)
|
||||
: shortcutFlags {},
|
||||
active_gamepad_state {},
|
||||
gamepads(MAX_GAMEPADS),
|
||||
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 } {}
|
||||
|
||||
// Keep track of alt+ctrl+shift key combo
|
||||
int shortcutFlags;
|
||||
|
||||
std::uint16_t active_gamepad_state;
|
||||
std::vector<gamepad_t> gamepads;
|
||||
|
||||
safe::mail_raw_t::event_t<input::touch_port_t> touch_port_event;
|
||||
platf::rumble_queue_t rumble_queue;
|
||||
|
||||
util::ThreadPool::task_id_t mouse_left_button_timeout;
|
||||
|
||||
input::touch_port_t touch_port;
|
||||
};
|
||||
|
||||
using namespace std::literals;
|
||||
/**
|
||||
* Apply shortcut based on VKEY
|
||||
* On success
|
||||
* return > 0
|
||||
* On nothing
|
||||
* return 0
|
||||
*/
|
||||
inline int apply_shortcut(short keyCode) {
|
||||
constexpr auto VK_F1 = 0x70;
|
||||
constexpr auto VK_F13 = 0x7C;
|
||||
|
||||
BOOST_LOG(debug) << "Apply Shortcut: 0x"sv << util::hex((std::uint8_t)keyCode).to_string_view();
|
||||
|
||||
if(keyCode >= VK_F1 && keyCode <= VK_F13) {
|
||||
mail::man->event<int>(mail::switch_display)->raise(keyCode - VK_F1);
|
||||
return 1;
|
||||
}
|
||||
|
||||
switch(keyCode) {
|
||||
case 0x4E /* VKEY_N */:
|
||||
display_cursor = !display_cursor;
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void print(PNV_REL_MOUSE_MOVE_PACKET packet) {
|
||||
BOOST_LOG(debug)
|
||||
@@ -142,7 +198,8 @@ void print(PNV_KEYBOARD_PACKET packet) {
|
||||
}
|
||||
|
||||
void print(PNV_MULTI_CONTROLLER_PACKET packet) {
|
||||
BOOST_LOG(debug)
|
||||
// Moonlight spams controller packet even when not necessary
|
||||
BOOST_LOG(verbose)
|
||||
<< "--begin controller packet--"sv << std::endl
|
||||
<< "controllerNumber ["sv << packet->controllerNumber << ']' << std::endl
|
||||
<< "activeGamepadMask ["sv << util::hex(packet->activeGamepadMask).to_string_view() << ']' << std::endl
|
||||
@@ -188,19 +245,17 @@ void print(void *input) {
|
||||
}
|
||||
|
||||
void passthrough(std::shared_ptr<input_t> &input, PNV_REL_MOUSE_MOVE_PACKET packet) {
|
||||
display_cursor = true;
|
||||
|
||||
input->mouse_left_button_timeout = DISABLE_LEFT_BUTTON_DELAY;
|
||||
platf::move_mouse(platf_input, util::endian::big(packet->deltaX), util::endian::big(packet->deltaY));
|
||||
}
|
||||
|
||||
void passthrough(std::shared_ptr<input_t> &input, PNV_ABS_MOUSE_MOVE_PACKET packet) {
|
||||
display_cursor = true;
|
||||
|
||||
if(input->mouse_left_button_timeout == DISABLE_LEFT_BUTTON_DELAY) {
|
||||
input->mouse_left_button_timeout = ENABLE_LEFT_BUTTON_DELAY;
|
||||
}
|
||||
|
||||
auto &touch_port_event = input->touch_port_event;
|
||||
auto &touch_port = input->touch_port;
|
||||
if(touch_port_event->peek()) {
|
||||
touch_port = *touch_port_event->pop();
|
||||
}
|
||||
@@ -216,13 +271,27 @@ void passthrough(std::shared_ptr<input_t> &input, PNV_ABS_MOUSE_MOVE_PACKET pack
|
||||
return;
|
||||
}
|
||||
|
||||
float width = util::endian::big(packet->width);
|
||||
float height = util::endian::big(packet->height);
|
||||
auto width = (float)util::endian::big(packet->width);
|
||||
auto height = (float)util::endian::big(packet->height);
|
||||
|
||||
auto scale_x = (float)touch_port.width / width;
|
||||
auto scale_y = (float)touch_port.height / height;
|
||||
auto scalarX = touch_port.width / width;
|
||||
auto scalarY = touch_port.height / height;
|
||||
|
||||
platf::abs_mouse(platf_input, touch_port, x * scale_x, y * scale_y);
|
||||
x *= scalarX;
|
||||
y *= scalarY;
|
||||
|
||||
auto offsetX = touch_port.client_offsetX;
|
||||
auto offsetY = touch_port.client_offsetY;
|
||||
|
||||
std::clamp(x, offsetX, width - offsetX);
|
||||
std::clamp(y, offsetY, height - offsetY);
|
||||
|
||||
platf::touch_port_t abs_port {
|
||||
touch_port.offset_x, touch_port.offset_y,
|
||||
touch_port.env_width, touch_port.env_height
|
||||
};
|
||||
|
||||
platf::abs_mouse(platf_input, abs_port, (x - offsetX) * touch_port.scalar_inv, (y - offsetY) * touch_port.scalar_inv);
|
||||
}
|
||||
|
||||
void passthrough(std::shared_ptr<input_t> &input, PNV_MOUSE_BUTTON_PACKET packet) {
|
||||
@@ -231,8 +300,6 @@ void passthrough(std::shared_ptr<input_t> &input, PNV_MOUSE_BUTTON_PACKET packet
|
||||
auto constexpr BUTTON_LEFT = 0x01;
|
||||
auto constexpr BUTTON_RIGHT = 0x03;
|
||||
|
||||
display_cursor = true;
|
||||
|
||||
auto release = packet->action == BUTTON_RELEASED;
|
||||
|
||||
auto button = util::endian::big(packet->button);
|
||||
@@ -291,32 +358,86 @@ void passthrough(std::shared_ptr<input_t> &input, PNV_MOUSE_BUTTON_PACKET packet
|
||||
platf::button_mouse(platf_input, button, release);
|
||||
}
|
||||
|
||||
short map_keycode(short keycode) {
|
||||
auto it = config::input.keybindings.find(keycode);
|
||||
if(it != std::end(config::input.keybindings)) {
|
||||
return it->second;
|
||||
}
|
||||
|
||||
return keycode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update flags for keyboard shortcut combo's
|
||||
*/
|
||||
inline void update_shortcutFlags(int *flags, short keyCode, bool release) {
|
||||
switch(keyCode) {
|
||||
case VKEY_SHIFT:
|
||||
case VKEY_LSHIFT:
|
||||
case VKEY_RSHIFT:
|
||||
if(release) {
|
||||
*flags &= ~input_t::SHIFT;
|
||||
}
|
||||
else {
|
||||
*flags |= input_t::SHIFT;
|
||||
}
|
||||
break;
|
||||
case VKEY_CONTROL:
|
||||
case VKEY_LCONTROL:
|
||||
case VKEY_RCONTROL:
|
||||
if(release) {
|
||||
*flags &= ~input_t::CTRL;
|
||||
}
|
||||
else {
|
||||
*flags |= input_t::CTRL;
|
||||
}
|
||||
break;
|
||||
case VKEY_MENU:
|
||||
case VKEY_LMENU:
|
||||
case VKEY_RMENU:
|
||||
if(release) {
|
||||
*flags &= ~input_t::ALT;
|
||||
}
|
||||
else {
|
||||
*flags |= input_t::ALT;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void repeat_key(short key_code) {
|
||||
// If key no longer pressed, stop repeating
|
||||
if(!key_press[key_code]) {
|
||||
task_id = nullptr;
|
||||
key_press_repeat_id = nullptr;
|
||||
return;
|
||||
}
|
||||
|
||||
platf::keyboard(platf_input, key_code & 0x00FF, false);
|
||||
platf::keyboard(platf_input, map_keycode(key_code), false);
|
||||
|
||||
task_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).task_id;
|
||||
}
|
||||
|
||||
void passthrough(std::shared_ptr<input_t> &input, PNV_KEYBOARD_PACKET packet) {
|
||||
auto constexpr BUTTON_RELEASED = 0x04;
|
||||
|
||||
auto release = packet->keyAction == BUTTON_RELEASED;
|
||||
auto keyCode = packet->keyCode & 0x00FF;
|
||||
|
||||
auto &pressed = key_press[packet->keyCode];
|
||||
auto &pressed = key_press[keyCode];
|
||||
if(!pressed) {
|
||||
if(!release) {
|
||||
if(task_id) {
|
||||
task_pool.cancel(task_id);
|
||||
// A new key has been pressed down, we need to check for key combo's
|
||||
// If a keycombo has been pressed down, don't pass it through
|
||||
if(input->shortcutFlags == input_t::SHORTCUT && apply_shortcut(keyCode) > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(key_press_repeat_id) {
|
||||
task_pool.cancel(key_press_repeat_id);
|
||||
}
|
||||
|
||||
if(config::input.key_repeat_delay.count() > 0) {
|
||||
task_id = task_pool.pushDelayed(repeat_key, config::input.key_repeat_delay, packet->keyCode).task_id;
|
||||
key_press_repeat_id = task_pool.pushDelayed(repeat_key, config::input.key_repeat_delay, keyCode).task_id;
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -330,16 +451,16 @@ void passthrough(std::shared_ptr<input_t> &input, PNV_KEYBOARD_PACKET packet) {
|
||||
}
|
||||
|
||||
pressed = !release;
|
||||
platf::keyboard(platf_input, packet->keyCode & 0x00FF, release);
|
||||
|
||||
update_shortcutFlags(&input->shortcutFlags, map_keycode(keyCode), release);
|
||||
platf::keyboard(platf_input, map_keycode(keyCode), release);
|
||||
}
|
||||
|
||||
void passthrough(PNV_SCROLL_PACKET packet) {
|
||||
display_cursor = true;
|
||||
|
||||
platf::scroll(platf_input, util::endian::big(packet->scrollAmt1));
|
||||
}
|
||||
|
||||
int updateGamepads(std::vector<gamepad_t> &gamepads, std::int16_t old_state, std::int16_t new_state) {
|
||||
int updateGamepads(std::vector<gamepad_t> &gamepads, std::int16_t old_state, std::int16_t new_state, const platf::rumble_queue_t &rumble_queue) {
|
||||
auto xorGamepadMask = old_state ^ new_state;
|
||||
if(!xorGamepadMask) {
|
||||
return 0;
|
||||
@@ -365,7 +486,7 @@ int updateGamepads(std::vector<gamepad_t> &gamepads, std::int16_t old_state, std
|
||||
return -1;
|
||||
}
|
||||
|
||||
if(platf::alloc_gamepad(platf_input, id)) {
|
||||
if(platf::alloc_gamepad(platf_input, id, rumble_queue)) {
|
||||
free_id(gamepadMask, id);
|
||||
// allocating a gamepad failed: solution: ignore gamepads
|
||||
// The implementations of platf::alloc_gamepad already has logging
|
||||
@@ -381,7 +502,7 @@ int updateGamepads(std::vector<gamepad_t> &gamepads, std::int16_t old_state, std
|
||||
}
|
||||
|
||||
void passthrough(std::shared_ptr<input_t> &input, PNV_MULTI_CONTROLLER_PACKET packet) {
|
||||
if(updateGamepads(input->gamepads, input->active_gamepad_state, packet->activeGamepadMask)) {
|
||||
if(updateGamepads(input->gamepads, input->active_gamepad_state, packet->activeGamepadMask, input->rumble_queue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -407,8 +528,6 @@ void passthrough(std::shared_ptr<input_t> &input, PNV_MULTI_CONTROLLER_PACKET pa
|
||||
return;
|
||||
}
|
||||
|
||||
display_cursor = false;
|
||||
|
||||
std::uint16_t bf = packet->buttonFlags;
|
||||
platf::gamepad_state_t gamepad_state {
|
||||
bf,
|
||||
@@ -517,7 +636,7 @@ void passthrough(std::shared_ptr<input_t> &input, std::vector<std::uint8_t> &&in
|
||||
}
|
||||
|
||||
void reset(std::shared_ptr<input_t> &input) {
|
||||
task_pool.cancel(task_id);
|
||||
task_pool.cancel(key_press_repeat_id);
|
||||
task_pool.cancel(input->mouse_left_button_timeout);
|
||||
|
||||
// Ensure input is synchronous, by using the task_pool
|
||||
@@ -536,13 +655,23 @@ void reset(std::shared_ptr<input_t> &input) {
|
||||
});
|
||||
}
|
||||
|
||||
void init() {
|
||||
touch_port_event = std::make_unique<touch_port_event_t::element_type>();
|
||||
platf_input = platf::input();
|
||||
class deinit_t : public platf::deinit_t {
|
||||
public:
|
||||
~deinit_t() override {
|
||||
platf_input.reset();
|
||||
}
|
||||
};
|
||||
|
||||
[[nodiscard]] std::unique_ptr<platf::deinit_t> init() {
|
||||
platf_input = platf::input();
|
||||
|
||||
return std::make_unique<deinit_t>();
|
||||
}
|
||||
|
||||
std::shared_ptr<input_t> alloc() {
|
||||
auto input = std::make_shared<input_t>();
|
||||
std::shared_ptr<input_t> alloc(safe::mail_t mail) {
|
||||
auto input = std::make_shared<input_t>(
|
||||
mail->event<input::touch_port_t>(mail::touch_port),
|
||||
mail->queue<platf::rumble_t>(mail::rumble));
|
||||
|
||||
// Workaround to ensure new frames will be captured when a client connects
|
||||
task_pool.pushDelayed([]() {
|
||||
|
||||
@@ -5,10 +5,12 @@
|
||||
#ifndef SUNSHINE_INPUT_H
|
||||
#define SUNSHINE_INPUT_H
|
||||
|
||||
#include <functional>
|
||||
|
||||
#include "platform/common.h"
|
||||
#include "thread_safe.h"
|
||||
namespace input {
|
||||
|
||||
namespace input {
|
||||
struct input_t;
|
||||
|
||||
void print(void *input);
|
||||
@@ -16,12 +18,18 @@ void reset(std::shared_ptr<input_t> &input);
|
||||
void passthrough(std::shared_ptr<input_t> &input, std::vector<std::uint8_t> &&input_data);
|
||||
|
||||
|
||||
void init();
|
||||
[[nodiscard]] std::unique_ptr<platf::deinit_t> init();
|
||||
|
||||
std::shared_ptr<input_t> alloc();
|
||||
std::shared_ptr<input_t> alloc(safe::mail_t mail);
|
||||
|
||||
using touch_port_event_t = std::unique_ptr<safe::event_t<platf::touch_port_t>>;
|
||||
extern touch_port_event_t touch_port_event;
|
||||
struct touch_port_t : public platf::touch_port_t {
|
||||
int env_width, env_height;
|
||||
|
||||
// Offset x and y coordinates of the client
|
||||
float client_offsetX, client_offsetY;
|
||||
|
||||
float scalar_inv;
|
||||
};
|
||||
} // namespace input
|
||||
|
||||
#endif //SUNSHINE_INPUT_H
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
#include "process.h"
|
||||
|
||||
#include <csignal>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
|
||||
@@ -15,9 +17,13 @@
|
||||
#include <boost/log/sources/severity_logger.hpp>
|
||||
|
||||
#include "config.h"
|
||||
#include "confighttp.h"
|
||||
#include "httpcommon.h"
|
||||
#include "main.h"
|
||||
#include "nvhttp.h"
|
||||
#include "rtsp.h"
|
||||
#include "thread_pool.h"
|
||||
#include "upnp.h"
|
||||
#include "video.h"
|
||||
|
||||
#include "platform/common.h"
|
||||
@@ -26,6 +32,8 @@ extern "C" {
|
||||
#include <rs.h>
|
||||
}
|
||||
|
||||
safe::mail_t mail::man;
|
||||
|
||||
using namespace std::literals;
|
||||
namespace bl = boost::log;
|
||||
|
||||
@@ -37,7 +45,7 @@ bl::sources::severity_logger<int> warning(3); // Strange events
|
||||
bl::sources::severity_logger<int> error(4); // Recoverable errors
|
||||
bl::sources::severity_logger<int> fatal(5); // Unrecoverable errors
|
||||
|
||||
bool display_cursor;
|
||||
bool display_cursor = true;
|
||||
|
||||
using text_sink = bl::sinks::asynchronous_sink<bl::sinks::text_ostream_backend>;
|
||||
boost::shared_ptr<text_sink> sink;
|
||||
@@ -48,6 +56,30 @@ struct NoDelete {
|
||||
|
||||
BOOST_LOG_ATTRIBUTE_KEYWORD(severity, "Severity", int)
|
||||
|
||||
void print_help(const char *name) {
|
||||
std::cout
|
||||
<< "Usage: "sv << name << " [options] [/path/to/configuration_file] [--cmd]"sv << std::endl
|
||||
<< " Any configurable option can be overwritten with: \"name=value\""sv << std::endl
|
||||
<< std::endl
|
||||
<< " --help | print help"sv << std::endl
|
||||
<< " --creds username password | set user credentials for the Web manager" << std::endl
|
||||
<< std::endl
|
||||
<< " flags"sv << std::endl
|
||||
<< " -0 | Read PIN from stdin"sv << std::endl
|
||||
<< " -1 | Do not load previously saved state and do retain any state after shutdown"sv << std::endl
|
||||
<< " | Effectively starting as if for the first time without overwriting any pairings with your devices"sv << std::endl
|
||||
<< " -2 | Force replacement of headers in video stream" << std::endl
|
||||
<< " -p | Enable/Disable UPnP" << std::endl
|
||||
<< std::endl;
|
||||
}
|
||||
|
||||
namespace help {
|
||||
int entry(const char *name, int argc, char *argv[]) {
|
||||
print_help(name);
|
||||
return 0;
|
||||
}
|
||||
} // namespace help
|
||||
|
||||
void log_flush() {
|
||||
sink->flush();
|
||||
}
|
||||
@@ -64,14 +96,54 @@ void on_signal(int sig, FN &&fn) {
|
||||
std::signal(sig, on_signal_forwarder);
|
||||
}
|
||||
|
||||
namespace gen_creds {
|
||||
int entry(const char *name, int argc, char *argv[]) {
|
||||
if(argc < 2 || argv[0] == "help"sv || argv[1] == "help"sv) {
|
||||
print_help(name);
|
||||
return 0;
|
||||
}
|
||||
|
||||
http::save_user_creds(config::sunshine.credentials_file, argv[0], argv[1]);
|
||||
|
||||
return 0;
|
||||
}
|
||||
} // namespace gen_creds
|
||||
|
||||
std::map<std::string_view, std::function<int(const char *name, int argc, char **argv)>> cmd_to_func {
|
||||
{ "creds"sv, gen_creds::entry },
|
||||
{ "help"sv, help::entry }
|
||||
};
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
util::TaskPool::task_id_t force_shutdown = nullptr;
|
||||
|
||||
bool shutdown_by_interrupt = false;
|
||||
|
||||
auto exit_guard = util::fail_guard([&shutdown_by_interrupt, &force_shutdown]() {
|
||||
if(!shutdown_by_interrupt) {
|
||||
return;
|
||||
}
|
||||
|
||||
task_pool.cancel(force_shutdown);
|
||||
|
||||
std::cout << "Sunshine exited: Press enter to continue"sv << std::endl;
|
||||
|
||||
std::string _;
|
||||
std::getline(std::cin, _);
|
||||
});
|
||||
|
||||
mail::man = std::make_shared<safe::mail_raw_t>();
|
||||
|
||||
if(config::parse(argc, argv)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if(config::sunshine.min_log_level >= 2) {
|
||||
if(config::sunshine.min_log_level >= 1) {
|
||||
av_log_set_level(AV_LOG_QUIET);
|
||||
}
|
||||
else {
|
||||
av_log_set_level(AV_LOG_DEBUG);
|
||||
}
|
||||
|
||||
sink = boost::make_shared<text_sink>();
|
||||
|
||||
@@ -113,28 +185,61 @@ int main(int argc, char *argv[]) {
|
||||
os << _date << log_type << view.attribute_values()[message].extract<std::string>();
|
||||
});
|
||||
|
||||
// Flush after each log record to ensure log file contents on disk isn't stale.
|
||||
// This is particularly important when running from a Windows service.
|
||||
sink->locked_backend()->auto_flush(true);
|
||||
|
||||
bl::core::get()->add_sink(sink);
|
||||
auto fg = util::fail_guard(log_flush);
|
||||
|
||||
if(!config::sunshine.cmd.name.empty()) {
|
||||
auto fn = cmd_to_func.find(config::sunshine.cmd.name);
|
||||
if(fn == std::end(cmd_to_func)) {
|
||||
BOOST_LOG(fatal) << "Unknown command: "sv << config::sunshine.cmd.name;
|
||||
|
||||
BOOST_LOG(info) << "Possible commands:"sv;
|
||||
for(auto &[key, _] : cmd_to_func) {
|
||||
BOOST_LOG(info) << '\t' << key;
|
||||
}
|
||||
|
||||
return 7;
|
||||
}
|
||||
|
||||
return fn->second(argv[0], config::sunshine.cmd.argc, config::sunshine.cmd.argv);
|
||||
}
|
||||
|
||||
task_pool.start(1);
|
||||
|
||||
// Create signal handler after logging has been initialized
|
||||
auto shutdown_event = std::make_shared<safe::event_t<bool>>();
|
||||
on_signal(SIGINT, [shutdown_event]() {
|
||||
auto shutdown_event = mail::man->event<bool>(mail::shutdown);
|
||||
on_signal(SIGINT, [&shutdown_by_interrupt, &force_shutdown, shutdown_event]() {
|
||||
BOOST_LOG(info) << "Interrupt handler called"sv;
|
||||
|
||||
auto task = []() {
|
||||
BOOST_LOG(fatal) << "10 seconds passed, yet Sunshine's still running: Forcing shutdown"sv;
|
||||
log_flush();
|
||||
std::abort();
|
||||
};
|
||||
force_shutdown = task_pool.pushDelayed(task, 10s).task_id;
|
||||
|
||||
shutdown_by_interrupt = true;
|
||||
shutdown_event->raise(true);
|
||||
});
|
||||
|
||||
auto proc_opt = proc::parse(config::stream.file_apps);
|
||||
if(!proc_opt) {
|
||||
return 7;
|
||||
}
|
||||
on_signal(SIGTERM, [&force_shutdown, shutdown_event]() {
|
||||
BOOST_LOG(info) << "Terminate handler called"sv;
|
||||
|
||||
{
|
||||
proc::ctx_t ctx;
|
||||
ctx.name = "Desktop"s;
|
||||
proc_opt->get_apps().emplace(std::begin(proc_opt->get_apps()), std::move(ctx));
|
||||
}
|
||||
auto task = []() {
|
||||
BOOST_LOG(fatal) << "10 seconds passed, yet Sunshine's still running: Forcing shutdown"sv;
|
||||
log_flush();
|
||||
std::abort();
|
||||
};
|
||||
force_shutdown = task_pool.pushDelayed(task, 10s).task_id;
|
||||
|
||||
proc::proc = std::move(*proc_opt);
|
||||
shutdown_event->raise(true);
|
||||
});
|
||||
|
||||
proc::refresh(config::stream.file_apps);
|
||||
|
||||
auto deinit_guard = platf::init();
|
||||
if(!deinit_guard) {
|
||||
@@ -142,18 +247,73 @@ int main(int argc, char *argv[]) {
|
||||
}
|
||||
|
||||
reed_solomon_init();
|
||||
auto input_deinit_guard = input::init();
|
||||
if(video::init()) {
|
||||
return 2;
|
||||
}
|
||||
if(http::init()) {
|
||||
return 3;
|
||||
}
|
||||
|
||||
task_pool.start(1);
|
||||
std::unique_ptr<platf::deinit_t> mDNS;
|
||||
auto sync_mDNS = std::async(std::launch::async, [&mDNS]() {
|
||||
mDNS = platf::publish::start();
|
||||
});
|
||||
|
||||
std::thread httpThread { nvhttp::start, shutdown_event };
|
||||
stream::rtpThread(shutdown_event);
|
||||
std::unique_ptr<platf::deinit_t> upnp_unmap;
|
||||
auto sync_upnp = std::async(std::launch::async, [&upnp_unmap]() {
|
||||
upnp_unmap = upnp::start();
|
||||
});
|
||||
|
||||
//FIXME: Temporary workaround: Simple-Web_server needs to be updated or replaced
|
||||
if(shutdown_event->peek()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::thread httpThread { nvhttp::start };
|
||||
std::thread configThread { confighttp::start };
|
||||
|
||||
stream::rtpThread();
|
||||
|
||||
httpThread.join();
|
||||
configThread.join();
|
||||
|
||||
task_pool.stop();
|
||||
task_pool.join();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::string read_file(const char *path) {
|
||||
if(!std::filesystem::exists(path)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::ifstream in(path);
|
||||
|
||||
std::string input;
|
||||
std::string base64_cert;
|
||||
|
||||
while(!in.eof()) {
|
||||
std::getline(in, input);
|
||||
base64_cert += input + '\n';
|
||||
}
|
||||
|
||||
return base64_cert;
|
||||
}
|
||||
|
||||
int write_file(const char *path, const std::string_view &contents) {
|
||||
std::ofstream out(path);
|
||||
|
||||
if(!out.is_open()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
out << contents;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::uint16_t map_port(int port) {
|
||||
return (std::uint16_t)((int)config::sunshine.port + port);
|
||||
}
|
||||
@@ -5,7 +5,11 @@
|
||||
#ifndef SUNSHINE_MAIN_H
|
||||
#define SUNSHINE_MAIN_H
|
||||
|
||||
#include <string_view>
|
||||
|
||||
#include "thread_pool.h"
|
||||
#include "thread_safe.h"
|
||||
|
||||
#include <boost/log/common.hpp>
|
||||
|
||||
extern util::ThreadPool task_pool;
|
||||
@@ -19,4 +23,35 @@ extern boost::log::sources::severity_logger<int> error;
|
||||
extern boost::log::sources::severity_logger<int> fatal;
|
||||
|
||||
void log_flush();
|
||||
|
||||
void print_help(const char *name);
|
||||
|
||||
std::string read_file(const char *path);
|
||||
int write_file(const char *path, const std::string_view &contents);
|
||||
|
||||
std::uint16_t map_port(int port);
|
||||
|
||||
namespace mail {
|
||||
#define MAIL(x) \
|
||||
constexpr auto x = std::string_view { #x }
|
||||
|
||||
extern safe::mail_t man;
|
||||
|
||||
// Global mail
|
||||
MAIL(shutdown);
|
||||
MAIL(broadcast_shutdown);
|
||||
|
||||
MAIL(video_packets);
|
||||
MAIL(audio_packets);
|
||||
|
||||
MAIL(switch_display);
|
||||
|
||||
// Local mail
|
||||
MAIL(touch_port);
|
||||
MAIL(idr);
|
||||
MAIL(rumble);
|
||||
#undef MAIL
|
||||
} // namespace mail
|
||||
|
||||
|
||||
#endif //SUNSHINE_MAIN_H
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
#include "utility.h"
|
||||
#include <algorithm>
|
||||
|
||||
namespace net {
|
||||
using namespace std::literals;
|
||||
namespace net {
|
||||
// In the format "xxx.xxx.xxx.xxx/x"
|
||||
std::pair<std::uint32_t, std::uint32_t> ip_block(const std::string_view &ip);
|
||||
|
||||
@@ -16,7 +16,7 @@ std::vector<std::pair<std::uint32_t, std::uint32_t>> pc_ips {
|
||||
};
|
||||
std::vector<std::tuple<std::uint32_t, std::uint32_t>> lan_ips {
|
||||
ip_block("192.168.0.0/16"sv),
|
||||
ip_block("172.16.0.0/12"),
|
||||
ip_block("172.16.0.0/12"sv),
|
||||
ip_block("10.0.0.0/8"sv)
|
||||
};
|
||||
|
||||
|
||||
@@ -18,8 +18,10 @@
|
||||
#include <Simple-Web-Server/server_https.hpp>
|
||||
#include <boost/asio/ssl/context_base.hpp>
|
||||
|
||||
|
||||
#include "config.h"
|
||||
#include "crypto.h"
|
||||
#include "httpcommon.h"
|
||||
#include "main.h"
|
||||
#include "network.h"
|
||||
#include "nvhttp.h"
|
||||
@@ -28,21 +30,15 @@
|
||||
#include "utility.h"
|
||||
#include "uuid.h"
|
||||
|
||||
|
||||
namespace nvhttp {
|
||||
using namespace std::literals;
|
||||
constexpr auto PORT_HTTP = 47989;
|
||||
constexpr auto PORT_HTTPS = 47984;
|
||||
namespace nvhttp {
|
||||
|
||||
constexpr auto VERSION = "7.1.400.0";
|
||||
constexpr auto GFE_VERSION = "3.12.0.1";
|
||||
constexpr auto VERSION = "7.1.431.0";
|
||||
constexpr auto GFE_VERSION = "3.23.0.74";
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
namespace pt = boost::property_tree;
|
||||
|
||||
std::string read_file(const char *path);
|
||||
int write_file(const char *path, const std::string_view &contents);
|
||||
|
||||
using https_server_t = SimpleWeb::Server<SimpleWeb::HTTPS>;
|
||||
using http_server_t = SimpleWeb::Server<SimpleWeb::HTTP>;
|
||||
|
||||
@@ -80,8 +76,6 @@ struct pair_session_t {
|
||||
// uniqueID, session
|
||||
std::unordered_map<std::string, pair_session_t> map_id_sess;
|
||||
std::unordered_map<std::string, client_t> map_id_client;
|
||||
std::string unique_id;
|
||||
net::net_e origin_pin_allowed;
|
||||
|
||||
using args_t = SimpleWeb::CaseInsensitiveMultimap;
|
||||
using resp_https_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTPS>::Response>;
|
||||
@@ -97,7 +91,19 @@ enum class op_e {
|
||||
void save_state() {
|
||||
pt::ptree root;
|
||||
|
||||
root.put("root.uniqueid", unique_id);
|
||||
if(fs::exists(config::nvhttp.file_state)) {
|
||||
try {
|
||||
pt::read_json(config::nvhttp.file_state, root);
|
||||
}
|
||||
catch(std::exception &e) {
|
||||
BOOST_LOG(error) << "Couldn't read "sv << config::nvhttp.file_state << ": "sv << e.what();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
root.erase("root"s);
|
||||
|
||||
root.put("root.uniqueid", http::unique_id);
|
||||
auto &nodes = root.add_child("root.devices", pt::ptree {});
|
||||
for(auto &[_, client] : map_id_client) {
|
||||
pt::ptree node;
|
||||
@@ -115,12 +121,19 @@ void save_state() {
|
||||
nodes.push_back(std::make_pair(""s, node));
|
||||
}
|
||||
|
||||
pt::write_json(config::nvhttp.file_state, root);
|
||||
try {
|
||||
pt::write_json(config::nvhttp.file_state, root);
|
||||
}
|
||||
catch(std::exception &e) {
|
||||
BOOST_LOG(error) << "Couldn't write "sv << config::nvhttp.file_state << ": "sv << e.what();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void load_state() {
|
||||
if(!fs::exists(config::nvhttp.file_state)) {
|
||||
unique_id = util::uuid_t::generate().string();
|
||||
BOOST_LOG(info) << "File "sv << config::nvhttp.file_state << " doesn't exist"sv;
|
||||
http::unique_id = util::uuid_t::generate().string();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -129,12 +142,19 @@ void load_state() {
|
||||
pt::read_json(config::nvhttp.file_state, root);
|
||||
}
|
||||
catch(std::exception &e) {
|
||||
BOOST_LOG(warning) << e.what();
|
||||
BOOST_LOG(error) << "Couldn't read "sv << config::nvhttp.file_state << ": "sv << e.what();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
unique_id = root.get<std::string>("root.uniqueid");
|
||||
auto unique_id_p = root.get_optional<std::string>("root.uniqueid");
|
||||
if(!unique_id_p) {
|
||||
// This file doesn't contain moonlight credentials
|
||||
http::unique_id = util::uuid_t::generate().string();
|
||||
return;
|
||||
}
|
||||
http::unique_id = std::move(*unique_id_p);
|
||||
|
||||
auto device_nodes = root.get_child("root.devices");
|
||||
|
||||
for(auto &[_, device_node] : device_nodes) {
|
||||
@@ -170,7 +190,7 @@ stream::launch_session_t make_launch_session(bool host_audio, const args_t &args
|
||||
stream::launch_session_t launch_session;
|
||||
|
||||
launch_session.host_audio = host_audio;
|
||||
launch_session.gcm_key = *util::from_hex<crypto::aes_t>(args.at("rikey"s), true);
|
||||
launch_session.gcm_key = util::from_hex<crypto::aes_t>(args.at("rikey"s), true);
|
||||
uint32_t prepend_iv = util::endian::big<uint32_t>(util::from_view(args.at("rikeyid"s)));
|
||||
auto prepend_iv_p = (uint8_t *)&prepend_iv;
|
||||
|
||||
@@ -191,7 +211,7 @@ void getservercert(pair_session_t &sess, pt::ptree &tree, const std::string &pin
|
||||
|
||||
auto salt = util::from_hex<std::array<uint8_t, 16>>(salt_view, true);
|
||||
|
||||
auto key = crypto::gen_aes_key(*salt, pin);
|
||||
auto key = crypto::gen_aes_key(salt, pin);
|
||||
sess.cipher_key = std::make_unique<crypto::aes_t>(key);
|
||||
|
||||
tree.put("root.paired", 1);
|
||||
@@ -202,8 +222,7 @@ void serverchallengeresp(pair_session_t &sess, pt::ptree &tree, const args_t &ar
|
||||
auto encrypted_response = util::from_hex_vec(args.at("serverchallengeresp"s), true);
|
||||
|
||||
std::vector<uint8_t> decrypted;
|
||||
crypto::cipher_t cipher(*sess.cipher_key);
|
||||
cipher.padding = false;
|
||||
crypto::cipher::ecb_t cipher(*sess.cipher_key, false);
|
||||
|
||||
cipher.decrypt(encrypted_response, decrypted);
|
||||
|
||||
@@ -222,8 +241,7 @@ void serverchallengeresp(pair_session_t &sess, pt::ptree &tree, const args_t &ar
|
||||
void clientchallenge(pair_session_t &sess, pt::ptree &tree, const args_t &args) {
|
||||
auto challenge = util::from_hex_vec(args.at("clientchallenge"s), true);
|
||||
|
||||
crypto::cipher_t cipher(*sess.cipher_key);
|
||||
cipher.padding = false;
|
||||
crypto::cipher::ecb_t cipher(*sess.cipher_key, false);
|
||||
|
||||
std::vector<uint8_t> decrypted;
|
||||
cipher.decrypt(challenge, decrypted);
|
||||
@@ -347,8 +365,11 @@ void not_found(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Response> resp
|
||||
pt::write_xml(data, tree);
|
||||
response->write(data.str());
|
||||
|
||||
*response << "HTTP/1.1 404 NOT FOUND\r\n"
|
||||
<< data.str();
|
||||
*response
|
||||
<< "HTTP/1.1 404 NOT FOUND\r\n"
|
||||
<< data.str();
|
||||
|
||||
response->close_connection_after_response = true;
|
||||
}
|
||||
|
||||
template<class T>
|
||||
@@ -357,6 +378,14 @@ void pair(std::shared_ptr<safe::queue_t<crypto::x509_t>> &add_cert, std::shared_
|
||||
|
||||
pt::ptree tree;
|
||||
|
||||
auto fg = util::fail_guard([&]() {
|
||||
std::ostringstream data;
|
||||
|
||||
pt::write_xml(data, tree);
|
||||
response->write(data.str());
|
||||
response->close_connection_after_response = true;
|
||||
});
|
||||
|
||||
auto args = request->parse_query_string();
|
||||
if(args.find("uniqueid"s) == std::end(args)) {
|
||||
tree.put("root.<xmlattr>.status_code", 400);
|
||||
@@ -380,11 +409,7 @@ void pair(std::shared_ptr<safe::queue_t<crypto::x509_t>> &add_cert, std::shared_
|
||||
|
||||
ptr->second.async_insert_pin.salt = std::move(args.at("salt"s));
|
||||
|
||||
if(config::sunshine.flags[config::flag::CONST_PIN]) {
|
||||
std::string pin("6174");
|
||||
getservercert(ptr->second, tree, pin);
|
||||
}
|
||||
else if(config::sunshine.flags[config::flag::PIN_STDIN]) {
|
||||
if(config::sunshine.flags[config::flag::PIN_STDIN]) {
|
||||
std::string pin;
|
||||
|
||||
std::cout << "Please insert pin: "sv;
|
||||
@@ -395,6 +420,7 @@ void pair(std::shared_ptr<safe::queue_t<crypto::x509_t>> &add_cert, std::shared_
|
||||
else {
|
||||
ptr->second.async_insert_pin.response = std::move(response);
|
||||
|
||||
fg.disable();
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -415,37 +441,16 @@ void pair(std::shared_ptr<safe::queue_t<crypto::x509_t>> &add_cert, std::shared_
|
||||
else {
|
||||
tree.put("root.<xmlattr>.status_code", 404);
|
||||
}
|
||||
|
||||
std::ostringstream data;
|
||||
|
||||
pt::write_xml(data, tree);
|
||||
response->write(data.str());
|
||||
}
|
||||
|
||||
template<class T>
|
||||
void pin(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Response> response, std::shared_ptr<typename SimpleWeb::ServerBase<T>::Request> request) {
|
||||
print_req<T>(request);
|
||||
|
||||
auto address = request->remote_endpoint_address();
|
||||
auto ip_type = net::from_address(address);
|
||||
if(ip_type > origin_pin_allowed) {
|
||||
BOOST_LOG(info) << '[' << address << "] -- denied"sv;
|
||||
|
||||
response->write(SimpleWeb::StatusCode::client_error_forbidden);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
bool pin(std::string pin) {
|
||||
pt::ptree tree;
|
||||
|
||||
if(map_id_sess.empty()) {
|
||||
response->write(SimpleWeb::StatusCode::client_error_im_a_teapot);
|
||||
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
auto &sess = std::begin(map_id_sess)->second;
|
||||
getservercert(sess, tree, request->path_match[1]);
|
||||
getservercert(sess, tree, pin);
|
||||
|
||||
// response to the request for pin
|
||||
std::ostringstream data;
|
||||
@@ -459,15 +464,38 @@ void pin(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Response> response,
|
||||
async_response.right()->write(data.str());
|
||||
}
|
||||
else {
|
||||
response->write(SimpleWeb::StatusCode::client_error_im_a_teapot);
|
||||
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
// reset async_response
|
||||
async_response = std::decay_t<decltype(async_response.left())>();
|
||||
// response to the current request
|
||||
response->write(SimpleWeb::StatusCode::success_ok);
|
||||
return true;
|
||||
}
|
||||
|
||||
template<class T>
|
||||
void pin(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Response> response, std::shared_ptr<typename SimpleWeb::ServerBase<T>::Request> request) {
|
||||
print_req<T>(request);
|
||||
|
||||
response->close_connection_after_response = true;
|
||||
|
||||
auto address = request->remote_endpoint_address();
|
||||
auto ip_type = net::from_address(address);
|
||||
if(ip_type > http::origin_pin_allowed) {
|
||||
BOOST_LOG(info) << "/pin: ["sv << address << "] -- denied"sv;
|
||||
|
||||
response->write(SimpleWeb::StatusCode::client_error_forbidden);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
bool pinResponse = pin(request->path_match[1]);
|
||||
if(pinResponse) {
|
||||
response->write(SimpleWeb::StatusCode::success_ok);
|
||||
}
|
||||
else {
|
||||
response->write(SimpleWeb::StatusCode::client_error_im_a_teapot);
|
||||
}
|
||||
}
|
||||
|
||||
template<class T>
|
||||
@@ -494,7 +522,9 @@ void serverinfo(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Response> res
|
||||
|
||||
tree.put("root.appversion", VERSION);
|
||||
tree.put("root.GfeVersion", GFE_VERSION);
|
||||
tree.put("root.uniqueid", unique_id);
|
||||
tree.put("root.uniqueid", http::unique_id);
|
||||
tree.put("root.HttpsPort", map_port(PORT_HTTPS));
|
||||
tree.put("root.ExternalPort", map_port(PORT_HTTP));
|
||||
tree.put("root.mac", platf::get_mac_address(request->local_endpoint_address()));
|
||||
tree.put("root.MaxLumaPixelsHEVC", config::video.hevc_mode > 1 ? "1869449984" : "0");
|
||||
tree.put("root.LocalIP", request->local_endpoint_address());
|
||||
@@ -547,6 +577,7 @@ void serverinfo(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Response> res
|
||||
|
||||
pt::write_xml(data, tree);
|
||||
response->write(data.str());
|
||||
response->close_connection_after_response = true;
|
||||
}
|
||||
|
||||
void applist(resp_https_t response, req_https_t request) {
|
||||
@@ -559,6 +590,7 @@ void applist(resp_https_t response, req_https_t request) {
|
||||
|
||||
pt::write_xml(data, tree);
|
||||
response->write(data.str());
|
||||
response->close_connection_after_response = true;
|
||||
});
|
||||
|
||||
auto args = request->parse_query_string();
|
||||
@@ -602,6 +634,7 @@ void launch(bool &host_audio, resp_https_t response, req_https_t request) {
|
||||
|
||||
pt::write_xml(data, tree);
|
||||
response->write(data.str());
|
||||
response->close_connection_after_response = true;
|
||||
});
|
||||
|
||||
if(stream::session_count() == config::stream.channels) {
|
||||
@@ -648,6 +681,7 @@ void launch(bool &host_audio, resp_https_t response, req_https_t request) {
|
||||
stream::launch_session_raise(make_launch_session(host_audio, args));
|
||||
|
||||
tree.put("root.<xmlattr>.status_code", 200);
|
||||
tree.put("root.sessionUrl0", "rtsp://"s + request->local_endpoint_address() + ':' + std::to_string(map_port(stream::RTSP_SETUP_PORT)));
|
||||
tree.put("root.gamesession", 1);
|
||||
}
|
||||
|
||||
@@ -660,6 +694,7 @@ void resume(bool &host_audio, resp_https_t response, req_https_t request) {
|
||||
|
||||
pt::write_xml(data, tree);
|
||||
response->write(data.str());
|
||||
response->close_connection_after_response = true;
|
||||
});
|
||||
|
||||
// It is possible that due a race condition that this if-statement gives a false negative,
|
||||
@@ -693,6 +728,7 @@ void resume(bool &host_audio, resp_https_t response, req_https_t request) {
|
||||
stream::launch_session_raise(make_launch_session(host_audio, args));
|
||||
|
||||
tree.put("root.<xmlattr>.status_code", 200);
|
||||
tree.put("root.sessionUrl0", "rtsp://"s + request->local_endpoint_address() + ':' + std::to_string(map_port(stream::RTSP_SETUP_PORT)));
|
||||
tree.put("root.resume", 1);
|
||||
}
|
||||
|
||||
@@ -705,6 +741,7 @@ void cancel(resp_https_t response, req_https_t request) {
|
||||
|
||||
pt::write_xml(data, tree);
|
||||
response->write(data.str());
|
||||
response->close_connection_after_response = true;
|
||||
});
|
||||
|
||||
// It is possible that due a race condition that this if-statement gives a false positive,
|
||||
@@ -729,83 +766,16 @@ void appasset(resp_https_t response, req_https_t request) {
|
||||
|
||||
std::ifstream in(SUNSHINE_ASSETS_DIR "/box.png");
|
||||
response->write(SimpleWeb::StatusCode::success_ok, in);
|
||||
response->close_connection_after_response = true;
|
||||
}
|
||||
|
||||
int create_creds(const std::string &pkey, const std::string &cert) {
|
||||
fs::path pkey_path = pkey;
|
||||
fs::path cert_path = cert;
|
||||
void start() {
|
||||
auto shutdown_event = mail::man->event<bool>(mail::shutdown);
|
||||
|
||||
auto creds = crypto::gen_creds("Sunshine Gamestream Host"sv, 2048);
|
||||
auto port_http = map_port(PORT_HTTP);
|
||||
auto port_https = map_port(PORT_HTTPS);
|
||||
|
||||
auto pkey_dir = pkey_path;
|
||||
auto cert_dir = cert_path;
|
||||
pkey_dir.remove_filename();
|
||||
cert_dir.remove_filename();
|
||||
|
||||
std::error_code err_code {};
|
||||
fs::create_directories(pkey_dir, err_code);
|
||||
if(err_code) {
|
||||
BOOST_LOG(fatal) << "Couldn't create directory ["sv << pkey_dir << "] :"sv << err_code.message();
|
||||
return -1;
|
||||
}
|
||||
|
||||
fs::create_directories(cert_dir, err_code);
|
||||
if(err_code) {
|
||||
BOOST_LOG(fatal) << "Couldn't create directory ["sv << cert_dir << "] :"sv << err_code.message();
|
||||
return -1;
|
||||
}
|
||||
|
||||
if(write_file(pkey.c_str(), creds.pkey)) {
|
||||
BOOST_LOG(fatal) << "Couldn't open ["sv << config::nvhttp.pkey << ']';
|
||||
return -1;
|
||||
}
|
||||
|
||||
if(write_file(cert.c_str(), creds.x509)) {
|
||||
BOOST_LOG(fatal) << "Couldn't open ["sv << config::nvhttp.cert << ']';
|
||||
return -1;
|
||||
}
|
||||
|
||||
fs::permissions(pkey_path,
|
||||
fs::perms::owner_read | fs::perms::owner_write,
|
||||
fs::perm_options::replace, err_code);
|
||||
|
||||
if(err_code) {
|
||||
BOOST_LOG(fatal) << "Couldn't change permissions of ["sv << config::nvhttp.pkey << "] :"sv << err_code.message();
|
||||
return -1;
|
||||
}
|
||||
|
||||
fs::permissions(cert_path,
|
||||
fs::perms::owner_read | fs::perms::group_read | fs::perms::others_read | fs::perms::owner_write,
|
||||
fs::perm_options::replace, err_code);
|
||||
|
||||
if(err_code) {
|
||||
BOOST_LOG(fatal) << "Couldn't change permissions of ["sv << config::nvhttp.cert << "] :"sv << err_code.message();
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void start(std::shared_ptr<safe::signal_t> shutdown_event) {
|
||||
bool clean_slate = config::sunshine.flags[config::flag::FRESH_STATE];
|
||||
if(clean_slate) {
|
||||
unique_id = util::uuid_t::generate().string();
|
||||
|
||||
auto dir = std::filesystem::temp_directory_path() / "Sushine"sv;
|
||||
|
||||
config::nvhttp.cert = (dir / ("cert-"s + unique_id)).string();
|
||||
config::nvhttp.pkey = (dir / ("pkey-"s + unique_id)).string();
|
||||
}
|
||||
|
||||
|
||||
if(!fs::exists(config::nvhttp.pkey) || !fs::exists(config::nvhttp.cert)) {
|
||||
if(create_creds(config::nvhttp.pkey, config::nvhttp.cert)) {
|
||||
shutdown_event->raise(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
origin_pin_allowed = net::from_enum_string(config::nvhttp.origin_pin_allowed);
|
||||
|
||||
if(!clean_slate) {
|
||||
load_state();
|
||||
@@ -827,22 +797,38 @@ void start(std::shared_ptr<safe::signal_t> shutdown_event) {
|
||||
|
||||
auto add_cert = std::make_shared<safe::queue_t<crypto::x509_t>>(30);
|
||||
|
||||
// Ugly hack for verifying certificates, see crypto::cert_chain_t::verify() for details
|
||||
ctx->set_verify_callback([&cert_chain, add_cert](int verified, boost::asio::ssl::verify_context &ctx) {
|
||||
ctx->set_verify_callback([](int verified, boost::asio::ssl::verify_context &ctx) {
|
||||
// To respond with an error message, a connection must be established
|
||||
return 1;
|
||||
});
|
||||
|
||||
// /resume doesn't get the parameter "localAudioPlayMode"
|
||||
// /launch will store it in host_audio
|
||||
bool host_audio {};
|
||||
|
||||
https_server_t https_server { ctx, boost::asio::ssl::verify_peer | boost::asio::ssl::verify_fail_if_no_peer_cert | boost::asio::ssl::verify_client_once };
|
||||
http_server_t http_server;
|
||||
|
||||
// Verify certificates after establishing connection
|
||||
https_server.verify = [&cert_chain, add_cert](SSL *ssl) {
|
||||
auto x509 = SSL_get_peer_certificate(ssl);
|
||||
if(!x509) {
|
||||
BOOST_LOG(info) << "unknown -- denied"sv;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int verified = 0;
|
||||
|
||||
auto fg = util::fail_guard([&]() {
|
||||
char subject_name[256];
|
||||
|
||||
auto x509 = ctx.native_handle();
|
||||
X509_NAME_oneline(X509_get_subject_name(X509_STORE_CTX_get_current_cert(x509)), subject_name, sizeof(subject_name));
|
||||
|
||||
X509_NAME_oneline(X509_get_subject_name(x509), subject_name, sizeof(subject_name));
|
||||
|
||||
|
||||
BOOST_LOG(info) << subject_name << " -- "sv << (verified ? "verfied"sv : "denied"sv);
|
||||
});
|
||||
|
||||
if(verified) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
while(add_cert->peek()) {
|
||||
char subject_name[256];
|
||||
|
||||
@@ -853,22 +839,32 @@ void start(std::shared_ptr<safe::signal_t> shutdown_event) {
|
||||
cert_chain.add(std::move(cert));
|
||||
}
|
||||
|
||||
auto err_str = cert_chain.verify(X509_STORE_CTX_get_current_cert(ctx.native_handle()));
|
||||
auto err_str = cert_chain.verify(x509);
|
||||
if(err_str) {
|
||||
BOOST_LOG(warning) << "SSL Verification error :: "sv << err_str;
|
||||
return 0;
|
||||
|
||||
return verified;
|
||||
}
|
||||
|
||||
verified = 1;
|
||||
return 1;
|
||||
});
|
||||
|
||||
// /resume doesn't get the parameter "localAudioPlayMode"
|
||||
// /launch will store it in host_audio
|
||||
bool host_audio {};
|
||||
return verified;
|
||||
};
|
||||
|
||||
https_server_t https_server { ctx, boost::asio::ssl::verify_peer | boost::asio::ssl::verify_fail_if_no_peer_cert | boost::asio::ssl::verify_client_once };
|
||||
http_server_t http_server;
|
||||
https_server.on_verify_failed = [](resp_https_t resp, req_https_t req) {
|
||||
pt::ptree tree;
|
||||
auto g = util::fail_guard([&]() {
|
||||
std::ostringstream data;
|
||||
|
||||
pt::write_xml(data, tree);
|
||||
resp->write(data.str());
|
||||
resp->close_connection_after_response = true;
|
||||
});
|
||||
|
||||
tree.put("root.<xmlattr>.status_code"s, 401);
|
||||
tree.put("root.<xmlattr>.query"s, req->path);
|
||||
tree.put("root.<xmlattr>.status_message"s, "The client is not authorized. Certificate verification failed."s);
|
||||
};
|
||||
|
||||
https_server.default_resource = not_found<SimpleWeb::HTTPS>;
|
||||
https_server.resource["^/serverinfo$"]["GET"] = serverinfo<SimpleWeb::HTTPS>;
|
||||
@@ -882,7 +878,7 @@ void start(std::shared_ptr<safe::signal_t> shutdown_event) {
|
||||
|
||||
https_server.config.reuse_address = true;
|
||||
https_server.config.address = "0.0.0.0"s;
|
||||
https_server.config.port = PORT_HTTPS;
|
||||
https_server.config.port = port_https;
|
||||
|
||||
http_server.default_resource = not_found<SimpleWeb::HTTP>;
|
||||
http_server.resource["^/serverinfo$"]["GET"] = serverinfo<SimpleWeb::HTTP>;
|
||||
@@ -891,14 +887,14 @@ void start(std::shared_ptr<safe::signal_t> shutdown_event) {
|
||||
|
||||
http_server.config.reuse_address = true;
|
||||
http_server.config.address = "0.0.0.0"s;
|
||||
http_server.config.port = PORT_HTTP;
|
||||
http_server.config.port = port_http;
|
||||
|
||||
try {
|
||||
https_server.bind();
|
||||
http_server.bind();
|
||||
}
|
||||
catch(boost::system::system_error &err) {
|
||||
BOOST_LOG(fatal) << "Couldn't bind http server to ports ["sv << PORT_HTTPS << ", "sv << PORT_HTTP << "]: "sv << err.what();
|
||||
BOOST_LOG(fatal) << "Couldn't bind http server to ports ["sv << port_http << ", "sv << port_http << "]: "sv << err.what();
|
||||
|
||||
shutdown_event->raise(true);
|
||||
return;
|
||||
@@ -914,7 +910,7 @@ void start(std::shared_ptr<safe::signal_t> shutdown_event) {
|
||||
return;
|
||||
}
|
||||
|
||||
BOOST_LOG(fatal) << "Couldn't start http server to ports ["sv << PORT_HTTPS << ", "sv << PORT_HTTP << "]: "sv << err.what();
|
||||
BOOST_LOG(fatal) << "Couldn't start http server to ports ["sv << port_https << ", "sv << port_https << "]: "sv << err.what();
|
||||
shutdown_event->raise(true);
|
||||
return;
|
||||
}
|
||||
@@ -932,30 +928,8 @@ void start(std::shared_ptr<safe::signal_t> shutdown_event) {
|
||||
tcp.join();
|
||||
}
|
||||
|
||||
int write_file(const char *path, const std::string_view &contents) {
|
||||
std::ofstream out(path);
|
||||
|
||||
if(!out.is_open()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
out << contents;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::string read_file(const char *path) {
|
||||
std::ifstream in(path);
|
||||
|
||||
std::string input;
|
||||
std::string base64_cert;
|
||||
|
||||
//FIXME: Being unable to read file could result in infinite loop
|
||||
while(!in.eof()) {
|
||||
std::getline(in, input);
|
||||
base64_cert += input + '\n';
|
||||
}
|
||||
|
||||
return base64_cert;
|
||||
void erase_all_clients(){
|
||||
map_id_client.clear();
|
||||
save_state();
|
||||
}
|
||||
} // namespace nvhttp
|
||||
|
||||
@@ -5,17 +5,16 @@
|
||||
#ifndef SUNSHINE_NVHTTP_H
|
||||
#define SUNSHINE_NVHTTP_H
|
||||
|
||||
#include <functional>
|
||||
#include "thread_safe.h"
|
||||
#include <string>
|
||||
|
||||
#include "thread_safe.h"
|
||||
|
||||
#define CA_DIR SUNSHINE_ASSETS_DIR "/demoCA"
|
||||
#define PRIVATE_KEY_FILE CA_DIR "/cakey.pem"
|
||||
#define CERTIFICATE_FILE CA_DIR "/cacert.pem"
|
||||
|
||||
namespace nvhttp {
|
||||
void start(std::shared_ptr<safe::signal_t> shutdown_event);
|
||||
}
|
||||
constexpr auto PORT_HTTP = 0;
|
||||
constexpr auto PORT_HTTPS = -5;
|
||||
|
||||
void start();
|
||||
bool pin(std::string pin);
|
||||
void erase_all_clients();
|
||||
} // namespace nvhttp
|
||||
|
||||
#endif //SUNSHINE_NVHTTP_H
|
||||
|
||||
@@ -5,12 +5,18 @@
|
||||
#ifndef SUNSHINE_COMMON_H
|
||||
#define SUNSHINE_COMMON_H
|
||||
|
||||
#include "sunshine/utility.h"
|
||||
#include <bitset>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
|
||||
#include "sunshine/thread_safe.h"
|
||||
#include "sunshine/utility.h"
|
||||
|
||||
struct sockaddr;
|
||||
struct AVFrame;
|
||||
|
||||
namespace platf {
|
||||
constexpr auto MAX_GAMEPADS = 32;
|
||||
|
||||
@@ -30,6 +36,18 @@ constexpr std::uint16_t B = 0x2000;
|
||||
constexpr std::uint16_t X = 0x4000;
|
||||
constexpr std::uint16_t Y = 0x8000;
|
||||
|
||||
struct rumble_t {
|
||||
KITTY_DEFAULT_CONSTR(rumble_t)
|
||||
|
||||
rumble_t(std::uint16_t id, std::uint16_t lowfreq, std::uint16_t highfreq)
|
||||
: id { id }, lowfreq { lowfreq }, highfreq { highfreq } {}
|
||||
|
||||
std::uint16_t id;
|
||||
std::uint16_t lowfreq;
|
||||
std::uint16_t highfreq;
|
||||
};
|
||||
using rumble_queue_t = safe::mail_raw_t::queue_t<rumble_t>;
|
||||
|
||||
namespace speaker {
|
||||
enum speaker_e {
|
||||
FRONT_LEFT,
|
||||
@@ -67,9 +85,11 @@ constexpr std::uint8_t map_surround71[] {
|
||||
};
|
||||
} // namespace speaker
|
||||
|
||||
enum class dev_type_e {
|
||||
none,
|
||||
enum class mem_type_e {
|
||||
system,
|
||||
vaapi,
|
||||
dxgi,
|
||||
cuda,
|
||||
unknown
|
||||
};
|
||||
|
||||
@@ -100,13 +120,8 @@ inline std::string_view from_pix_fmt(pix_fmt_e pix_fmt) {
|
||||
|
||||
// Dimensions for touchscreen input
|
||||
struct touch_port_t {
|
||||
std::uint32_t offset_x, offset_y;
|
||||
std::uint32_t width, height;
|
||||
|
||||
constexpr touch_port_t(
|
||||
std::uint32_t offset_x, std::uint32_t offset_y,
|
||||
std::uint32_t width, std::uint32_t height) noexcept : offset_x { offset_x }, offset_y { offset_y },
|
||||
width { width }, height { height } {};
|
||||
int offset_x, offset_y;
|
||||
int width, height;
|
||||
};
|
||||
|
||||
struct gamepad_state_t {
|
||||
@@ -126,16 +141,19 @@ public:
|
||||
|
||||
struct img_t {
|
||||
public:
|
||||
img_t() = default;
|
||||
|
||||
img_t(img_t &&) = delete;
|
||||
img_t(const img_t &) = delete;
|
||||
img_t &operator=(img_t &&) = delete;
|
||||
img_t &operator=(const img_t &) = delete;
|
||||
|
||||
std::uint8_t *data {};
|
||||
std::int32_t width {};
|
||||
std::int32_t height {};
|
||||
std::int32_t pixel_pitch {};
|
||||
std::int32_t row_pitch {};
|
||||
|
||||
img_t() = default;
|
||||
img_t(const img_t &) = delete;
|
||||
img_t(img_t &&) = delete;
|
||||
|
||||
virtual ~img_t() = default;
|
||||
};
|
||||
|
||||
@@ -155,12 +173,20 @@ struct sink_t {
|
||||
|
||||
struct hwdevice_t {
|
||||
void *data {};
|
||||
platf::img_t *img {};
|
||||
AVFrame *frame {};
|
||||
|
||||
virtual int convert(platf::img_t &img) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* implementations must take ownership of 'frame'
|
||||
*/
|
||||
virtual int set_frame(AVFrame *frame) {
|
||||
std::abort(); // ^ This function must never be called
|
||||
return -1;
|
||||
};
|
||||
|
||||
virtual void set_colorspace(std::uint32_t colorspace, std::uint32_t color_range) {};
|
||||
|
||||
virtual ~hwdevice_t() = default;
|
||||
@@ -175,13 +201,37 @@ enum class capture_e : int {
|
||||
|
||||
class display_t {
|
||||
public:
|
||||
/**
|
||||
* When display has a new image ready, this callback will be called with the new image.
|
||||
*
|
||||
* 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_t>(std::shared_ptr<img_t> &img)>;
|
||||
|
||||
display_t() noexcept : offset_x { 0 }, offset_y { 0 } {}
|
||||
virtual capture_e snapshot(img_t *img, std::chrono::milliseconds timeout, bool cursor) = 0;
|
||||
virtual std::shared_ptr<img_t> alloc_img() = 0;
|
||||
|
||||
/**
|
||||
* snapshot_cb --> the callback
|
||||
* std::shared_ptr<img_t> 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
|
||||
*/
|
||||
virtual capture_e capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr<img_t> img, bool *cursor) = 0;
|
||||
|
||||
virtual std::shared_ptr<img_t> alloc_img() = 0;
|
||||
|
||||
virtual int dummy_img(img_t *img) = 0;
|
||||
|
||||
virtual std::shared_ptr<hwdevice_t> make_hwdevice(int width, int height, pix_fmt_e pix_fmt) {
|
||||
virtual std::shared_ptr<hwdevice_t> make_hwdevice(pix_fmt_e pix_fmt) {
|
||||
return std::make_shared<hwdevice_t>();
|
||||
}
|
||||
|
||||
@@ -189,6 +239,7 @@ public:
|
||||
|
||||
// Offsets for when streaming a specific monitor. By default, they are 0.
|
||||
int offset_x, offset_y;
|
||||
int env_width, env_height;
|
||||
|
||||
int width, height;
|
||||
};
|
||||
@@ -215,13 +266,28 @@ void freeInput(void *);
|
||||
|
||||
using input_t = util::safe_ptr<void, freeInput>;
|
||||
|
||||
std::filesystem::path appdata();
|
||||
|
||||
std::string get_mac_address(const std::string_view &address);
|
||||
|
||||
std::string from_sockaddr(const sockaddr *const);
|
||||
std::pair<std::uint16_t, std::string> from_sockaddr_ex(const sockaddr *const);
|
||||
|
||||
std::unique_ptr<audio_control_t> audio_control();
|
||||
std::shared_ptr<display_t> display(dev_type_e 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.
|
||||
*
|
||||
* framerate --> The peak number of images per second
|
||||
*
|
||||
* Returns display_t based on hwdevice_type
|
||||
*/
|
||||
std::shared_ptr<display_t> display(mem_type_e hwdevice_type, const std::string &display_name, int framerate);
|
||||
|
||||
// A list of names of displays accepted as display_name with the mem_type_e
|
||||
std::vector<std::string> display_names(mem_type_e hwdevice_type);
|
||||
|
||||
input_t input();
|
||||
void move_mouse(input_t &input, int deltaX, int deltaY);
|
||||
@@ -231,10 +297,19 @@ void scroll(input_t &input, int distance);
|
||||
void keyboard(input_t &input, uint16_t modcode, bool release);
|
||||
void gamepad(input_t &input, int nr, const gamepad_state_t &gamepad_state);
|
||||
|
||||
int alloc_gamepad(input_t &input, int nr);
|
||||
int alloc_gamepad(input_t &input, int nr, rumble_queue_t rumble_queue);
|
||||
void free_gamepad(input_t &input, int nr);
|
||||
|
||||
#define SERVICE_NAME "Sunshine"
|
||||
#define SERVICE_TYPE "_nvstream._tcp"
|
||||
|
||||
namespace publish {
|
||||
[[nodiscard]] std::unique_ptr<deinit_t> start();
|
||||
}
|
||||
|
||||
[[nodiscard]] std::unique_ptr<deinit_t> init();
|
||||
|
||||
std::vector<std::string_view> &supported_gamepads();
|
||||
} // namespace platf
|
||||
|
||||
#endif //SUNSHINE_COMMON_H
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
// Created by loki on 5/16/21.
|
||||
//
|
||||
#include <bitset>
|
||||
#include <sstream>
|
||||
|
||||
#include <boost/regex.hpp>
|
||||
|
||||
#include <pulse/error.h>
|
||||
@@ -63,7 +65,7 @@ struct mic_attr_t : public mic_t {
|
||||
}
|
||||
};
|
||||
|
||||
std::unique_ptr<mic_t> microphone(const std::uint8_t *mapping, int channels, std::uint32_t sample_rate) {
|
||||
std::unique_ptr<mic_t> microphone(const std::uint8_t *mapping, int channels, std::uint32_t sample_rate, std::uint32_t frame_size) {
|
||||
auto mic = std::make_unique<mic_attr_t>();
|
||||
|
||||
pa_sample_spec ss { PA_SAMPLE_S16LE, sample_rate, (std::uint8_t)channels };
|
||||
@@ -74,6 +76,9 @@ std::unique_ptr<mic_t> microphone(const std::uint8_t *mapping, int channels, std
|
||||
channel = position_mapping[*mapping++];
|
||||
});
|
||||
|
||||
pa_buffer_attr pa_attr = {};
|
||||
pa_attr.maxlength = frame_size * 8;
|
||||
|
||||
int status;
|
||||
|
||||
const char *audio_sink = "@DEFAULT_MONITOR@";
|
||||
@@ -84,7 +89,7 @@ std::unique_ptr<mic_t> microphone(const std::uint8_t *mapping, int channels, std
|
||||
mic->mic.reset(
|
||||
pa_simple_new(nullptr, "sunshine",
|
||||
pa_stream_direction_t::PA_STREAM_RECORD, audio_sink,
|
||||
"sunshine-record", &ss, &pa_map, nullptr, &status));
|
||||
"sunshine-record", &ss, &pa_map, &pa_attr, &status));
|
||||
|
||||
if(!mic->mic) {
|
||||
auto err_str = pa_strerror(status);
|
||||
@@ -283,7 +288,7 @@ public:
|
||||
sink_t sink;
|
||||
|
||||
// If hardware sink with more channels found, set that as host
|
||||
int channels = 0;
|
||||
int channels = 0;
|
||||
// Count of all virtual sinks that are created by us
|
||||
int nullcount = 0;
|
||||
|
||||
@@ -299,9 +304,7 @@ public:
|
||||
return;
|
||||
}
|
||||
|
||||
if(sink_info->flags & PA_SINK_HARDWARE &&
|
||||
sink_info->channel_map.channels > channels) {
|
||||
|
||||
if(sink_info->active_port != nullptr) {
|
||||
sink.host = sink_info->name;
|
||||
channels = sink_info->channel_map.channels;
|
||||
}
|
||||
@@ -339,7 +342,7 @@ public:
|
||||
}
|
||||
|
||||
if(!channels) {
|
||||
BOOST_LOG(warning) << "Couldn't find hardware sink"sv;
|
||||
BOOST_LOG(warning) << "Couldn't find an active sink"sv;
|
||||
}
|
||||
|
||||
if(index.stereo == PA_INVALID_INDEX) {
|
||||
@@ -380,7 +383,7 @@ public:
|
||||
}
|
||||
|
||||
std::unique_ptr<mic_t> microphone(const std::uint8_t *mapping, int channels, std::uint32_t sample_rate, std::uint32_t frame_size) override {
|
||||
return ::platf::microphone(mapping, channels, sample_rate);
|
||||
return ::platf::microphone(mapping, channels, sample_rate, frame_size);
|
||||
}
|
||||
|
||||
int set_sink(const std::string &sink) override {
|
||||
@@ -434,8 +437,4 @@ std::unique_ptr<audio_control_t> audio_control() {
|
||||
|
||||
return audio;
|
||||
}
|
||||
|
||||
std::unique_ptr<deinit_t> init() {
|
||||
return std::make_unique<deinit_t>();
|
||||
}
|
||||
} // namespace platf
|
||||
717
sunshine/platform/linux/cuda.cpp
Normal file
717
sunshine/platform/linux/cuda.cpp
Normal file
@@ -0,0 +1,717 @@
|
||||
#include <bitset>
|
||||
|
||||
#include <NvFBC.h>
|
||||
#include <ffnvcodec/dynlink_loader.h>
|
||||
|
||||
extern "C" {
|
||||
#include <libavcodec/avcodec.h>
|
||||
#include <libavutil/hwcontext_cuda.h>
|
||||
#include <libavutil/imgutils.h>
|
||||
}
|
||||
|
||||
#include "cuda.h"
|
||||
#include "graphics.h"
|
||||
#include "sunshine/main.h"
|
||||
#include "sunshine/utility.h"
|
||||
#include "wayland.h"
|
||||
|
||||
#define SUNSHINE_STRINGVIEW_HELPER(x) x##sv
|
||||
#define SUNSHINE_STRINGVIEW(x) SUNSHINE_STRINGVIEW_HELPER(x)
|
||||
|
||||
#define CU_CHECK(x, y) \
|
||||
if(check((x), SUNSHINE_STRINGVIEW(y ": "))) return -1
|
||||
|
||||
#define CU_CHECK_IGNORE(x, y) \
|
||||
check((x), SUNSHINE_STRINGVIEW(y ": "))
|
||||
|
||||
using namespace std::literals;
|
||||
namespace cuda {
|
||||
constexpr auto cudaDevAttrMaxThreadsPerBlock = (CUdevice_attribute)1;
|
||||
constexpr auto cudaDevAttrMaxThreadsPerMultiProcessor = (CUdevice_attribute)39;
|
||||
|
||||
void pass_error(const std::string_view &sv, const char *name, const char *description) {
|
||||
BOOST_LOG(error) << sv << name << ':' << description;
|
||||
}
|
||||
|
||||
void cff(CudaFunctions *cf) {
|
||||
cuda_free_functions(&cf);
|
||||
}
|
||||
|
||||
using cdf_t = util::safe_ptr<CudaFunctions, cff>;
|
||||
|
||||
static cdf_t cdf;
|
||||
|
||||
inline static int check(CUresult result, const std::string_view &sv) {
|
||||
if(result != CUDA_SUCCESS) {
|
||||
const char *name;
|
||||
const char *description;
|
||||
|
||||
cdf->cuGetErrorName(result, &name);
|
||||
cdf->cuGetErrorString(result, &description);
|
||||
|
||||
BOOST_LOG(error) << sv << name << ':' << description;
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void freeStream(CUstream stream) {
|
||||
CU_CHECK_IGNORE(cdf->cuStreamDestroy(stream), "Couldn't destroy cuda stream");
|
||||
}
|
||||
|
||||
class img_t : public platf::img_t {
|
||||
public:
|
||||
tex_t tex;
|
||||
};
|
||||
|
||||
int init() {
|
||||
auto status = cuda_load_functions(&cdf, nullptr);
|
||||
if(status) {
|
||||
BOOST_LOG(error) << "Couldn't load cuda: "sv << status;
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
CU_CHECK(cdf->cuInit(0), "Couldn't initialize cuda");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
class cuda_t : public platf::hwdevice_t {
|
||||
public:
|
||||
int init(int in_width, int in_height) {
|
||||
if(!cdf) {
|
||||
BOOST_LOG(warning) << "cuda not initialized"sv;
|
||||
return -1;
|
||||
}
|
||||
|
||||
data = (void *)0x1;
|
||||
|
||||
width = in_width;
|
||||
height = in_height;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int set_frame(AVFrame *frame) override {
|
||||
this->hwframe.reset(frame);
|
||||
this->frame = frame;
|
||||
|
||||
auto hwframe_ctx = (AVHWFramesContext *)frame->hw_frames_ctx->data;
|
||||
if(hwframe_ctx->sw_format != AV_PIX_FMT_NV12) {
|
||||
BOOST_LOG(error) << "cuda::cuda_t doesn't support any format other than AV_PIX_FMT_NV12"sv;
|
||||
return -1;
|
||||
}
|
||||
|
||||
if(av_hwframe_get_buffer(frame->hw_frames_ctx, frame, 0)) {
|
||||
BOOST_LOG(error) << "Couldn't get hwframe for NVENC"sv;
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
auto cuda_ctx = (AVCUDADeviceContext *)hwframe_ctx->device_ctx->hwctx;
|
||||
|
||||
stream = make_stream();
|
||||
if(!stream) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
cuda_ctx->stream = stream.get();
|
||||
|
||||
auto sws_opt = sws_t::make(width, height, frame->width, frame->height, width * 4);
|
||||
if(!sws_opt) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
sws = std::move(*sws_opt);
|
||||
|
||||
linear_interpolation = width != frame->width || height != frame->height;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void set_colorspace(std::uint32_t colorspace, std::uint32_t color_range) override {
|
||||
sws.set_colorspace(colorspace, color_range);
|
||||
|
||||
auto tex = tex_t::make(height, width * 4);
|
||||
if(!tex) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The default green color is ugly.
|
||||
// Update the background color
|
||||
platf::img_t img;
|
||||
img.width = width;
|
||||
img.height = height;
|
||||
img.pixel_pitch = 4;
|
||||
img.row_pitch = img.width * img.pixel_pitch;
|
||||
|
||||
std::vector<std::uint8_t> image_data;
|
||||
image_data.resize(img.row_pitch * img.height);
|
||||
|
||||
img.data = image_data.data();
|
||||
|
||||
if(sws.load_ram(img, tex->array)) {
|
||||
return;
|
||||
}
|
||||
|
||||
sws.convert(frame->data[0], frame->data[1], frame->linesize[0], frame->linesize[1], tex->texture.linear, stream.get(), { frame->width, frame->height, 0, 0 });
|
||||
}
|
||||
|
||||
cudaTextureObject_t tex_obj(const tex_t &tex) const {
|
||||
return linear_interpolation ? tex.texture.linear : tex.texture.point;
|
||||
}
|
||||
|
||||
stream_t stream;
|
||||
frame_t hwframe;
|
||||
|
||||
int width, height;
|
||||
|
||||
// When heigth and width don't change, it's not necessary to use linear interpolation
|
||||
bool linear_interpolation;
|
||||
|
||||
sws_t sws;
|
||||
};
|
||||
|
||||
class cuda_ram_t : public cuda_t {
|
||||
public:
|
||||
int convert(platf::img_t &img) override {
|
||||
return sws.load_ram(img, tex.array) || sws.convert(frame->data[0], frame->data[1], frame->linesize[0], frame->linesize[1], tex_obj(tex), stream.get());
|
||||
}
|
||||
|
||||
int set_frame(AVFrame *frame) {
|
||||
if(cuda_t::set_frame(frame)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
auto tex_opt = tex_t::make(height, width * 4);
|
||||
if(!tex_opt) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
tex = std::move(*tex_opt);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
tex_t tex;
|
||||
};
|
||||
|
||||
class cuda_vram_t : public cuda_t {
|
||||
public:
|
||||
int convert(platf::img_t &img) override {
|
||||
return sws.convert(frame->data[0], frame->data[1], frame->linesize[0], frame->linesize[1], tex_obj(((img_t *)&img)->tex), stream.get());
|
||||
}
|
||||
};
|
||||
|
||||
std::shared_ptr<platf::hwdevice_t> make_hwdevice(int width, int height, bool vram) {
|
||||
if(init()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::shared_ptr<cuda_t> cuda;
|
||||
|
||||
if(vram) {
|
||||
cuda = std::make_shared<cuda_vram_t>();
|
||||
}
|
||||
else {
|
||||
cuda = std::make_shared<cuda_ram_t>();
|
||||
}
|
||||
|
||||
if(cuda->init(width, height)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return cuda;
|
||||
}
|
||||
|
||||
namespace nvfbc {
|
||||
static PNVFBCCREATEINSTANCE createInstance {};
|
||||
static NVFBC_API_FUNCTION_LIST func { NVFBC_VERSION };
|
||||
|
||||
static constexpr inline NVFBC_BOOL nv_bool(bool b) {
|
||||
return b ? NVFBC_TRUE : NVFBC_FALSE;
|
||||
}
|
||||
|
||||
static void *handle { nullptr };
|
||||
int init() {
|
||||
static bool funcs_loaded = false;
|
||||
|
||||
if(funcs_loaded) return 0;
|
||||
|
||||
if(!handle) {
|
||||
handle = dyn::handle({ "libnvidia-fbc.so.1", "libnvidia-fbc.so" });
|
||||
if(!handle) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::tuple<dyn::apiproc *, const char *>> funcs {
|
||||
{ (dyn::apiproc *)&createInstance, "NvFBCCreateInstance" },
|
||||
};
|
||||
|
||||
if(dyn::load(handle, funcs)) {
|
||||
dlclose(handle);
|
||||
handle = nullptr;
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
auto status = cuda::nvfbc::createInstance(&cuda::nvfbc::func);
|
||||
if(status) {
|
||||
BOOST_LOG(error) << "Unable to create NvFBC instance"sv;
|
||||
|
||||
dlclose(handle);
|
||||
handle = nullptr;
|
||||
return -1;
|
||||
}
|
||||
|
||||
funcs_loaded = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
class ctx_t {
|
||||
public:
|
||||
ctx_t(NVFBC_SESSION_HANDLE handle) {
|
||||
NVFBC_BIND_CONTEXT_PARAMS params { NVFBC_BIND_CONTEXT_PARAMS_VER };
|
||||
|
||||
if(func.nvFBCBindContext(handle, ¶ms)) {
|
||||
BOOST_LOG(error) << "Couldn't bind NvFBC context to current thread: " << func.nvFBCGetLastErrorStr(handle);
|
||||
}
|
||||
|
||||
this->handle = handle;
|
||||
}
|
||||
|
||||
~ctx_t() {
|
||||
NVFBC_RELEASE_CONTEXT_PARAMS params { NVFBC_RELEASE_CONTEXT_PARAMS_VER };
|
||||
if(func.nvFBCReleaseContext(handle, ¶ms)) {
|
||||
BOOST_LOG(error) << "Couldn't release NvFBC context from current thread: " << func.nvFBCGetLastErrorStr(handle);
|
||||
}
|
||||
}
|
||||
|
||||
NVFBC_SESSION_HANDLE handle;
|
||||
};
|
||||
|
||||
class handle_t {
|
||||
enum flag_e {
|
||||
SESSION_HANDLE,
|
||||
SESSION_CAPTURE,
|
||||
MAX_FLAGS,
|
||||
};
|
||||
|
||||
public:
|
||||
handle_t() = default;
|
||||
handle_t(handle_t &&other) : handle_flags { other.handle_flags }, handle { other.handle } {
|
||||
other.handle_flags.reset();
|
||||
}
|
||||
|
||||
handle_t &operator=(handle_t &&other) {
|
||||
std::swap(handle_flags, other.handle_flags);
|
||||
std::swap(handle, other.handle);
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
static std::optional<handle_t> make() {
|
||||
NVFBC_CREATE_HANDLE_PARAMS params { NVFBC_CREATE_HANDLE_PARAMS_VER };
|
||||
|
||||
handle_t handle;
|
||||
auto status = func.nvFBCCreateHandle(&handle.handle, ¶ms);
|
||||
if(status) {
|
||||
BOOST_LOG(error) << "Failed to create session: "sv << handle.last_error();
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
handle.handle_flags[SESSION_HANDLE] = true;
|
||||
|
||||
return std::move(handle);
|
||||
}
|
||||
|
||||
const char *last_error() {
|
||||
return func.nvFBCGetLastErrorStr(handle);
|
||||
}
|
||||
|
||||
std::optional<NVFBC_GET_STATUS_PARAMS> status() {
|
||||
NVFBC_GET_STATUS_PARAMS params { NVFBC_GET_STATUS_PARAMS_VER };
|
||||
|
||||
auto status = func.nvFBCGetStatus(handle, ¶ms);
|
||||
if(status) {
|
||||
BOOST_LOG(error) << "Failed to get NvFBC status: "sv << last_error();
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
int capture(NVFBC_CREATE_CAPTURE_SESSION_PARAMS &capture_params) {
|
||||
if(func.nvFBCCreateCaptureSession(handle, &capture_params)) {
|
||||
BOOST_LOG(error) << "Failed to start capture session: "sv << last_error();
|
||||
return -1;
|
||||
}
|
||||
|
||||
handle_flags[SESSION_CAPTURE] = true;
|
||||
|
||||
NVFBC_TOCUDA_SETUP_PARAMS setup_params {
|
||||
NVFBC_TOCUDA_SETUP_PARAMS_VER,
|
||||
NVFBC_BUFFER_FORMAT_BGRA,
|
||||
};
|
||||
|
||||
if(func.nvFBCToCudaSetUp(handle, &setup_params)) {
|
||||
BOOST_LOG(error) << "Failed to setup cuda interop with nvFBC: "sv << last_error();
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int stop() {
|
||||
if(!handle_flags[SESSION_CAPTURE]) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
NVFBC_DESTROY_CAPTURE_SESSION_PARAMS params { NVFBC_DESTROY_CAPTURE_SESSION_PARAMS_VER };
|
||||
|
||||
if(func.nvFBCDestroyCaptureSession(handle, ¶ms)) {
|
||||
BOOST_LOG(error) << "Couldn't destroy capture session: "sv << last_error();
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
handle_flags[SESSION_CAPTURE] = false;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int reset() {
|
||||
if(!handle_flags[SESSION_HANDLE]) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
stop();
|
||||
|
||||
NVFBC_DESTROY_HANDLE_PARAMS params { NVFBC_DESTROY_HANDLE_PARAMS_VER };
|
||||
|
||||
if(func.nvFBCDestroyHandle(handle, ¶ms)) {
|
||||
BOOST_LOG(error) << "Couldn't destroy session handle: "sv << func.nvFBCGetLastErrorStr(handle);
|
||||
}
|
||||
|
||||
handle_flags[SESSION_HANDLE] = false;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
~handle_t() {
|
||||
reset();
|
||||
}
|
||||
|
||||
std::bitset<MAX_FLAGS> handle_flags;
|
||||
|
||||
NVFBC_SESSION_HANDLE handle;
|
||||
};
|
||||
|
||||
class display_t : public platf::display_t {
|
||||
public:
|
||||
int init(const std::string_view &display_name, int framerate) {
|
||||
auto handle = handle_t::make();
|
||||
if(!handle) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
ctx_t ctx { handle->handle };
|
||||
|
||||
auto status_params = handle->status();
|
||||
if(!status_params) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int streamedMonitor = -1;
|
||||
if(!display_name.empty()) {
|
||||
if(status_params->bXRandRAvailable) {
|
||||
auto monitor_nr = util::from_view(display_name);
|
||||
|
||||
if(monitor_nr < 0 || monitor_nr >= status_params->dwOutputNum) {
|
||||
BOOST_LOG(warning) << "Can't stream monitor ["sv << monitor_nr << "], it needs to be between [0] and ["sv << status_params->dwOutputNum - 1 << "], defaulting to virtual desktop"sv;
|
||||
}
|
||||
else {
|
||||
streamedMonitor = monitor_nr;
|
||||
}
|
||||
}
|
||||
else {
|
||||
BOOST_LOG(warning) << "XrandR not available, streaming entire virtual desktop"sv;
|
||||
}
|
||||
}
|
||||
|
||||
delay = std::chrono::nanoseconds { 1s } / framerate;
|
||||
|
||||
capture_params = NVFBC_CREATE_CAPTURE_SESSION_PARAMS { NVFBC_CREATE_CAPTURE_SESSION_PARAMS_VER };
|
||||
|
||||
capture_params.eCaptureType = NVFBC_CAPTURE_SHARED_CUDA;
|
||||
capture_params.bDisableAutoModesetRecovery = nv_bool(true);
|
||||
|
||||
capture_params.dwSamplingRateMs = 1000 /* ms */ / framerate;
|
||||
|
||||
if(streamedMonitor != -1) {
|
||||
auto &output = status_params->outputs[streamedMonitor];
|
||||
|
||||
width = output.trackedBox.w;
|
||||
height = output.trackedBox.h;
|
||||
offset_x = output.trackedBox.x;
|
||||
offset_y = output.trackedBox.y;
|
||||
|
||||
capture_params.eTrackingType = NVFBC_TRACKING_OUTPUT;
|
||||
capture_params.dwOutputId = output.dwId;
|
||||
}
|
||||
else {
|
||||
capture_params.eTrackingType = NVFBC_TRACKING_SCREEN;
|
||||
|
||||
width = status_params->screenSize.w;
|
||||
height = status_params->screenSize.h;
|
||||
}
|
||||
|
||||
env_width = status_params->screenSize.w;
|
||||
env_height = status_params->screenSize.h;
|
||||
|
||||
this->handle = std::move(*handle);
|
||||
return 0;
|
||||
}
|
||||
|
||||
platf::capture_e capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr<platf::img_t> img, bool *cursor) override {
|
||||
auto next_frame = std::chrono::steady_clock::now();
|
||||
|
||||
// Force display_t::capture to initialize handle_t::capture
|
||||
cursor_visible = !*cursor;
|
||||
|
||||
ctx_t ctx { handle.handle };
|
||||
auto fg = util::fail_guard([&]() {
|
||||
handle.reset();
|
||||
});
|
||||
|
||||
while(img) {
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
if(next_frame > now) {
|
||||
std::this_thread::sleep_for((next_frame - now) / 3 * 2);
|
||||
}
|
||||
while(next_frame > now) {
|
||||
std::this_thread::sleep_for(1ns);
|
||||
now = std::chrono::steady_clock::now();
|
||||
}
|
||||
next_frame = now + delay;
|
||||
|
||||
auto status = snapshot(img.get(), 150ms, *cursor);
|
||||
switch(status) {
|
||||
case platf::capture_e::reinit:
|
||||
case platf::capture_e::error:
|
||||
return status;
|
||||
case platf::capture_e::timeout:
|
||||
std::this_thread::sleep_for(1ms);
|
||||
continue;
|
||||
case platf::capture_e::ok:
|
||||
img = snapshot_cb(img);
|
||||
break;
|
||||
default:
|
||||
BOOST_LOG(error) << "Unrecognized capture status ["sv << (int)status << ']';
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
return platf::capture_e::ok;
|
||||
}
|
||||
|
||||
// Reinitialize the capture session.
|
||||
platf::capture_e reinit(bool cursor) {
|
||||
if(handle.stop()) {
|
||||
return platf::capture_e::error;
|
||||
}
|
||||
|
||||
cursor_visible = cursor;
|
||||
if(cursor) {
|
||||
capture_params.bPushModel = nv_bool(false);
|
||||
capture_params.bWithCursor = nv_bool(true);
|
||||
capture_params.bAllowDirectCapture = nv_bool(false);
|
||||
}
|
||||
else {
|
||||
capture_params.bPushModel = nv_bool(true);
|
||||
capture_params.bWithCursor = nv_bool(false);
|
||||
capture_params.bAllowDirectCapture = nv_bool(true);
|
||||
}
|
||||
|
||||
if(handle.capture(capture_params)) {
|
||||
return platf::capture_e::error;
|
||||
}
|
||||
|
||||
// If trying to capture directly, test if it actually does.
|
||||
if(capture_params.bAllowDirectCapture) {
|
||||
CUdeviceptr device_ptr;
|
||||
NVFBC_FRAME_GRAB_INFO info;
|
||||
|
||||
NVFBC_TOCUDA_GRAB_FRAME_PARAMS grab {
|
||||
NVFBC_TOCUDA_GRAB_FRAME_PARAMS_VER,
|
||||
NVFBC_TOCUDA_GRAB_FLAGS_NOWAIT,
|
||||
&device_ptr,
|
||||
&info,
|
||||
0,
|
||||
};
|
||||
|
||||
// Direct Capture may fail the first few times, even if it's possible
|
||||
for(int x = 0; x < 3; ++x) {
|
||||
if(auto status = func.nvFBCToCudaGrabFrame(handle.handle, &grab)) {
|
||||
if(status == NVFBC_ERR_MUST_RECREATE) {
|
||||
return platf::capture_e::reinit;
|
||||
}
|
||||
|
||||
BOOST_LOG(error) << "Couldn't capture nvFramebuffer: "sv << handle.last_error();
|
||||
|
||||
return platf::capture_e::error;
|
||||
}
|
||||
|
||||
if(info.bDirectCapture) {
|
||||
break;
|
||||
}
|
||||
|
||||
BOOST_LOG(debug) << "Direct capture failed attempt ["sv << x << ']';
|
||||
}
|
||||
|
||||
if(!info.bDirectCapture) {
|
||||
BOOST_LOG(debug) << "Direct capture failed, trying the extra copy method"sv;
|
||||
// Direct capture failed
|
||||
capture_params.bPushModel = nv_bool(false);
|
||||
capture_params.bWithCursor = nv_bool(false);
|
||||
capture_params.bAllowDirectCapture = nv_bool(false);
|
||||
|
||||
if(handle.stop() || handle.capture(capture_params)) {
|
||||
return platf::capture_e::error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return platf::capture_e::ok;
|
||||
}
|
||||
|
||||
platf::capture_e snapshot(platf::img_t *img, std::chrono::milliseconds timeout, bool cursor) {
|
||||
if(cursor != cursor_visible) {
|
||||
auto status = reinit(cursor);
|
||||
if(status != platf::capture_e::ok) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
CUdeviceptr device_ptr;
|
||||
NVFBC_FRAME_GRAB_INFO info;
|
||||
|
||||
NVFBC_TOCUDA_GRAB_FRAME_PARAMS grab {
|
||||
NVFBC_TOCUDA_GRAB_FRAME_PARAMS_VER,
|
||||
NVFBC_TOCUDA_GRAB_FLAGS_NOWAIT,
|
||||
&device_ptr,
|
||||
&info,
|
||||
(std::uint32_t)timeout.count(),
|
||||
};
|
||||
|
||||
if(auto status = func.nvFBCToCudaGrabFrame(handle.handle, &grab)) {
|
||||
if(status == NVFBC_ERR_MUST_RECREATE) {
|
||||
return platf::capture_e::reinit;
|
||||
}
|
||||
|
||||
BOOST_LOG(error) << "Couldn't capture nvFramebuffer: "sv << handle.last_error();
|
||||
return platf::capture_e::error;
|
||||
}
|
||||
|
||||
if(((img_t *)img)->tex.copy((std::uint8_t *)device_ptr, img->height, img->row_pitch)) {
|
||||
return platf::capture_e::error;
|
||||
}
|
||||
|
||||
return platf::capture_e::ok;
|
||||
}
|
||||
|
||||
std::shared_ptr<platf::hwdevice_t> make_hwdevice(platf::pix_fmt_e pix_fmt) override {
|
||||
return ::cuda::make_hwdevice(width, height, true);
|
||||
}
|
||||
|
||||
std::shared_ptr<platf::img_t> alloc_img() override {
|
||||
auto img = std::make_shared<cuda::img_t>();
|
||||
|
||||
img->data = nullptr;
|
||||
img->width = width;
|
||||
img->height = height;
|
||||
img->pixel_pitch = 4;
|
||||
img->row_pitch = img->width * img->pixel_pitch;
|
||||
|
||||
auto tex_opt = tex_t::make(height, width * img->pixel_pitch);
|
||||
if(!tex_opt) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
img->tex = std::move(*tex_opt);
|
||||
|
||||
return img;
|
||||
};
|
||||
|
||||
int dummy_img(platf::img_t *) override {
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::chrono::nanoseconds delay;
|
||||
|
||||
bool cursor_visible;
|
||||
handle_t handle;
|
||||
|
||||
NVFBC_CREATE_CAPTURE_SESSION_PARAMS capture_params;
|
||||
};
|
||||
} // namespace nvfbc
|
||||
} // namespace cuda
|
||||
|
||||
namespace platf {
|
||||
std::shared_ptr<display_t> nvfbc_display(mem_type_e hwdevice_type, const std::string &display_name, int framerate) {
|
||||
if(hwdevice_type != mem_type_e::cuda) {
|
||||
BOOST_LOG(error) << "Could not initialize nvfbc display with the given hw device type"sv;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto display = std::make_shared<cuda::nvfbc::display_t>();
|
||||
|
||||
if(display->init(display_name, framerate)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return display;
|
||||
}
|
||||
|
||||
std::vector<std::string> nvfbc_display_names() {
|
||||
if(cuda::init() || cuda::nvfbc::init()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<std::string> display_names;
|
||||
|
||||
auto handle = cuda::nvfbc::handle_t::make();
|
||||
if(!handle) {
|
||||
return {};
|
||||
}
|
||||
|
||||
auto status_params = handle->status();
|
||||
if(!status_params) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if(!status_params->bIsCapturePossible) {
|
||||
BOOST_LOG(error) << "NVidia driver doesn't support NvFBC screencasting"sv;
|
||||
}
|
||||
|
||||
BOOST_LOG(info) << "Found ["sv << status_params->dwOutputNum << "] outputs"sv;
|
||||
BOOST_LOG(info) << "Virtual Desktop: "sv << status_params->screenSize.w << 'x' << status_params->screenSize.h;
|
||||
BOOST_LOG(info) << "XrandR: "sv << (status_params->bXRandRAvailable ? "available"sv : "unavailable"sv);
|
||||
|
||||
for(auto x = 0; x < status_params->dwOutputNum; ++x) {
|
||||
auto &output = status_params->outputs[x];
|
||||
BOOST_LOG(info) << "-- Output --"sv;
|
||||
BOOST_LOG(debug) << " ID: "sv << output.dwId;
|
||||
BOOST_LOG(debug) << " Name: "sv << output.name;
|
||||
BOOST_LOG(info) << " Resolution: "sv << output.trackedBox.w << 'x' << output.trackedBox.h;
|
||||
BOOST_LOG(info) << " Offset: "sv << output.trackedBox.x << 'x' << output.trackedBox.y;
|
||||
display_names.emplace_back(std::to_string(x));
|
||||
}
|
||||
|
||||
return display_names;
|
||||
}
|
||||
} // namespace platf
|
||||
331
sunshine/platform/linux/cuda.cu
Normal file
331
sunshine/platform/linux/cuda.cu
Normal file
@@ -0,0 +1,331 @@
|
||||
// #include <algorithm>
|
||||
#include <helper_math.h>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
|
||||
#include "cuda.h"
|
||||
|
||||
using namespace std::literals;
|
||||
|
||||
#define SUNSHINE_STRINGVIEW_HELPER(x) x##sv
|
||||
#define SUNSHINE_STRINGVIEW(x) SUNSHINE_STRINGVIEW_HELPER(x)
|
||||
|
||||
#define CU_CHECK(x, y) \
|
||||
if(check((x), SUNSHINE_STRINGVIEW(y ": "))) return -1
|
||||
|
||||
#define CU_CHECK_VOID(x, y) \
|
||||
if(check((x), SUNSHINE_STRINGVIEW(y ": "))) return;
|
||||
|
||||
#define CU_CHECK_PTR(x, y) \
|
||||
if(check((x), SUNSHINE_STRINGVIEW(y ": "))) return nullptr;
|
||||
|
||||
#define CU_CHECK_OPT(x, y) \
|
||||
if(check((x), SUNSHINE_STRINGVIEW(y ": "))) return std::nullopt;
|
||||
|
||||
#define CU_CHECK_IGNORE(x, y) \
|
||||
check((x), SUNSHINE_STRINGVIEW(y ": "))
|
||||
|
||||
using namespace std::literals;
|
||||
|
||||
//////////////////// Special desclarations
|
||||
/**
|
||||
* NVCC segfaults when including <chrono>
|
||||
* Therefore, some declarations need to be added explicitely
|
||||
*/
|
||||
namespace platf {
|
||||
struct img_t {
|
||||
public:
|
||||
std::uint8_t *data {};
|
||||
std::int32_t width {};
|
||||
std::int32_t height {};
|
||||
std::int32_t pixel_pitch {};
|
||||
std::int32_t row_pitch {};
|
||||
|
||||
virtual ~img_t() = default;
|
||||
};
|
||||
} // namespace platf
|
||||
|
||||
namespace video {
|
||||
using __float4 = float[4];
|
||||
using __float3 = float[3];
|
||||
using __float2 = float[2];
|
||||
|
||||
struct __attribute__((__aligned__(16))) color_t {
|
||||
float4 color_vec_y;
|
||||
float4 color_vec_u;
|
||||
float4 color_vec_v;
|
||||
float2 range_y;
|
||||
float2 range_uv;
|
||||
};
|
||||
|
||||
struct __attribute__((__aligned__(16))) color_extern_t {
|
||||
__float4 color_vec_y;
|
||||
__float4 color_vec_u;
|
||||
__float4 color_vec_v;
|
||||
__float2 range_y;
|
||||
__float2 range_uv;
|
||||
};
|
||||
|
||||
static_assert(sizeof(video::color_t) == sizeof(video::color_extern_t), "color matrix struct mismatch");
|
||||
|
||||
extern color_t colors[4];
|
||||
} // namespace video
|
||||
|
||||
//////////////////// End special declarations
|
||||
|
||||
namespace cuda {
|
||||
auto constexpr INVALID_TEXTURE = std::numeric_limits<cudaTextureObject_t>::max();
|
||||
|
||||
template<class T>
|
||||
inline T div_align(T l, T r) {
|
||||
return (l + r - 1) / r;
|
||||
}
|
||||
|
||||
void pass_error(const std::string_view &sv, const char *name, const char *description);
|
||||
inline static int check(cudaError_t result, const std::string_view &sv) {
|
||||
if(result) {
|
||||
auto name = cudaGetErrorName(result);
|
||||
auto description = cudaGetErrorString(result);
|
||||
|
||||
pass_error(sv, name, description);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
template<class T>
|
||||
ptr_t make_ptr() {
|
||||
void *p;
|
||||
CU_CHECK_PTR(cudaMalloc(&p, sizeof(T)), "Couldn't allocate color matrix");
|
||||
|
||||
ptr_t ptr { p };
|
||||
|
||||
return ptr;
|
||||
}
|
||||
|
||||
void freeCudaPtr_t::operator()(void *ptr) {
|
||||
CU_CHECK_IGNORE(cudaFree(ptr), "Couldn't free cuda device pointer");
|
||||
}
|
||||
|
||||
void freeCudaStream_t::operator()(cudaStream_t ptr) {
|
||||
CU_CHECK_IGNORE(cudaStreamDestroy(ptr), "Couldn't free cuda stream");
|
||||
}
|
||||
|
||||
stream_t make_stream(int flags) {
|
||||
cudaStream_t stream;
|
||||
|
||||
if(!flags) {
|
||||
CU_CHECK_PTR(cudaStreamCreate(&stream), "Couldn't create cuda stream");
|
||||
}
|
||||
else {
|
||||
CU_CHECK_PTR(cudaStreamCreateWithFlags(&stream, flags), "Couldn't create cuda stream with flags");
|
||||
}
|
||||
|
||||
return stream_t { stream };
|
||||
}
|
||||
|
||||
inline __device__ float3 bgra_to_rgb(uchar4 vec) {
|
||||
return make_float3((float)vec.z, (float)vec.y, (float)vec.x);
|
||||
}
|
||||
|
||||
inline __device__ float3 bgra_to_rgb(float4 vec) {
|
||||
return make_float3(vec.z, vec.y, vec.x);
|
||||
}
|
||||
|
||||
inline __device__ float2 calcUV(float3 pixel, const video::color_t *const color_matrix) {
|
||||
float4 vec_u = color_matrix->color_vec_u;
|
||||
float4 vec_v = color_matrix->color_vec_v;
|
||||
|
||||
float u = dot(pixel, make_float3(vec_u)) + vec_u.w;
|
||||
float v = dot(pixel, make_float3(vec_v)) + vec_v.w;
|
||||
|
||||
u = u * color_matrix->range_uv.x + color_matrix->range_uv.y;
|
||||
v = (v * color_matrix->range_uv.x + color_matrix->range_uv.y) * 224.0f / 256.0f + 0.0625f;
|
||||
|
||||
return make_float2(u, v);
|
||||
}
|
||||
|
||||
inline __device__ float calcY(float3 pixel, const video::color_t *const color_matrix) {
|
||||
float4 vec_y = color_matrix->color_vec_y;
|
||||
|
||||
return (dot(pixel, make_float3(vec_y)) + vec_y.w) * color_matrix->range_y.x + color_matrix->range_y.y;
|
||||
}
|
||||
|
||||
__global__ void RGBA_to_NV12(
|
||||
cudaTextureObject_t srcImage, std::uint8_t *dstY, std::uint8_t *dstUV,
|
||||
std::uint32_t dstPitchY, std::uint32_t dstPitchUV,
|
||||
float scale, const viewport_t viewport, const video::color_t *const color_matrix) {
|
||||
|
||||
int idX = (threadIdx.x + blockDim.x * blockIdx.x) * 2;
|
||||
int idY = (threadIdx.y + blockDim.y * blockIdx.y);
|
||||
|
||||
if(idX >= viewport.width) return;
|
||||
if(idY >= viewport.height) return;
|
||||
|
||||
float x = idX * scale;
|
||||
float y = idY * scale;
|
||||
|
||||
idX += viewport.offsetX;
|
||||
idY += viewport.offsetY;
|
||||
|
||||
dstY = dstY + idX + idY * dstPitchY;
|
||||
dstUV = dstUV + idX + (idY / 2 * dstPitchUV);
|
||||
|
||||
float3 rgb_l = bgra_to_rgb(tex2D<float4>(srcImage, x, y));
|
||||
float3 rgb_r = bgra_to_rgb(tex2D<float4>(srcImage, x + scale, y));
|
||||
|
||||
float2 uv = calcUV((rgb_l + rgb_r) * 0.5f, color_matrix) * 256.0f;
|
||||
|
||||
dstUV[0] = uv.x;
|
||||
dstUV[1] = uv.y;
|
||||
dstY[0] = calcY(rgb_l, color_matrix) * 245.0f; // 245.0f is a magic number to ensure slight changes in luminosity are more visisble
|
||||
dstY[1] = calcY(rgb_r, color_matrix) * 245.0f; // 245.0f is a magic number to ensure slight changes in luminosity are more visisble
|
||||
}
|
||||
|
||||
int tex_t::copy(std::uint8_t *src, int height, int pitch) {
|
||||
CU_CHECK(cudaMemcpy2DToArray(array, 0, 0, src, pitch, pitch, height, cudaMemcpyDeviceToDevice), "Couldn't copy to cuda array from deviceptr");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::optional<tex_t> tex_t::make(int height, int pitch) {
|
||||
tex_t tex;
|
||||
|
||||
auto format = cudaCreateChannelDesc<uchar4>();
|
||||
CU_CHECK_OPT(cudaMallocArray(&tex.array, &format, pitch, height, cudaArrayDefault), "Couldn't allocate cuda array");
|
||||
|
||||
cudaResourceDesc res {};
|
||||
res.resType = cudaResourceTypeArray;
|
||||
res.res.array.array = tex.array;
|
||||
|
||||
cudaTextureDesc desc {};
|
||||
|
||||
desc.readMode = cudaReadModeNormalizedFloat;
|
||||
desc.filterMode = cudaFilterModePoint;
|
||||
desc.normalizedCoords = false;
|
||||
|
||||
std::fill_n(std::begin(desc.addressMode), 2, cudaAddressModeClamp);
|
||||
|
||||
CU_CHECK_OPT(cudaCreateTextureObject(&tex.texture.point, &res, &desc, nullptr), "Couldn't create cuda texture that uses point interpolation");
|
||||
|
||||
desc.filterMode = cudaFilterModeLinear;
|
||||
|
||||
CU_CHECK_OPT(cudaCreateTextureObject(&tex.texture.linear, &res, &desc, nullptr), "Couldn't create cuda texture that uses linear interpolation");
|
||||
|
||||
return std::move(tex);
|
||||
}
|
||||
|
||||
tex_t::tex_t() : array {}, texture { INVALID_TEXTURE } {}
|
||||
tex_t::tex_t(tex_t &&other) : array { other.array }, texture { other.texture } {
|
||||
other.array = 0;
|
||||
other.texture.point = INVALID_TEXTURE;
|
||||
other.texture.linear = INVALID_TEXTURE;
|
||||
}
|
||||
|
||||
tex_t &tex_t::operator=(tex_t &&other) {
|
||||
std::swap(array, other.array);
|
||||
std::swap(texture, other.texture);
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
tex_t::~tex_t() {
|
||||
if(texture.point != INVALID_TEXTURE) {
|
||||
CU_CHECK_IGNORE(cudaDestroyTextureObject(texture.point), "Couldn't deallocate cuda texture that uses point interpolation");
|
||||
|
||||
texture.point = INVALID_TEXTURE;
|
||||
}
|
||||
|
||||
if(texture.linear != INVALID_TEXTURE) {
|
||||
CU_CHECK_IGNORE(cudaDestroyTextureObject(texture.linear), "Couldn't deallocate cuda texture that uses linear interpolation");
|
||||
|
||||
texture.linear = INVALID_TEXTURE;
|
||||
}
|
||||
|
||||
if(array) {
|
||||
CU_CHECK_IGNORE(cudaFreeArray(array), "Couldn't deallocate cuda array");
|
||||
|
||||
array = cudaArray_t {};
|
||||
}
|
||||
}
|
||||
|
||||
sws_t::sws_t(int in_width, int in_height, int out_width, int out_height, int pitch, int threadsPerBlock, ptr_t &&color_matrix)
|
||||
: threadsPerBlock { threadsPerBlock }, color_matrix { std::move(color_matrix) } {
|
||||
// Ensure aspect ratio is maintained
|
||||
auto scalar = std::fminf(out_width / (float)in_width, out_height / (float)in_height);
|
||||
auto out_width_f = in_width * scalar;
|
||||
auto out_height_f = in_height * scalar;
|
||||
|
||||
// result is always positive
|
||||
auto offsetX_f = (out_width - out_width_f) / 2;
|
||||
auto offsetY_f = (out_height - out_height_f) / 2;
|
||||
|
||||
viewport.width = out_width_f;
|
||||
viewport.height = out_height_f;
|
||||
|
||||
viewport.offsetX = offsetX_f;
|
||||
viewport.offsetY = offsetY_f;
|
||||
|
||||
scale = 1.0f / scalar;
|
||||
}
|
||||
|
||||
std::optional<sws_t> sws_t::make(int in_width, int in_height, int out_width, int out_height, int pitch) {
|
||||
cudaDeviceProp props;
|
||||
int device;
|
||||
CU_CHECK_OPT(cudaGetDevice(&device), "Couldn't get cuda device");
|
||||
CU_CHECK_OPT(cudaGetDeviceProperties(&props, device), "Couldn't get cuda device properties");
|
||||
|
||||
auto ptr = make_ptr<video::color_t>();
|
||||
if(!ptr) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return std::make_optional<sws_t>(in_width, in_height, out_width, out_height, pitch, props.maxThreadsPerMultiProcessor / props.maxBlocksPerMultiProcessor, std::move(ptr));
|
||||
}
|
||||
|
||||
int sws_t::convert(std::uint8_t *Y, std::uint8_t *UV, std::uint32_t pitchY, std::uint32_t pitchUV, cudaTextureObject_t texture, stream_t::pointer stream) {
|
||||
return convert(Y, UV, pitchY, pitchUV, texture, stream, viewport);
|
||||
}
|
||||
|
||||
int sws_t::convert(std::uint8_t *Y, std::uint8_t *UV, std::uint32_t pitchY, std::uint32_t pitchUV, cudaTextureObject_t texture, stream_t::pointer stream, const viewport_t &viewport) {
|
||||
int threadsX = viewport.width / 2;
|
||||
int threadsY = viewport.height;
|
||||
|
||||
dim3 block(threadsPerBlock);
|
||||
dim3 grid(div_align(threadsX, threadsPerBlock), threadsY);
|
||||
|
||||
RGBA_to_NV12<<<grid, block, 0, stream>>>(texture, Y, UV, pitchY, pitchUV, scale, viewport, (video::color_t *)color_matrix.get());
|
||||
|
||||
return CU_CHECK_IGNORE(cudaGetLastError(), "RGBA_to_NV12 failed");
|
||||
}
|
||||
|
||||
void sws_t::set_colorspace(std::uint32_t colorspace, std::uint32_t color_range) {
|
||||
video::color_t *color_p;
|
||||
switch(colorspace) {
|
||||
case 5: // SWS_CS_SMPTE170M
|
||||
color_p = &video::colors[0];
|
||||
break;
|
||||
case 1: // SWS_CS_ITU709
|
||||
color_p = &video::colors[2];
|
||||
break;
|
||||
case 9: // SWS_CS_BT2020
|
||||
default:
|
||||
color_p = &video::colors[0];
|
||||
};
|
||||
|
||||
if(color_range > 1) {
|
||||
// Full range
|
||||
++color_p;
|
||||
}
|
||||
|
||||
CU_CHECK_IGNORE(cudaMemcpy(color_matrix.get(), color_p, sizeof(video::color_t), cudaMemcpyHostToDevice), "Couldn't copy color matrix to cuda");
|
||||
}
|
||||
|
||||
int sws_t::load_ram(platf::img_t &img, cudaArray_t array) {
|
||||
return CU_CHECK_IGNORE(cudaMemcpy2DToArray(array, 0, 0, img.data, img.row_pitch, img.width * img.pixel_pitch, img.height, cudaMemcpyHostToDevice), "Couldn't copy to cuda array");
|
||||
}
|
||||
|
||||
} // namespace cuda
|
||||
107
sunshine/platform/linux/cuda.h
Normal file
107
sunshine/platform/linux/cuda.h
Normal file
@@ -0,0 +1,107 @@
|
||||
#if !defined(SUNSHINE_PLATFORM_CUDA_H) && defined(SUNSHINE_BUILD_CUDA)
|
||||
#define SUNSHINE_PLATFORM_CUDA_H
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace platf {
|
||||
class hwdevice_t;
|
||||
class img_t;
|
||||
} // namespace platf
|
||||
|
||||
namespace cuda {
|
||||
|
||||
namespace nvfbc {
|
||||
std::vector<std::string> display_names();
|
||||
}
|
||||
std::shared_ptr<platf::hwdevice_t> make_hwdevice(int width, int height, bool vram);
|
||||
int init();
|
||||
} // namespace cuda
|
||||
|
||||
typedef struct cudaArray *cudaArray_t;
|
||||
|
||||
#if !defined(__CUDACC__)
|
||||
typedef struct CUstream_st *cudaStream_t;
|
||||
typedef unsigned long long cudaTextureObject_t;
|
||||
#else /* defined(__CUDACC__) */
|
||||
typedef __location__(device_builtin) struct CUstream_st *cudaStream_t;
|
||||
typedef __location__(device_builtin) unsigned long long cudaTextureObject_t;
|
||||
#endif /* !defined(__CUDACC__) */
|
||||
|
||||
namespace cuda {
|
||||
|
||||
class freeCudaPtr_t {
|
||||
public:
|
||||
void operator()(void *ptr);
|
||||
};
|
||||
|
||||
class freeCudaStream_t {
|
||||
public:
|
||||
void operator()(cudaStream_t ptr);
|
||||
};
|
||||
|
||||
using ptr_t = std::unique_ptr<void, freeCudaPtr_t>;
|
||||
using stream_t = std::unique_ptr<CUstream_st, freeCudaStream_t>;
|
||||
|
||||
stream_t make_stream(int flags = 0);
|
||||
|
||||
struct viewport_t {
|
||||
int width, height;
|
||||
int offsetX, offsetY;
|
||||
};
|
||||
|
||||
class tex_t {
|
||||
public:
|
||||
static std::optional<tex_t> make(int height, int pitch);
|
||||
|
||||
tex_t();
|
||||
tex_t(tex_t &&);
|
||||
|
||||
tex_t &operator=(tex_t &&other);
|
||||
|
||||
~tex_t();
|
||||
|
||||
int copy(std::uint8_t *src, int height, int pitch);
|
||||
|
||||
cudaArray_t array;
|
||||
|
||||
struct texture {
|
||||
cudaTextureObject_t point;
|
||||
cudaTextureObject_t linear;
|
||||
} texture;
|
||||
};
|
||||
|
||||
class sws_t {
|
||||
public:
|
||||
sws_t() = default;
|
||||
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
|
||||
*/
|
||||
static std::optional<sws_t> make(int in_width, int in_height, int out_width, int out_height, int pitch);
|
||||
|
||||
// Converts loaded image into a CUDevicePtr
|
||||
int convert(std::uint8_t *Y, std::uint8_t *UV, std::uint32_t pitchY, std::uint32_t pitchUV, cudaTextureObject_t texture, stream_t::pointer stream);
|
||||
int convert(std::uint8_t *Y, std::uint8_t *UV, std::uint32_t pitchY, std::uint32_t pitchUV, cudaTextureObject_t texture, stream_t::pointer stream, const viewport_t &viewport);
|
||||
|
||||
void set_colorspace(std::uint32_t colorspace, std::uint32_t color_range);
|
||||
|
||||
int load_ram(platf::img_t &img, cudaArray_t array);
|
||||
|
||||
ptr_t color_matrix;
|
||||
|
||||
int threadsPerBlock;
|
||||
|
||||
viewport_t viewport;
|
||||
|
||||
float scale;
|
||||
};
|
||||
} // namespace cuda
|
||||
|
||||
#endif
|
||||
@@ -1,454 +0,0 @@
|
||||
//
|
||||
// Created by loki on 6/21/19.
|
||||
//
|
||||
|
||||
#include "sunshine/platform/common.h"
|
||||
|
||||
#include <fstream>
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <ifaddrs.h>
|
||||
|
||||
#include <X11/X.h>
|
||||
#include <X11/Xlib.h>
|
||||
#include <X11/Xutil.h>
|
||||
#include <X11/extensions/Xfixes.h>
|
||||
#include <X11/extensions/Xrandr.h>
|
||||
#include <sys/ipc.h>
|
||||
#include <sys/shm.h>
|
||||
#include <xcb/shm.h>
|
||||
#include <xcb/xfixes.h>
|
||||
|
||||
#include "sunshine/config.h"
|
||||
#include "sunshine/main.h"
|
||||
#include "sunshine/task_pool.h"
|
||||
|
||||
namespace platf {
|
||||
using namespace std::literals;
|
||||
|
||||
void freeImage(XImage *);
|
||||
void freeX(XFixesCursorImage *);
|
||||
|
||||
using ifaddr_t = util::safe_ptr<ifaddrs, freeifaddrs>;
|
||||
using xcb_connect_t = util::safe_ptr<xcb_connection_t, xcb_disconnect>;
|
||||
using xcb_img_t = util::c_ptr<xcb_shm_get_image_reply_t>;
|
||||
|
||||
using xdisplay_t = util::safe_ptr_v2<Display, int, XCloseDisplay>;
|
||||
using ximg_t = util::safe_ptr<XImage, freeImage>;
|
||||
using xcursor_t = util::safe_ptr<XFixesCursorImage, freeX>;
|
||||
|
||||
using crtc_info_t = util::safe_ptr<_XRRCrtcInfo, XRRFreeCrtcInfo>;
|
||||
using output_info_t = util::safe_ptr<_XRROutputInfo, XRRFreeOutputInfo>;
|
||||
using screen_res_t = util::safe_ptr<_XRRScreenResources, XRRFreeScreenResources>;
|
||||
|
||||
class shm_id_t {
|
||||
public:
|
||||
shm_id_t() : id { -1 } {}
|
||||
shm_id_t(int id) : id { id } {}
|
||||
shm_id_t(shm_id_t &&other) noexcept : id(other.id) {
|
||||
other.id = -1;
|
||||
}
|
||||
|
||||
~shm_id_t() {
|
||||
if(id != -1) {
|
||||
shmctl(id, IPC_RMID, nullptr);
|
||||
id = -1;
|
||||
}
|
||||
}
|
||||
int id;
|
||||
};
|
||||
|
||||
class shm_data_t {
|
||||
public:
|
||||
shm_data_t() : data { (void *)-1 } {}
|
||||
shm_data_t(void *data) : data { data } {}
|
||||
|
||||
shm_data_t(shm_data_t &&other) noexcept : data(other.data) {
|
||||
other.data = (void *)-1;
|
||||
}
|
||||
|
||||
~shm_data_t() {
|
||||
if((std::uintptr_t)data != -1) {
|
||||
shmdt(data);
|
||||
}
|
||||
}
|
||||
|
||||
void *data;
|
||||
};
|
||||
|
||||
struct x11_img_t : public img_t {
|
||||
ximg_t img;
|
||||
};
|
||||
|
||||
struct shm_img_t : public img_t {
|
||||
~shm_img_t() override {
|
||||
delete[] data;
|
||||
data = nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
void blend_cursor(Display *display, img_t &img, int offsetX, int offsetY) {
|
||||
xcursor_t overlay { XFixesGetCursorImage(display) };
|
||||
|
||||
if(!overlay) {
|
||||
BOOST_LOG(error) << "Couldn't get cursor from XFixesGetCursorImage"sv;
|
||||
return;
|
||||
}
|
||||
|
||||
overlay->x -= overlay->xhot;
|
||||
overlay->y -= overlay->yhot;
|
||||
|
||||
overlay->x -= offsetX;
|
||||
overlay->y -= offsetY;
|
||||
|
||||
overlay->x = std::max((short)0, overlay->x);
|
||||
overlay->y = std::max((short)0, overlay->y);
|
||||
|
||||
auto pixels = (int *)img.data;
|
||||
|
||||
auto screen_height = img.height;
|
||||
auto screen_width = img.width;
|
||||
|
||||
auto delta_height = std::min<uint16_t>(overlay->height, std::max(0, screen_height - overlay->y));
|
||||
auto delta_width = std::min<uint16_t>(overlay->width, std::max(0, screen_width - overlay->x));
|
||||
for(auto y = 0; y < delta_height; ++y) {
|
||||
auto overlay_begin = &overlay->pixels[y * overlay->width];
|
||||
auto overlay_end = &overlay->pixels[y * overlay->width + delta_width];
|
||||
|
||||
auto pixels_begin = &pixels[(y + overlay->y) * (img.row_pitch / img.pixel_pitch) + overlay->x];
|
||||
|
||||
std::for_each(overlay_begin, overlay_end, [&](long pixel) {
|
||||
int *pixel_p = (int *)&pixel;
|
||||
|
||||
auto colors_in = (uint8_t *)pixels_begin;
|
||||
|
||||
auto alpha = (*(uint *)pixel_p) >> 24u;
|
||||
if(alpha == 255) {
|
||||
*pixels_begin = *pixel_p;
|
||||
}
|
||||
else {
|
||||
auto colors_out = (uint8_t *)pixel_p;
|
||||
colors_in[0] = colors_out[0] + (colors_in[0] * (255 - alpha) + 255 / 2) / 255;
|
||||
colors_in[1] = colors_out[1] + (colors_in[1] * (255 - alpha) + 255 / 2) / 255;
|
||||
colors_in[2] = colors_out[2] + (colors_in[2] * (255 - alpha) + 255 / 2) / 255;
|
||||
}
|
||||
++pixels_begin;
|
||||
});
|
||||
}
|
||||
}
|
||||
struct x11_attr_t : public display_t {
|
||||
xdisplay_t xdisplay;
|
||||
Window xwindow;
|
||||
XWindowAttributes xattr;
|
||||
|
||||
/*
|
||||
* Last X (NOT the streamed monitor!) size.
|
||||
* This way we can trigger reinitialization if the dimensions changed while streaming
|
||||
*/
|
||||
int lastWidth, lastHeight;
|
||||
|
||||
x11_attr_t() : xdisplay { XOpenDisplay(nullptr) }, xwindow {}, xattr {} {
|
||||
XInitThreads();
|
||||
}
|
||||
|
||||
int init() {
|
||||
if(!xdisplay) {
|
||||
BOOST_LOG(error) << "Could not open X11 display"sv;
|
||||
return -1;
|
||||
}
|
||||
|
||||
xwindow = DefaultRootWindow(xdisplay.get());
|
||||
|
||||
refresh();
|
||||
|
||||
int streamedMonitor = -1;
|
||||
if(!config::video.output_name.empty()) {
|
||||
streamedMonitor = (int)util::from_view(config::video.output_name);
|
||||
}
|
||||
|
||||
if(streamedMonitor != -1) {
|
||||
BOOST_LOG(info) << "Configuring selected monitor ("sv << streamedMonitor << ") to stream"sv;
|
||||
screen_res_t screenr { XRRGetScreenResources(xdisplay.get(), xwindow) };
|
||||
int output = screenr->noutput;
|
||||
|
||||
if(streamedMonitor >= output) {
|
||||
BOOST_LOG(error) << "Could not stream display number ["sv << streamedMonitor << "], there are only ["sv << output << "] displays."sv;
|
||||
return -1;
|
||||
}
|
||||
|
||||
output_info_t out_info { XRRGetOutputInfo(xdisplay.get(), screenr.get(), screenr->outputs[streamedMonitor]) };
|
||||
if(!out_info || out_info->connection != RR_Connected) {
|
||||
BOOST_LOG(error) << "Could not stream selected display because it doesn't seem to be connected"sv;
|
||||
return -1;
|
||||
}
|
||||
|
||||
crtc_info_t crt_info { XRRGetCrtcInfo(xdisplay.get(), screenr.get(), out_info->crtc) };
|
||||
BOOST_LOG(info)
|
||||
<< "Streaming display: "sv << out_info->name << " with res "sv << crt_info->width << 'x' << crt_info->height << " offset by "sv << crt_info->x << 'x' << crt_info->y;
|
||||
|
||||
width = crt_info->width;
|
||||
height = crt_info->height;
|
||||
offset_x = crt_info->x;
|
||||
offset_y = crt_info->y;
|
||||
}
|
||||
else {
|
||||
width = xattr.width;
|
||||
height = xattr.height;
|
||||
}
|
||||
|
||||
lastWidth = xattr.width;
|
||||
lastHeight = xattr.height;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the display attributes should change.
|
||||
*/
|
||||
void refresh() {
|
||||
XGetWindowAttributes(xdisplay.get(), xwindow, &xattr); //Update xattr's
|
||||
}
|
||||
|
||||
capture_e snapshot(img_t *img_out_base, std::chrono::milliseconds timeout, bool cursor) override {
|
||||
refresh();
|
||||
|
||||
//The whole X server changed, so we gotta reinit everything
|
||||
if(xattr.width != lastWidth || xattr.height != lastHeight) {
|
||||
BOOST_LOG(warning) << "X dimensions changed in non-SHM mode, request reinit"sv;
|
||||
return capture_e::reinit;
|
||||
}
|
||||
XImage *img { XGetImage(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(cursor) {
|
||||
blend_cursor(xdisplay.get(), *img_out_base, offset_x, offset_y);
|
||||
}
|
||||
|
||||
return capture_e::ok;
|
||||
}
|
||||
|
||||
std::shared_ptr<img_t> alloc_img() override {
|
||||
return std::make_shared<x11_img_t>();
|
||||
}
|
||||
|
||||
int dummy_img(img_t *img) override {
|
||||
snapshot(img, 0s, true);
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
struct shm_attr_t : public x11_attr_t {
|
||||
xdisplay_t shm_xdisplay; // Prevent race condition with x11_attr_t::xdisplay
|
||||
xcb_connect_t xcb;
|
||||
xcb_screen_t *display;
|
||||
std::uint32_t seg;
|
||||
|
||||
shm_id_t shm_id;
|
||||
|
||||
shm_data_t data;
|
||||
|
||||
util::TaskPool::task_id_t refresh_task_id;
|
||||
|
||||
void delayed_refresh() {
|
||||
refresh();
|
||||
|
||||
refresh_task_id = task_pool.pushDelayed(&shm_attr_t::delayed_refresh, 2s, this).task_id;
|
||||
}
|
||||
|
||||
shm_attr_t() : x11_attr_t(), shm_xdisplay { XOpenDisplay(nullptr) } {
|
||||
refresh_task_id = task_pool.pushDelayed(&shm_attr_t::delayed_refresh, 2s, this).task_id;
|
||||
}
|
||||
|
||||
~shm_attr_t() override {
|
||||
while(!task_pool.cancel(refresh_task_id))
|
||||
;
|
||||
}
|
||||
|
||||
capture_e snapshot(img_t *img, std::chrono::milliseconds timeout, bool cursor) override {
|
||||
//The whole X server changed, so we gotta reinit everything
|
||||
if(xattr.width != lastWidth || xattr.height != lastHeight) {
|
||||
BOOST_LOG(warning) << "X dimensions changed in SHM mode, request reinit"sv;
|
||||
return capture_e::reinit;
|
||||
}
|
||||
else {
|
||||
auto img_cookie = xcb_shm_get_image_unchecked(xcb.get(), display->root, offset_x, offset_y, width, height, ~0, XCB_IMAGE_FORMAT_Z_PIXMAP, seg, 0);
|
||||
|
||||
xcb_img_t img_reply { xcb_shm_get_image_reply(xcb.get(), img_cookie, nullptr) };
|
||||
if(!img_reply) {
|
||||
BOOST_LOG(error) << "Could not get image reply"sv;
|
||||
return capture_e::reinit;
|
||||
}
|
||||
|
||||
std::copy_n((std::uint8_t *)data.data, frame_size(), img->data);
|
||||
|
||||
if(cursor) {
|
||||
blend_cursor(shm_xdisplay.get(), *img, offset_x, offset_y);
|
||||
}
|
||||
|
||||
return capture_e::ok;
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<img_t> alloc_img() override {
|
||||
auto img = std::make_shared<shm_img_t>();
|
||||
img->width = width;
|
||||
img->height = height;
|
||||
img->pixel_pitch = 4;
|
||||
img->row_pitch = img->pixel_pitch * width;
|
||||
img->data = new std::uint8_t[height * img->row_pitch];
|
||||
|
||||
return img;
|
||||
}
|
||||
|
||||
int dummy_img(platf::img_t *img) override {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int init() {
|
||||
if(x11_attr_t::init()) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
shm_xdisplay.reset(XOpenDisplay(nullptr));
|
||||
xcb.reset(xcb_connect(nullptr, nullptr));
|
||||
if(xcb_connection_has_error(xcb.get())) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if(!xcb_get_extension_data(xcb.get(), &xcb_shm_id)->present) {
|
||||
BOOST_LOG(error) << "Missing SHM extension"sv;
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
auto iter = xcb_setup_roots_iterator(xcb_get_setup(xcb.get()));
|
||||
display = iter.data;
|
||||
seg = xcb_generate_id(xcb.get());
|
||||
|
||||
shm_id.id = shmget(IPC_PRIVATE, frame_size(), IPC_CREAT | 0777);
|
||||
if(shm_id.id == -1) {
|
||||
BOOST_LOG(error) << "shmget failed"sv;
|
||||
return -1;
|
||||
}
|
||||
|
||||
xcb_shm_attach(xcb.get(), seg, shm_id.id, false);
|
||||
data.data = shmat(shm_id.id, nullptr, 0);
|
||||
|
||||
if((uintptr_t)data.data == -1) {
|
||||
BOOST_LOG(error) << "shmat failed"sv;
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::uint32_t frame_size() {
|
||||
return width * height * 4;
|
||||
}
|
||||
};
|
||||
|
||||
std::shared_ptr<display_t> display(platf::dev_type_e hwdevice_type) {
|
||||
if(hwdevice_type != platf::dev_type_e::none) {
|
||||
BOOST_LOG(error) << "Could not initialize display with the given hw device type."sv;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Attempt to use shared memory X11 to avoid copying the frame
|
||||
auto shm_disp = std::make_shared<shm_attr_t>();
|
||||
|
||||
auto status = shm_disp->init();
|
||||
if(status > 0) {
|
||||
// x11_attr_t::init() failed, don't bother trying again.
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if(status == 0) {
|
||||
return shm_disp;
|
||||
}
|
||||
|
||||
// Fallback
|
||||
auto x11_disp = std::make_shared<x11_attr_t>();
|
||||
if(x11_disp->init()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return x11_disp;
|
||||
}
|
||||
|
||||
ifaddr_t get_ifaddrs() {
|
||||
ifaddrs *p { nullptr };
|
||||
|
||||
getifaddrs(&p);
|
||||
|
||||
return ifaddr_t { p };
|
||||
}
|
||||
|
||||
std::string from_sockaddr(const sockaddr *const ip_addr) {
|
||||
char data[INET6_ADDRSTRLEN];
|
||||
|
||||
auto family = ip_addr->sa_family;
|
||||
if(family == AF_INET6) {
|
||||
inet_ntop(AF_INET6, &((sockaddr_in6 *)ip_addr)->sin6_addr, data,
|
||||
INET6_ADDRSTRLEN);
|
||||
}
|
||||
|
||||
if(family == AF_INET) {
|
||||
inet_ntop(AF_INET, &((sockaddr_in *)ip_addr)->sin_addr, data,
|
||||
INET_ADDRSTRLEN);
|
||||
}
|
||||
|
||||
return std::string { data };
|
||||
}
|
||||
|
||||
std::pair<std::uint16_t, std::string> from_sockaddr_ex(const sockaddr *const ip_addr) {
|
||||
char data[INET6_ADDRSTRLEN];
|
||||
|
||||
auto family = ip_addr->sa_family;
|
||||
std::uint16_t port;
|
||||
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) {
|
||||
inet_ntop(AF_INET, &((sockaddr_in *)ip_addr)->sin_addr, data,
|
||||
INET_ADDRSTRLEN);
|
||||
port = ((sockaddr_in *)ip_addr)->sin_port;
|
||||
}
|
||||
|
||||
return { port, std::string { data } };
|
||||
}
|
||||
|
||||
std::string get_mac_address(const std::string_view &address) {
|
||||
auto ifaddrs = get_ifaddrs();
|
||||
for(auto pos = ifaddrs.get(); pos != nullptr; pos = pos->ifa_next) {
|
||||
if(pos->ifa_addr && address == from_sockaddr(pos->ifa_addr)) {
|
||||
std::ifstream mac_file("/sys/class/net/"s + pos->ifa_name + "/address");
|
||||
if(mac_file.good()) {
|
||||
std::string mac_address;
|
||||
std::getline(mac_file, mac_address);
|
||||
return mac_address;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_LOG(warning) << "Unable to find MAC address for "sv << address;
|
||||
return "00:00:00:00:00:00"s;
|
||||
}
|
||||
|
||||
void freeImage(XImage *p) {
|
||||
XDestroyImage(p);
|
||||
}
|
||||
void freeX(XFixesCursorImage *p) {
|
||||
XFree(p);
|
||||
}
|
||||
} // namespace platf
|
||||
866
sunshine/platform/linux/graphics.cpp
Normal file
866
sunshine/platform/linux/graphics.cpp
Normal file
@@ -0,0 +1,866 @@
|
||||
#include "graphics.h"
|
||||
#include "sunshine/video.h"
|
||||
|
||||
#include <fcntl.h>
|
||||
|
||||
// I want to have as little build dependencies as possible
|
||||
// There aren't that many DRM_FORMAT I need to use, so define them here
|
||||
//
|
||||
// They aren't likely to change any time soon.
|
||||
#define fourcc_code(a, b, c, d) ((std::uint32_t)(a) | ((std::uint32_t)(b) << 8) | \
|
||||
((std::uint32_t)(c) << 16) | ((std::uint32_t)(d) << 24))
|
||||
#define fourcc_mod_code(vendor, val) ((((uint64_t)vendor) << 56) | ((val)&0x00ffffffffffffffULL))
|
||||
#define DRM_FORMAT_R8 fourcc_code('R', '8', ' ', ' ') /* [7:0] R */
|
||||
#define DRM_FORMAT_GR88 fourcc_code('G', 'R', '8', '8') /* [15:0] G:R 8:8 little endian */
|
||||
#define DRM_FORMAT_ARGB8888 fourcc_code('A', 'R', '2', '4') /* [31:0] A:R:G:B 8:8:8:8 little endian */
|
||||
#define DRM_FORMAT_XRGB8888 fourcc_code('X', 'R', '2', '4') /* [31:0] x:R:G:B 8:8:8:8 little endian */
|
||||
#define DRM_FORMAT_XBGR8888 fourcc_code('X', 'B', '2', '4') /* [31:0] x:B:G:R 8:8:8:8 little endian */
|
||||
#define DRM_FORMAT_MOD_INVALID fourcc_mod_code(0, ((1ULL << 56) - 1))
|
||||
|
||||
#define SUNSHINE_SHADERS_DIR SUNSHINE_ASSETS_DIR "/shaders/opengl"
|
||||
|
||||
using namespace std::literals;
|
||||
namespace gl {
|
||||
GladGLContext ctx;
|
||||
|
||||
void drain_errors(const std::string_view &prefix) {
|
||||
GLenum err;
|
||||
while((err = ctx.GetError()) != GL_NO_ERROR) {
|
||||
BOOST_LOG(error) << "GL: "sv << prefix << ": ["sv << util::hex(err).to_string_view() << ']';
|
||||
}
|
||||
}
|
||||
|
||||
tex_t::~tex_t() {
|
||||
if(!size() == 0) {
|
||||
ctx.DeleteTextures(size(), begin());
|
||||
}
|
||||
}
|
||||
|
||||
tex_t tex_t::make(std::size_t count) {
|
||||
tex_t textures { count };
|
||||
|
||||
ctx.GenTextures(textures.size(), textures.begin());
|
||||
|
||||
float color[] = { 0.0f, 0.0f, 0.0f, 1.0f };
|
||||
|
||||
for(auto tex : textures) {
|
||||
gl::ctx.BindTexture(GL_TEXTURE_2D, tex);
|
||||
gl::ctx.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); // x
|
||||
gl::ctx.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); // y
|
||||
gl::ctx.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
gl::ctx.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
gl::ctx.TexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, color);
|
||||
}
|
||||
|
||||
return textures;
|
||||
}
|
||||
|
||||
frame_buf_t::~frame_buf_t() {
|
||||
if(begin()) {
|
||||
ctx.DeleteFramebuffers(size(), begin());
|
||||
}
|
||||
}
|
||||
|
||||
frame_buf_t frame_buf_t::make(std::size_t count) {
|
||||
frame_buf_t frame_buf { count };
|
||||
|
||||
ctx.GenFramebuffers(frame_buf.size(), frame_buf.begin());
|
||||
|
||||
return frame_buf;
|
||||
}
|
||||
|
||||
void frame_buf_t::copy(int id, int texture, int offset_x, int offset_y, int width, int height) {
|
||||
gl::ctx.BindFramebuffer(GL_FRAMEBUFFER, (*this)[id]);
|
||||
gl::ctx.ReadBuffer(GL_COLOR_ATTACHMENT0 + id);
|
||||
gl::ctx.BindTexture(GL_TEXTURE_2D, texture);
|
||||
gl::ctx.CopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, offset_x, offset_y, width, height);
|
||||
}
|
||||
|
||||
std::string shader_t::err_str() {
|
||||
int length;
|
||||
ctx.GetShaderiv(handle(), GL_INFO_LOG_LENGTH, &length);
|
||||
|
||||
std::string string;
|
||||
string.resize(length);
|
||||
|
||||
ctx.GetShaderInfoLog(handle(), length, &length, string.data());
|
||||
|
||||
string.resize(length - 1);
|
||||
|
||||
return string;
|
||||
}
|
||||
|
||||
util::Either<shader_t, std::string> shader_t::compile(const std::string_view &source, GLenum type) {
|
||||
shader_t shader;
|
||||
|
||||
auto data = source.data();
|
||||
GLint length = source.length();
|
||||
|
||||
shader._shader.el = ctx.CreateShader(type);
|
||||
ctx.ShaderSource(shader.handle(), 1, &data, &length);
|
||||
ctx.CompileShader(shader.handle());
|
||||
|
||||
int status = 0;
|
||||
ctx.GetShaderiv(shader.handle(), GL_COMPILE_STATUS, &status);
|
||||
|
||||
if(!status) {
|
||||
return shader.err_str();
|
||||
}
|
||||
|
||||
return shader;
|
||||
}
|
||||
|
||||
GLuint shader_t::handle() const {
|
||||
return _shader.el;
|
||||
}
|
||||
|
||||
buffer_t buffer_t::make(util::buffer_t<GLint> &&offsets, const char *block, const std::string_view &data) {
|
||||
buffer_t buffer;
|
||||
buffer._block = block;
|
||||
buffer._size = data.size();
|
||||
buffer._offsets = std::move(offsets);
|
||||
|
||||
ctx.GenBuffers(1, &buffer._buffer.el);
|
||||
ctx.BindBuffer(GL_UNIFORM_BUFFER, buffer.handle());
|
||||
ctx.BufferData(GL_UNIFORM_BUFFER, data.size(), (const std::uint8_t *)data.data(), GL_DYNAMIC_DRAW);
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
GLuint buffer_t::handle() const {
|
||||
return _buffer.el;
|
||||
}
|
||||
|
||||
const char *buffer_t::block() const {
|
||||
return _block;
|
||||
}
|
||||
|
||||
void buffer_t::update(const std::string_view &view, std::size_t offset) {
|
||||
ctx.BindBuffer(GL_UNIFORM_BUFFER, handle());
|
||||
ctx.BufferSubData(GL_UNIFORM_BUFFER, offset, view.size(), (const void *)view.data());
|
||||
}
|
||||
|
||||
void buffer_t::update(std::string_view *members, std::size_t count, std::size_t offset) {
|
||||
util::buffer_t<std::uint8_t> buffer { _size };
|
||||
|
||||
for(int x = 0; x < count; ++x) {
|
||||
auto val = members[x];
|
||||
|
||||
std::copy_n((const std::uint8_t *)val.data(), val.size(), &buffer[_offsets[x]]);
|
||||
}
|
||||
|
||||
update(util::view(buffer.begin(), buffer.end()), offset);
|
||||
}
|
||||
|
||||
std::string program_t::err_str() {
|
||||
int length;
|
||||
ctx.GetProgramiv(handle(), GL_INFO_LOG_LENGTH, &length);
|
||||
|
||||
std::string string;
|
||||
string.resize(length);
|
||||
|
||||
ctx.GetShaderInfoLog(handle(), length, &length, string.data());
|
||||
|
||||
string.resize(length - 1);
|
||||
|
||||
return string;
|
||||
}
|
||||
|
||||
util::Either<program_t, std::string> program_t::link(const shader_t &vert, const shader_t &frag) {
|
||||
program_t program;
|
||||
|
||||
program._program.el = ctx.CreateProgram();
|
||||
|
||||
ctx.AttachShader(program.handle(), vert.handle());
|
||||
ctx.AttachShader(program.handle(), frag.handle());
|
||||
|
||||
// p_handle stores a copy of the program handle, since program will be moved before
|
||||
// the fail guard funcion is called.
|
||||
auto fg = util::fail_guard([p_handle = program.handle(), &vert, &frag]() {
|
||||
ctx.DetachShader(p_handle, vert.handle());
|
||||
ctx.DetachShader(p_handle, frag.handle());
|
||||
});
|
||||
|
||||
ctx.LinkProgram(program.handle());
|
||||
|
||||
int status = 0;
|
||||
ctx.GetProgramiv(program.handle(), GL_LINK_STATUS, &status);
|
||||
|
||||
if(!status) {
|
||||
return program.err_str();
|
||||
}
|
||||
|
||||
return program;
|
||||
}
|
||||
|
||||
void program_t::bind(const buffer_t &buffer) {
|
||||
ctx.UseProgram(handle());
|
||||
auto i = ctx.GetUniformBlockIndex(handle(), buffer.block());
|
||||
|
||||
ctx.BindBufferBase(GL_UNIFORM_BUFFER, i, buffer.handle());
|
||||
}
|
||||
|
||||
std::optional<buffer_t> program_t::uniform(const char *block, std::pair<const char *, std::string_view> *members, std::size_t count) {
|
||||
auto i = ctx.GetUniformBlockIndex(handle(), block);
|
||||
if(i == GL_INVALID_INDEX) {
|
||||
BOOST_LOG(error) << "Couldn't find index of ["sv << block << ']';
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
int size;
|
||||
ctx.GetActiveUniformBlockiv(handle(), i, GL_UNIFORM_BLOCK_DATA_SIZE, &size);
|
||||
|
||||
bool error_flag = false;
|
||||
|
||||
util::buffer_t<GLint> offsets { count };
|
||||
auto indices = (std::uint32_t *)alloca(count * sizeof(std::uint32_t));
|
||||
auto names = (const char **)alloca(count * sizeof(const char *));
|
||||
auto names_p = names;
|
||||
|
||||
std::for_each_n(members, count, [names_p](auto &member) mutable {
|
||||
*names_p++ = std::get<0>(member);
|
||||
});
|
||||
|
||||
std::fill_n(indices, count, GL_INVALID_INDEX);
|
||||
ctx.GetUniformIndices(handle(), count, names, indices);
|
||||
|
||||
for(int x = 0; x < count; ++x) {
|
||||
if(indices[x] == GL_INVALID_INDEX) {
|
||||
error_flag = true;
|
||||
|
||||
BOOST_LOG(error) << "Couldn't find ["sv << block << '.' << members[x].first << ']';
|
||||
}
|
||||
}
|
||||
|
||||
if(error_flag) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
ctx.GetActiveUniformsiv(handle(), count, indices, GL_UNIFORM_OFFSET, offsets.begin());
|
||||
util::buffer_t<std::uint8_t> buffer { (std::size_t)size };
|
||||
|
||||
for(int x = 0; x < count; ++x) {
|
||||
auto val = std::get<1>(members[x]);
|
||||
|
||||
std::copy_n((const std::uint8_t *)val.data(), val.size(), &buffer[offsets[x]]);
|
||||
}
|
||||
|
||||
return buffer_t::make(std::move(offsets), block, std::string_view { (char *)buffer.begin(), buffer.size() });
|
||||
}
|
||||
|
||||
GLuint program_t::handle() const {
|
||||
return _program.el;
|
||||
}
|
||||
|
||||
} // namespace gl
|
||||
|
||||
namespace gbm {
|
||||
device_destroy_fn device_destroy;
|
||||
create_device_fn create_device;
|
||||
|
||||
int init() {
|
||||
static void *handle { nullptr };
|
||||
static bool funcs_loaded = false;
|
||||
|
||||
if(funcs_loaded) return 0;
|
||||
|
||||
if(!handle) {
|
||||
handle = dyn::handle({ "libgbm.so.1", "libgbm.so" });
|
||||
if(!handle) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::tuple<GLADapiproc *, const char *>> funcs {
|
||||
{ (GLADapiproc *)&device_destroy, "gbm_device_destroy" },
|
||||
{ (GLADapiproc *)&create_device, "gbm_create_device" },
|
||||
};
|
||||
|
||||
if(dyn::load(handle, funcs)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
funcs_loaded = true;
|
||||
return 0;
|
||||
}
|
||||
} // namespace gbm
|
||||
|
||||
namespace egl {
|
||||
constexpr auto EGL_LINUX_DMA_BUF_EXT = 0x3270;
|
||||
constexpr auto EGL_LINUX_DRM_FOURCC_EXT = 0x3271;
|
||||
constexpr auto EGL_DMA_BUF_PLANE0_FD_EXT = 0x3272;
|
||||
constexpr auto EGL_DMA_BUF_PLANE0_OFFSET_EXT = 0x3273;
|
||||
constexpr auto EGL_DMA_BUF_PLANE0_PITCH_EXT = 0x3274;
|
||||
constexpr auto EGL_DMA_BUF_PLANE1_FD_EXT = 0x3275;
|
||||
constexpr auto EGL_DMA_BUF_PLANE1_OFFSET_EXT = 0x3276;
|
||||
constexpr auto EGL_DMA_BUF_PLANE1_PITCH_EXT = 0x3277;
|
||||
constexpr auto EGL_DMA_BUF_PLANE2_FD_EXT = 0x3278;
|
||||
constexpr auto EGL_DMA_BUF_PLANE2_OFFSET_EXT = 0x3279;
|
||||
constexpr auto EGL_DMA_BUF_PLANE2_PITCH_EXT = 0x327A;
|
||||
constexpr auto EGL_DMA_BUF_PLANE3_FD_EXT = 0x3440;
|
||||
constexpr auto EGL_DMA_BUF_PLANE3_OFFSET_EXT = 0x3441;
|
||||
constexpr auto EGL_DMA_BUF_PLANE3_PITCH_EXT = 0x3442;
|
||||
constexpr auto EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT = 0x3443;
|
||||
constexpr auto EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT = 0x3444;
|
||||
constexpr auto EGL_DMA_BUF_PLANE1_MODIFIER_LO_EXT = 0x3445;
|
||||
constexpr auto EGL_DMA_BUF_PLANE1_MODIFIER_HI_EXT = 0x3446;
|
||||
constexpr auto EGL_DMA_BUF_PLANE2_MODIFIER_LO_EXT = 0x3447;
|
||||
constexpr auto EGL_DMA_BUF_PLANE2_MODIFIER_HI_EXT = 0x3448;
|
||||
constexpr auto EGL_DMA_BUF_PLANE3_MODIFIER_LO_EXT = 0x3449;
|
||||
constexpr auto EGL_DMA_BUF_PLANE3_MODIFIER_HI_EXT = 0x344A;
|
||||
|
||||
bool fail() {
|
||||
return eglGetError() != EGL_SUCCESS;
|
||||
}
|
||||
|
||||
display_t make_display(std::variant<gbm::gbm_t::pointer, wl_display *, _XDisplay *> native_display) {
|
||||
constexpr auto EGL_PLATFORM_GBM_MESA = 0x31D7;
|
||||
constexpr auto EGL_PLATFORM_WAYLAND_KHR = 0x31D8;
|
||||
constexpr auto EGL_PLATFORM_X11_KHR = 0x31D5;
|
||||
|
||||
int egl_platform;
|
||||
void *native_display_p;
|
||||
|
||||
switch(native_display.index()) {
|
||||
case 0:
|
||||
egl_platform = EGL_PLATFORM_GBM_MESA;
|
||||
native_display_p = std::get<0>(native_display);
|
||||
break;
|
||||
case 1:
|
||||
egl_platform = EGL_PLATFORM_WAYLAND_KHR;
|
||||
native_display_p = std::get<1>(native_display);
|
||||
break;
|
||||
case 2:
|
||||
egl_platform = EGL_PLATFORM_X11_KHR;
|
||||
native_display_p = std::get<2>(native_display);
|
||||
break;
|
||||
default:
|
||||
BOOST_LOG(error) << "egl::make_display(): Index ["sv << native_display.index() << "] not implemented"sv;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// native_display.left() equals native_display.right()
|
||||
display_t display = eglGetPlatformDisplay(egl_platform, native_display_p, nullptr);
|
||||
|
||||
if(fail()) {
|
||||
BOOST_LOG(error) << "Couldn't open EGL display: ["sv << util::hex(eglGetError()).to_string_view() << ']';
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int major, minor;
|
||||
if(!eglInitialize(display.get(), &major, &minor)) {
|
||||
BOOST_LOG(error) << "Couldn't initialize EGL display: ["sv << util::hex(eglGetError()).to_string_view() << ']';
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const char *extension_st = eglQueryString(display.get(), EGL_EXTENSIONS);
|
||||
const char *version = eglQueryString(display.get(), EGL_VERSION);
|
||||
const char *vendor = eglQueryString(display.get(), EGL_VENDOR);
|
||||
const char *apis = eglQueryString(display.get(), EGL_CLIENT_APIS);
|
||||
|
||||
BOOST_LOG(debug) << "EGL: ["sv << vendor << "]: version ["sv << version << ']';
|
||||
BOOST_LOG(debug) << "API's supported: ["sv << apis << ']';
|
||||
|
||||
const char *extensions[] {
|
||||
"EGL_KHR_create_context",
|
||||
"EGL_KHR_surfaceless_context",
|
||||
"EGL_EXT_image_dma_buf_import",
|
||||
};
|
||||
|
||||
for(auto ext : extensions) {
|
||||
if(!std::strstr(extension_st, ext)) {
|
||||
BOOST_LOG(error) << "Missing extension: ["sv << ext << ']';
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
return display;
|
||||
}
|
||||
|
||||
std::optional<ctx_t> make_ctx(display_t::pointer display) {
|
||||
constexpr int conf_attr[] {
|
||||
EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, EGL_NONE
|
||||
};
|
||||
|
||||
int count;
|
||||
EGLConfig conf;
|
||||
if(!eglChooseConfig(display, conf_attr, &conf, 1, &count)) {
|
||||
BOOST_LOG(error) << "Couldn't set config attributes: ["sv << util::hex(eglGetError()).to_string_view() << ']';
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if(!eglBindAPI(EGL_OPENGL_API)) {
|
||||
BOOST_LOG(error) << "Couldn't bind API: ["sv << util::hex(eglGetError()).to_string_view() << ']';
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
constexpr int attr[] {
|
||||
EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE
|
||||
};
|
||||
|
||||
ctx_t ctx { display, eglCreateContext(display, conf, EGL_NO_CONTEXT, attr) };
|
||||
if(fail()) {
|
||||
BOOST_LOG(error) << "Couldn't create EGL context: ["sv << util::hex(eglGetError()).to_string_view() << ']';
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
TUPLE_EL_REF(ctx_p, 1, ctx.el);
|
||||
if(!eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, ctx_p)) {
|
||||
BOOST_LOG(error) << "Couldn't make current display"sv;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if(!gladLoadGLContext(&gl::ctx, eglGetProcAddress)) {
|
||||
BOOST_LOG(error) << "Couldn't load OpenGL library"sv;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
BOOST_LOG(debug) << "GL: vendor: "sv << gl::ctx.GetString(GL_VENDOR);
|
||||
BOOST_LOG(debug) << "GL: renderer: "sv << gl::ctx.GetString(GL_RENDERER);
|
||||
BOOST_LOG(debug) << "GL: version: "sv << gl::ctx.GetString(GL_VERSION);
|
||||
BOOST_LOG(debug) << "GL: shader: "sv << gl::ctx.GetString(GL_SHADING_LANGUAGE_VERSION);
|
||||
|
||||
gl::ctx.PixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
|
||||
return ctx;
|
||||
}
|
||||
|
||||
struct plane_attr_t {
|
||||
EGLAttrib fd;
|
||||
EGLAttrib offset;
|
||||
EGLAttrib pitch;
|
||||
EGLAttrib lo;
|
||||
EGLAttrib hi;
|
||||
};
|
||||
|
||||
inline plane_attr_t get_plane(std::uint32_t plane_indice) {
|
||||
switch(plane_indice) {
|
||||
case 0:
|
||||
return {
|
||||
EGL_DMA_BUF_PLANE0_FD_EXT,
|
||||
EGL_DMA_BUF_PLANE0_OFFSET_EXT,
|
||||
EGL_DMA_BUF_PLANE0_PITCH_EXT,
|
||||
EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT,
|
||||
EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT,
|
||||
};
|
||||
case 1:
|
||||
return {
|
||||
EGL_DMA_BUF_PLANE1_FD_EXT,
|
||||
EGL_DMA_BUF_PLANE1_OFFSET_EXT,
|
||||
EGL_DMA_BUF_PLANE1_PITCH_EXT,
|
||||
EGL_DMA_BUF_PLANE1_MODIFIER_LO_EXT,
|
||||
EGL_DMA_BUF_PLANE1_MODIFIER_HI_EXT,
|
||||
};
|
||||
case 2:
|
||||
return {
|
||||
EGL_DMA_BUF_PLANE2_FD_EXT,
|
||||
EGL_DMA_BUF_PLANE2_OFFSET_EXT,
|
||||
EGL_DMA_BUF_PLANE2_PITCH_EXT,
|
||||
EGL_DMA_BUF_PLANE2_MODIFIER_LO_EXT,
|
||||
EGL_DMA_BUF_PLANE2_MODIFIER_HI_EXT,
|
||||
};
|
||||
case 3:
|
||||
return {
|
||||
EGL_DMA_BUF_PLANE3_FD_EXT,
|
||||
EGL_DMA_BUF_PLANE3_OFFSET_EXT,
|
||||
EGL_DMA_BUF_PLANE3_PITCH_EXT,
|
||||
EGL_DMA_BUF_PLANE3_MODIFIER_LO_EXT,
|
||||
EGL_DMA_BUF_PLANE3_MODIFIER_HI_EXT,
|
||||
};
|
||||
}
|
||||
|
||||
// Avoid warning
|
||||
return {};
|
||||
}
|
||||
|
||||
std::optional<rgb_t> import_source(display_t::pointer egl_display, const surface_descriptor_t &xrgb) {
|
||||
EGLAttrib attribs[47];
|
||||
int atti = 0;
|
||||
attribs[atti++] = EGL_WIDTH;
|
||||
attribs[atti++] = xrgb.width;
|
||||
attribs[atti++] = EGL_HEIGHT;
|
||||
attribs[atti++] = xrgb.height;
|
||||
attribs[atti++] = EGL_LINUX_DRM_FOURCC_EXT;
|
||||
attribs[atti++] = xrgb.fourcc;
|
||||
|
||||
for(auto x = 0; x < 4; ++x) {
|
||||
auto fd = xrgb.fds[x];
|
||||
|
||||
if(fd < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto plane_attr = get_plane(x);
|
||||
|
||||
attribs[atti++] = plane_attr.fd;
|
||||
attribs[atti++] = fd;
|
||||
attribs[atti++] = plane_attr.offset;
|
||||
attribs[atti++] = xrgb.offsets[x];
|
||||
attribs[atti++] = plane_attr.pitch;
|
||||
attribs[atti++] = xrgb.pitches[x];
|
||||
|
||||
if(xrgb.modifier != DRM_FORMAT_MOD_INVALID) {
|
||||
attribs[atti++] = plane_attr.lo;
|
||||
attribs[atti++] = xrgb.modifier & 0xFFFFFFFF;
|
||||
attribs[atti++] = plane_attr.hi;
|
||||
attribs[atti++] = xrgb.modifier >> 32;
|
||||
}
|
||||
}
|
||||
attribs[atti++] = EGL_NONE;
|
||||
|
||||
rgb_t rgb {
|
||||
egl_display,
|
||||
eglCreateImage(egl_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, attribs),
|
||||
gl::tex_t::make(1)
|
||||
};
|
||||
|
||||
if(!rgb->xrgb8) {
|
||||
BOOST_LOG(error) << "Couldn't import RGB Image: "sv << util::hex(eglGetError()).to_string_view();
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
gl::ctx.BindTexture(GL_TEXTURE_2D, rgb->tex[0]);
|
||||
gl::ctx.EGLImageTargetTexture2DOES(GL_TEXTURE_2D, rgb->xrgb8);
|
||||
|
||||
gl::ctx.BindTexture(GL_TEXTURE_2D, 0);
|
||||
|
||||
gl_drain_errors;
|
||||
|
||||
return rgb;
|
||||
}
|
||||
|
||||
std::optional<nv12_t> import_target(display_t::pointer egl_display, std::array<file_t, nv12_img_t::num_fds> &&fds, const surface_descriptor_t &r8, const surface_descriptor_t &gr88) {
|
||||
EGLAttrib img_attr_planes[2][13] {
|
||||
{ EGL_LINUX_DRM_FOURCC_EXT, DRM_FORMAT_R8,
|
||||
EGL_WIDTH, r8.width,
|
||||
EGL_HEIGHT, r8.height,
|
||||
EGL_DMA_BUF_PLANE0_FD_EXT, r8.fds[0],
|
||||
EGL_DMA_BUF_PLANE0_OFFSET_EXT, r8.offsets[0],
|
||||
EGL_DMA_BUF_PLANE0_PITCH_EXT, r8.pitches[0],
|
||||
EGL_NONE },
|
||||
|
||||
{ EGL_LINUX_DRM_FOURCC_EXT, DRM_FORMAT_GR88,
|
||||
EGL_WIDTH, gr88.width,
|
||||
EGL_HEIGHT, gr88.height,
|
||||
EGL_DMA_BUF_PLANE0_FD_EXT, r8.fds[0],
|
||||
EGL_DMA_BUF_PLANE0_OFFSET_EXT, gr88.offsets[0],
|
||||
EGL_DMA_BUF_PLANE0_PITCH_EXT, gr88.pitches[0],
|
||||
EGL_NONE },
|
||||
};
|
||||
|
||||
nv12_t nv12 {
|
||||
egl_display,
|
||||
eglCreateImage(egl_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, img_attr_planes[0]),
|
||||
eglCreateImage(egl_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, img_attr_planes[1]),
|
||||
gl::tex_t::make(2),
|
||||
gl::frame_buf_t::make(2),
|
||||
std::move(fds)
|
||||
};
|
||||
|
||||
if(!nv12->r8 || !nv12->bg88) {
|
||||
BOOST_LOG(error) << "Couldn't create KHR Image"sv;
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
gl::ctx.BindTexture(GL_TEXTURE_2D, nv12->tex[0]);
|
||||
gl::ctx.EGLImageTargetTexture2DOES(GL_TEXTURE_2D, nv12->r8);
|
||||
|
||||
gl::ctx.BindTexture(GL_TEXTURE_2D, nv12->tex[1]);
|
||||
gl::ctx.EGLImageTargetTexture2DOES(GL_TEXTURE_2D, nv12->bg88);
|
||||
|
||||
nv12->buf.bind(std::begin(nv12->tex), std::end(nv12->tex));
|
||||
|
||||
gl_drain_errors;
|
||||
|
||||
return nv12;
|
||||
}
|
||||
|
||||
void sws_t::set_colorspace(std::uint32_t colorspace, std::uint32_t color_range) {
|
||||
video::color_t *color_p;
|
||||
switch(colorspace) {
|
||||
case 5: // SWS_CS_SMPTE170M
|
||||
color_p = &video::colors[0];
|
||||
break;
|
||||
case 1: // SWS_CS_ITU709
|
||||
color_p = &video::colors[2];
|
||||
break;
|
||||
case 9: // SWS_CS_BT2020
|
||||
default:
|
||||
BOOST_LOG(warning) << "Colorspace: ["sv << colorspace << "] not yet supported: switching to default"sv;
|
||||
color_p = &video::colors[0];
|
||||
};
|
||||
|
||||
if(color_range > 1) {
|
||||
// Full range
|
||||
++color_p;
|
||||
}
|
||||
|
||||
std::string_view members[] {
|
||||
util::view(color_p->color_vec_y),
|
||||
util::view(color_p->color_vec_u),
|
||||
util::view(color_p->color_vec_v),
|
||||
util::view(color_p->range_y),
|
||||
util::view(color_p->range_uv),
|
||||
};
|
||||
|
||||
color_matrix.update(members, sizeof(members) / sizeof(decltype(members[0])));
|
||||
|
||||
program[0].bind(color_matrix);
|
||||
program[1].bind(color_matrix);
|
||||
}
|
||||
|
||||
std::optional<sws_t> sws_t::make(int in_width, int in_height, int out_width, int out_heigth, gl::tex_t &&tex) {
|
||||
sws_t sws;
|
||||
|
||||
sws.serial = std::numeric_limits<std::uint64_t>::max();
|
||||
|
||||
// Ensure aspect ratio is maintained
|
||||
auto scalar = std::fminf(out_width / (float)in_width, out_heigth / (float)in_height);
|
||||
auto out_width_f = in_width * scalar;
|
||||
auto out_height_f = in_height * scalar;
|
||||
|
||||
// result is always positive
|
||||
auto offsetX_f = (out_width - out_width_f) / 2;
|
||||
auto offsetY_f = (out_heigth - out_height_f) / 2;
|
||||
|
||||
sws.out_width = out_width_f;
|
||||
sws.out_height = out_height_f;
|
||||
|
||||
sws.in_width = in_width;
|
||||
sws.in_height = in_height;
|
||||
|
||||
sws.offsetX = offsetX_f;
|
||||
sws.offsetY = offsetY_f;
|
||||
|
||||
auto width_i = 1.0f / sws.out_width;
|
||||
|
||||
{
|
||||
const char *sources[] {
|
||||
SUNSHINE_SHADERS_DIR "/ConvertUV.frag",
|
||||
SUNSHINE_SHADERS_DIR "/ConvertUV.vert",
|
||||
SUNSHINE_SHADERS_DIR "/ConvertY.frag",
|
||||
SUNSHINE_SHADERS_DIR "/Scene.vert",
|
||||
SUNSHINE_SHADERS_DIR "/Scene.frag",
|
||||
};
|
||||
|
||||
GLenum shader_type[2] {
|
||||
GL_FRAGMENT_SHADER,
|
||||
GL_VERTEX_SHADER,
|
||||
};
|
||||
|
||||
constexpr auto count = sizeof(sources) / sizeof(const char *);
|
||||
|
||||
util::Either<gl::shader_t, std::string> compiled_sources[count];
|
||||
|
||||
bool error_flag = false;
|
||||
for(int x = 0; x < count; ++x) {
|
||||
auto &compiled_source = compiled_sources[x];
|
||||
|
||||
compiled_source = gl::shader_t::compile(read_file(sources[x]), shader_type[x % 2]);
|
||||
gl_drain_errors;
|
||||
|
||||
if(compiled_source.has_right()) {
|
||||
BOOST_LOG(error) << sources[x] << ": "sv << compiled_source.right();
|
||||
error_flag = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(error_flag) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto program = gl::program_t::link(compiled_sources[3].left(), compiled_sources[4].left());
|
||||
if(program.has_right()) {
|
||||
BOOST_LOG(error) << "GL linker: "sv << program.right();
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Cursor - shader
|
||||
sws.program[2] = std::move(program.left());
|
||||
|
||||
program = gl::program_t::link(compiled_sources[1].left(), compiled_sources[0].left());
|
||||
if(program.has_right()) {
|
||||
BOOST_LOG(error) << "GL linker: "sv << program.right();
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// UV - shader
|
||||
sws.program[1] = std::move(program.left());
|
||||
|
||||
program = gl::program_t::link(compiled_sources[3].left(), compiled_sources[2].left());
|
||||
if(program.has_right()) {
|
||||
BOOST_LOG(error) << "GL linker: "sv << program.right();
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Y - shader
|
||||
sws.program[0] = std::move(program.left());
|
||||
}
|
||||
|
||||
auto loc_width_i = gl::ctx.GetUniformLocation(sws.program[1].handle(), "width_i");
|
||||
if(loc_width_i < 0) {
|
||||
BOOST_LOG(error) << "Couldn't find uniform [width_i]"sv;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
gl::ctx.UseProgram(sws.program[1].handle());
|
||||
gl::ctx.Uniform1fv(loc_width_i, 1, &width_i);
|
||||
|
||||
auto color_p = &video::colors[0];
|
||||
std::pair<const char *, std::string_view> members[] {
|
||||
std::make_pair("color_vec_y", util::view(color_p->color_vec_y)),
|
||||
std::make_pair("color_vec_u", util::view(color_p->color_vec_u)),
|
||||
std::make_pair("color_vec_v", util::view(color_p->color_vec_v)),
|
||||
std::make_pair("range_y", util::view(color_p->range_y)),
|
||||
std::make_pair("range_uv", util::view(color_p->range_uv)),
|
||||
};
|
||||
|
||||
auto color_matrix = sws.program[0].uniform("ColorMatrix", members, sizeof(members) / sizeof(decltype(members[0])));
|
||||
if(!color_matrix) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
sws.color_matrix = std::move(*color_matrix);
|
||||
|
||||
sws.tex = std::move(tex);
|
||||
|
||||
sws.cursor_framebuffer = gl::frame_buf_t::make(1);
|
||||
sws.cursor_framebuffer.bind(&sws.tex[0], &sws.tex[1]);
|
||||
|
||||
sws.program[0].bind(sws.color_matrix);
|
||||
sws.program[1].bind(sws.color_matrix);
|
||||
|
||||
gl::ctx.BlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
gl_drain_errors;
|
||||
|
||||
return std::move(sws);
|
||||
}
|
||||
|
||||
int sws_t::blank(gl::frame_buf_t &fb, int offsetX, int offsetY, int width, int height) {
|
||||
auto f = [&]() {
|
||||
std::swap(offsetX, this->offsetX);
|
||||
std::swap(offsetY, this->offsetY);
|
||||
std::swap(width, this->out_width);
|
||||
std::swap(height, this->out_height);
|
||||
};
|
||||
|
||||
f();
|
||||
auto fg = util::fail_guard(f);
|
||||
|
||||
return convert(fb);
|
||||
}
|
||||
|
||||
std::optional<sws_t> sws_t::make(int in_width, int in_height, int out_width, int out_heigth) {
|
||||
auto tex = gl::tex_t::make(2);
|
||||
gl::ctx.BindTexture(GL_TEXTURE_2D, tex[0]);
|
||||
gl::ctx.TexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, in_width, in_height);
|
||||
|
||||
return make(in_width, in_height, out_width, out_heigth, std::move(tex));
|
||||
}
|
||||
|
||||
void sws_t::load_ram(platf::img_t &img) {
|
||||
loaded_texture = tex[0];
|
||||
|
||||
gl::ctx.BindTexture(GL_TEXTURE_2D, loaded_texture);
|
||||
gl::ctx.TexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, img.width, img.height, GL_BGRA, GL_UNSIGNED_BYTE, img.data);
|
||||
}
|
||||
|
||||
void sws_t::load_vram(img_descriptor_t &img, int offset_x, int offset_y, int texture) {
|
||||
// When only a sub-part of the image must be encoded...
|
||||
const bool copy = offset_x || offset_y || img.sd.width != in_width || img.sd.height != in_height;
|
||||
if(copy) {
|
||||
auto framebuf = gl::frame_buf_t::make(1);
|
||||
framebuf.bind(&texture, &texture + 1);
|
||||
|
||||
loaded_texture = tex[0];
|
||||
framebuf.copy(0, loaded_texture, offset_x, offset_y, in_width, in_height);
|
||||
}
|
||||
else {
|
||||
loaded_texture = texture;
|
||||
}
|
||||
|
||||
if(img.data) {
|
||||
GLenum attachment = GL_COLOR_ATTACHMENT0;
|
||||
|
||||
gl::ctx.BindFramebuffer(GL_FRAMEBUFFER, cursor_framebuffer[0]);
|
||||
gl::ctx.UseProgram(program[2].handle());
|
||||
|
||||
// When a copy has already been made...
|
||||
if(!copy) {
|
||||
gl::ctx.BindTexture(GL_TEXTURE_2D, texture);
|
||||
gl::ctx.DrawBuffers(1, &attachment);
|
||||
|
||||
gl::ctx.Viewport(0, 0, in_width, in_height);
|
||||
gl::ctx.DrawArrays(GL_TRIANGLES, 0, 3);
|
||||
|
||||
loaded_texture = tex[0];
|
||||
}
|
||||
|
||||
gl::ctx.BindTexture(GL_TEXTURE_2D, tex[1]);
|
||||
if(serial != img.serial) {
|
||||
serial = img.serial;
|
||||
|
||||
gl::ctx.TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, img.width, img.height, 0, GL_BGRA, GL_UNSIGNED_BYTE, img.data);
|
||||
}
|
||||
|
||||
gl::ctx.Enable(GL_BLEND);
|
||||
|
||||
gl::ctx.DrawBuffers(1, &attachment);
|
||||
|
||||
#ifndef NDEBUG
|
||||
auto status = gl::ctx.CheckFramebufferStatus(GL_FRAMEBUFFER);
|
||||
if(status != GL_FRAMEBUFFER_COMPLETE) {
|
||||
BOOST_LOG(error) << "Pass Cursor: CheckFramebufferStatus() --> [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
gl::ctx.Viewport(img.x, img.y, img.width, img.height);
|
||||
gl::ctx.DrawArrays(GL_TRIANGLES, 0, 3);
|
||||
|
||||
gl::ctx.Disable(GL_BLEND);
|
||||
|
||||
gl::ctx.BindTexture(GL_TEXTURE_2D, 0);
|
||||
gl::ctx.BindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
}
|
||||
}
|
||||
|
||||
int sws_t::convert(gl::frame_buf_t &fb) {
|
||||
gl::ctx.BindTexture(GL_TEXTURE_2D, loaded_texture);
|
||||
|
||||
GLenum attachments[] {
|
||||
GL_COLOR_ATTACHMENT0,
|
||||
GL_COLOR_ATTACHMENT1
|
||||
};
|
||||
|
||||
for(int x = 0; x < sizeof(attachments) / sizeof(decltype(attachments[0])); ++x) {
|
||||
gl::ctx.BindFramebuffer(GL_FRAMEBUFFER, fb[x]);
|
||||
gl::ctx.DrawBuffers(1, &attachments[x]);
|
||||
|
||||
#ifndef NDEBUG
|
||||
auto status = gl::ctx.CheckFramebufferStatus(GL_FRAMEBUFFER);
|
||||
if(status != GL_FRAMEBUFFER_COMPLETE) {
|
||||
BOOST_LOG(error) << "Pass "sv << x << ": CheckFramebufferStatus() --> [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return -1;
|
||||
}
|
||||
#endif
|
||||
|
||||
gl::ctx.UseProgram(program[x].handle());
|
||||
gl::ctx.Viewport(offsetX / (x + 1), offsetY / (x + 1), out_width / (x + 1), out_height / (x + 1));
|
||||
gl::ctx.DrawArrays(GL_TRIANGLES, 0, 3);
|
||||
}
|
||||
|
||||
gl::ctx.BindTexture(GL_TEXTURE_2D, 0);
|
||||
|
||||
gl::ctx.Flush();
|
||||
|
||||
return 0;
|
||||
}
|
||||
} // namespace egl
|
||||
|
||||
void free_frame(AVFrame *frame) {
|
||||
av_frame_free(&frame);
|
||||
}
|
||||
319
sunshine/platform/linux/graphics.h
Normal file
319
sunshine/platform/linux/graphics.h
Normal file
@@ -0,0 +1,319 @@
|
||||
#ifndef SUNSHINE_PLATFORM_LINUX_OPENGL_H
|
||||
#define SUNSHINE_PLATFORM_LINUX_OPENGL_H
|
||||
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
|
||||
#include <glad/egl.h>
|
||||
#include <glad/gl.h>
|
||||
|
||||
#include "misc.h"
|
||||
#include "sunshine/main.h"
|
||||
#include "sunshine/platform/common.h"
|
||||
#include "sunshine/utility.h"
|
||||
|
||||
#define SUNSHINE_STRINGIFY_HELPER(x) #x
|
||||
#define SUNSHINE_STRINGIFY(x) SUNSHINE_STRINGIFY_HELPER(x)
|
||||
#define gl_drain_errors_helper(x) gl::drain_errors(x)
|
||||
#define gl_drain_errors gl_drain_errors_helper(__FILE__ ":" SUNSHINE_STRINGIFY(__LINE__))
|
||||
|
||||
extern "C" int close(int __fd);
|
||||
|
||||
// X11 Display
|
||||
extern "C" struct _XDisplay;
|
||||
|
||||
struct AVFrame;
|
||||
void free_frame(AVFrame *frame);
|
||||
|
||||
using frame_t = util::safe_ptr<AVFrame, free_frame>;
|
||||
|
||||
namespace gl {
|
||||
extern GladGLContext ctx;
|
||||
void drain_errors(const std::string_view &prefix);
|
||||
|
||||
class tex_t : public util::buffer_t<GLuint> {
|
||||
using util::buffer_t<GLuint>::buffer_t;
|
||||
|
||||
public:
|
||||
tex_t(tex_t &&) = default;
|
||||
tex_t &operator=(tex_t &&) = default;
|
||||
|
||||
~tex_t();
|
||||
|
||||
static tex_t make(std::size_t count);
|
||||
};
|
||||
|
||||
class frame_buf_t : public util::buffer_t<GLuint> {
|
||||
using util::buffer_t<GLuint>::buffer_t;
|
||||
|
||||
public:
|
||||
frame_buf_t(frame_buf_t &&) = default;
|
||||
frame_buf_t &operator=(frame_buf_t &&) = default;
|
||||
|
||||
~frame_buf_t();
|
||||
|
||||
static frame_buf_t make(std::size_t count);
|
||||
|
||||
inline void bind(std::nullptr_t, std::nullptr_t) {
|
||||
int x = 0;
|
||||
for(auto fb : (*this)) {
|
||||
ctx.BindFramebuffer(GL_FRAMEBUFFER, fb);
|
||||
ctx.FramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + x, 0, 0);
|
||||
|
||||
++x;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
template<class It>
|
||||
void bind(It it_begin, It it_end) {
|
||||
using namespace std::literals;
|
||||
if(std::distance(it_begin, it_end) > size()) {
|
||||
BOOST_LOG(warning) << "To many elements to bind"sv;
|
||||
return;
|
||||
}
|
||||
|
||||
int x = 0;
|
||||
std::for_each(it_begin, it_end, [&](auto tex) {
|
||||
ctx.BindFramebuffer(GL_FRAMEBUFFER, (*this)[x]);
|
||||
ctx.BindTexture(GL_TEXTURE_2D, tex);
|
||||
|
||||
ctx.FramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + x, tex, 0);
|
||||
|
||||
++x;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies a part of the framebuffer to texture
|
||||
*/
|
||||
void copy(int id, int texture, int offset_x, int offset_y, int width, int height);
|
||||
};
|
||||
|
||||
class shader_t {
|
||||
KITTY_USING_MOVE_T(shader_internal_t, GLuint, std::numeric_limits<GLuint>::max(), {
|
||||
if(el != std::numeric_limits<GLuint>::max()) {
|
||||
ctx.DeleteShader(el);
|
||||
}
|
||||
});
|
||||
|
||||
public:
|
||||
std::string err_str();
|
||||
|
||||
static util::Either<shader_t, std::string> compile(const std::string_view &source, GLenum type);
|
||||
|
||||
GLuint handle() const;
|
||||
|
||||
private:
|
||||
shader_internal_t _shader;
|
||||
};
|
||||
|
||||
class buffer_t {
|
||||
KITTY_USING_MOVE_T(buffer_internal_t, GLuint, std::numeric_limits<GLuint>::max(), {
|
||||
if(el != std::numeric_limits<GLuint>::max()) {
|
||||
ctx.DeleteBuffers(1, &el);
|
||||
}
|
||||
});
|
||||
|
||||
public:
|
||||
static buffer_t make(util::buffer_t<GLint> &&offsets, const char *block, const std::string_view &data);
|
||||
|
||||
GLuint handle() const;
|
||||
|
||||
const char *block() const;
|
||||
|
||||
void update(const std::string_view &view, std::size_t offset = 0);
|
||||
void update(std::string_view *members, std::size_t count, std::size_t offset = 0);
|
||||
|
||||
private:
|
||||
const char *_block;
|
||||
|
||||
std::size_t _size;
|
||||
|
||||
util::buffer_t<GLint> _offsets;
|
||||
|
||||
buffer_internal_t _buffer;
|
||||
};
|
||||
|
||||
class program_t {
|
||||
KITTY_USING_MOVE_T(program_internal_t, GLuint, std::numeric_limits<GLuint>::max(), {
|
||||
if(el != std::numeric_limits<GLuint>::max()) {
|
||||
ctx.DeleteProgram(el);
|
||||
}
|
||||
});
|
||||
|
||||
public:
|
||||
std::string err_str();
|
||||
|
||||
static util::Either<program_t, std::string> link(const shader_t &vert, const shader_t &frag);
|
||||
|
||||
void bind(const buffer_t &buffer);
|
||||
|
||||
std::optional<buffer_t> uniform(const char *block, std::pair<const char *, std::string_view> *members, std::size_t count);
|
||||
|
||||
GLuint handle() const;
|
||||
|
||||
private:
|
||||
program_internal_t _program;
|
||||
};
|
||||
} // namespace gl
|
||||
|
||||
namespace gbm {
|
||||
struct device;
|
||||
typedef void (*device_destroy_fn)(device *gbm);
|
||||
typedef device *(*create_device_fn)(int fd);
|
||||
|
||||
extern device_destroy_fn device_destroy;
|
||||
extern create_device_fn create_device;
|
||||
|
||||
using gbm_t = util::dyn_safe_ptr<device, &device_destroy>;
|
||||
|
||||
int init();
|
||||
|
||||
} // namespace gbm
|
||||
|
||||
namespace egl {
|
||||
using display_t = util::dyn_safe_ptr_v2<void, EGLBoolean, &eglTerminate>;
|
||||
|
||||
struct rgb_img_t {
|
||||
display_t::pointer display;
|
||||
EGLImage xrgb8;
|
||||
|
||||
gl::tex_t tex;
|
||||
};
|
||||
|
||||
struct nv12_img_t {
|
||||
display_t::pointer display;
|
||||
EGLImage r8;
|
||||
EGLImage bg88;
|
||||
|
||||
gl::tex_t tex;
|
||||
gl::frame_buf_t buf;
|
||||
|
||||
// sizeof(va::DRMPRIMESurfaceDescriptor::objects) / sizeof(va::DRMPRIMESurfaceDescriptor::objects[0]);
|
||||
static constexpr std::size_t num_fds = 4;
|
||||
|
||||
std::array<file_t, num_fds> fds;
|
||||
};
|
||||
|
||||
KITTY_USING_MOVE_T(rgb_t, rgb_img_t, , {
|
||||
if(el.xrgb8) {
|
||||
eglDestroyImage(el.display, el.xrgb8);
|
||||
}
|
||||
});
|
||||
|
||||
KITTY_USING_MOVE_T(nv12_t, nv12_img_t, , {
|
||||
if(el.r8) {
|
||||
eglDestroyImage(el.display, el.r8);
|
||||
}
|
||||
|
||||
if(el.bg88) {
|
||||
eglDestroyImage(el.display, el.bg88);
|
||||
}
|
||||
});
|
||||
|
||||
KITTY_USING_MOVE_T(ctx_t, (std::tuple<display_t::pointer, EGLContext>), , {
|
||||
TUPLE_2D_REF(disp, ctx, el);
|
||||
if(ctx) {
|
||||
eglMakeCurrent(disp, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
|
||||
eglDestroyContext(disp, ctx);
|
||||
}
|
||||
});
|
||||
|
||||
struct surface_descriptor_t {
|
||||
int width;
|
||||
int height;
|
||||
int fds[4];
|
||||
std::uint32_t fourcc;
|
||||
std::uint64_t modifier;
|
||||
std::uint32_t pitches[4];
|
||||
std::uint32_t offsets[4];
|
||||
};
|
||||
|
||||
display_t make_display(std::variant<gbm::gbm_t::pointer, wl_display *, _XDisplay *> native_display);
|
||||
std::optional<ctx_t> make_ctx(display_t::pointer display);
|
||||
|
||||
std::optional<rgb_t> import_source(
|
||||
display_t::pointer egl_display,
|
||||
const surface_descriptor_t &xrgb);
|
||||
|
||||
std::optional<nv12_t> import_target(
|
||||
display_t::pointer egl_display,
|
||||
std::array<file_t, nv12_img_t::num_fds> &&fds,
|
||||
const surface_descriptor_t &r8, const surface_descriptor_t &gr88);
|
||||
|
||||
class cursor_t : public platf::img_t {
|
||||
public:
|
||||
int x, y;
|
||||
|
||||
unsigned long serial;
|
||||
|
||||
std::vector<std::uint8_t> buffer;
|
||||
};
|
||||
|
||||
// Allow cursor and the underlying image to be kept together
|
||||
class img_descriptor_t : public cursor_t {
|
||||
public:
|
||||
~img_descriptor_t() {
|
||||
reset();
|
||||
}
|
||||
|
||||
void reset() {
|
||||
for(auto x = 0; x < 4; ++x) {
|
||||
if(sd.fds[x] >= 0) {
|
||||
close(sd.fds[x]);
|
||||
|
||||
sd.fds[x] = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
surface_descriptor_t sd;
|
||||
|
||||
// Increment sequence when new rgb_t needs to be created
|
||||
std::uint64_t sequence;
|
||||
};
|
||||
|
||||
class sws_t {
|
||||
public:
|
||||
static std::optional<sws_t> make(int in_width, int in_height, int out_width, int out_heigth, gl::tex_t &&tex);
|
||||
static std::optional<sws_t> make(int in_width, int in_height, int out_width, int out_heigth);
|
||||
|
||||
// Convert the loaded image into the first two framebuffers
|
||||
int convert(gl::frame_buf_t &fb);
|
||||
|
||||
// Make an area of the image black
|
||||
int blank(gl::frame_buf_t &fb, int offsetX, int offsetY, int width, int height);
|
||||
|
||||
void load_ram(platf::img_t &img);
|
||||
void load_vram(img_descriptor_t &img, int offset_x, int offset_y, int texture);
|
||||
|
||||
void set_colorspace(std::uint32_t colorspace, std::uint32_t color_range);
|
||||
|
||||
// The first texture is the monitor image.
|
||||
// The second texture is the cursor image
|
||||
gl::tex_t tex;
|
||||
|
||||
// The cursor image will be blended into this framebuffer
|
||||
gl::frame_buf_t cursor_framebuffer;
|
||||
gl::frame_buf_t copy_framebuffer;
|
||||
|
||||
// Y - shader, UV - shader, Cursor - shader
|
||||
gl::program_t program[3];
|
||||
gl::buffer_t color_matrix;
|
||||
|
||||
int out_width, out_height;
|
||||
int in_width, in_height;
|
||||
int offsetX, offsetY;
|
||||
|
||||
// Pointer to the texture to be converted to nv12
|
||||
int loaded_texture;
|
||||
|
||||
// Store latest cursor for load_vram
|
||||
std::uint64_t serial;
|
||||
};
|
||||
|
||||
bool fail();
|
||||
} // namespace egl
|
||||
|
||||
#endif
|
||||
File diff suppressed because it is too large
Load Diff
1058
sunshine/platform/linux/kmsgrab.cpp
Normal file
1058
sunshine/platform/linux/kmsgrab.cpp
Normal file
File diff suppressed because it is too large
Load Diff
302
sunshine/platform/linux/misc.cpp
Normal file
302
sunshine/platform/linux/misc.cpp
Normal file
@@ -0,0 +1,302 @@
|
||||
#include <arpa/inet.h>
|
||||
#include <dlfcn.h>
|
||||
#include <fcntl.h>
|
||||
#include <ifaddrs.h>
|
||||
#include <pwd.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <fstream>
|
||||
|
||||
#include "graphics.h"
|
||||
#include "misc.h"
|
||||
#include "vaapi.h"
|
||||
|
||||
#include "sunshine/main.h"
|
||||
#include "sunshine/platform/common.h"
|
||||
|
||||
#ifdef __GNUC__
|
||||
#define SUNSHINE_GNUC_EXTENSION __extension__
|
||||
#else
|
||||
#define SUNSHINE_GNUC_EXTENSION
|
||||
#endif
|
||||
|
||||
using namespace std::literals;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
window_system_e window_system;
|
||||
|
||||
namespace dyn {
|
||||
void *handle(const std::vector<const char *> &libs) {
|
||||
void *handle;
|
||||
|
||||
for(auto lib : libs) {
|
||||
handle = dlopen(lib, RTLD_LAZY | RTLD_LOCAL);
|
||||
if(handle) {
|
||||
return handle;
|
||||
}
|
||||
}
|
||||
|
||||
std::stringstream ss;
|
||||
ss << "Couldn't find any of the following libraries: ["sv << libs.front();
|
||||
std::for_each(std::begin(libs) + 1, std::end(libs), [&](auto lib) {
|
||||
ss << ", "sv << lib;
|
||||
});
|
||||
|
||||
ss << ']';
|
||||
|
||||
BOOST_LOG(error) << ss.str();
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int load(void *handle, const std::vector<std::tuple<apiproc *, const char *>> &funcs, bool strict) {
|
||||
int err = 0;
|
||||
for(auto &func : funcs) {
|
||||
TUPLE_2D_REF(fn, name, func);
|
||||
|
||||
*fn = SUNSHINE_GNUC_EXTENSION(apiproc) dlsym(handle, name);
|
||||
|
||||
if(!*fn && strict) {
|
||||
BOOST_LOG(error) << "Couldn't find function: "sv << name;
|
||||
|
||||
err = -1;
|
||||
}
|
||||
}
|
||||
|
||||
return err;
|
||||
}
|
||||
} // namespace dyn
|
||||
namespace platf {
|
||||
using ifaddr_t = util::safe_ptr<ifaddrs, freeifaddrs>;
|
||||
|
||||
ifaddr_t get_ifaddrs() {
|
||||
ifaddrs *p { nullptr };
|
||||
|
||||
getifaddrs(&p);
|
||||
|
||||
return ifaddr_t { p };
|
||||
}
|
||||
|
||||
fs::path appdata() {
|
||||
const char *homedir;
|
||||
if((homedir = getenv("HOME")) == nullptr) {
|
||||
homedir = getpwuid(geteuid())->pw_dir;
|
||||
}
|
||||
|
||||
return fs::path { homedir } / ".config/sunshine"sv;
|
||||
}
|
||||
|
||||
std::string from_sockaddr(const sockaddr *const ip_addr) {
|
||||
char data[INET6_ADDRSTRLEN];
|
||||
|
||||
auto family = ip_addr->sa_family;
|
||||
if(family == AF_INET6) {
|
||||
inet_ntop(AF_INET6, &((sockaddr_in6 *)ip_addr)->sin6_addr, data,
|
||||
INET6_ADDRSTRLEN);
|
||||
}
|
||||
|
||||
if(family == AF_INET) {
|
||||
inet_ntop(AF_INET, &((sockaddr_in *)ip_addr)->sin_addr, data,
|
||||
INET_ADDRSTRLEN);
|
||||
}
|
||||
|
||||
return std::string { data };
|
||||
}
|
||||
|
||||
std::pair<std::uint16_t, std::string> from_sockaddr_ex(const sockaddr *const ip_addr) {
|
||||
char data[INET6_ADDRSTRLEN];
|
||||
|
||||
auto family = ip_addr->sa_family;
|
||||
std::uint16_t port;
|
||||
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) {
|
||||
inet_ntop(AF_INET, &((sockaddr_in *)ip_addr)->sin_addr, data,
|
||||
INET_ADDRSTRLEN);
|
||||
port = ((sockaddr_in *)ip_addr)->sin_port;
|
||||
}
|
||||
|
||||
return { port, std::string { data } };
|
||||
}
|
||||
|
||||
std::string get_mac_address(const std::string_view &address) {
|
||||
auto ifaddrs = get_ifaddrs();
|
||||
for(auto pos = ifaddrs.get(); pos != nullptr; pos = pos->ifa_next) {
|
||||
if(pos->ifa_addr && address == from_sockaddr(pos->ifa_addr)) {
|
||||
std::ifstream mac_file("/sys/class/net/"s + pos->ifa_name + "/address");
|
||||
if(mac_file.good()) {
|
||||
std::string mac_address;
|
||||
std::getline(mac_file, mac_address);
|
||||
return mac_address;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_LOG(warning) << "Unable to find MAC address for "sv << address;
|
||||
return "00:00:00:00:00:00"s;
|
||||
}
|
||||
|
||||
namespace source {
|
||||
enum source_e : std::size_t {
|
||||
#ifdef SUNSHINE_BUILD_CUDA
|
||||
NVFBC,
|
||||
#endif
|
||||
#ifdef SUNSHINE_BUILD_WAYLAND
|
||||
WAYLAND,
|
||||
#endif
|
||||
#ifdef SUNSHINE_BUILD_DRM
|
||||
KMS,
|
||||
#endif
|
||||
#ifdef SUNSHINE_BUILD_X11
|
||||
X11,
|
||||
#endif
|
||||
MAX_FLAGS
|
||||
};
|
||||
} // namespace source
|
||||
|
||||
static std::bitset<source::MAX_FLAGS> sources;
|
||||
|
||||
#ifdef SUNSHINE_BUILD_CUDA
|
||||
std::vector<std::string> nvfbc_display_names();
|
||||
std::shared_ptr<display_t> nvfbc_display(mem_type_e hwdevice_type, const std::string &display_name, int framerate);
|
||||
|
||||
bool verify_nvfbc() {
|
||||
return !nvfbc_display_names().empty();
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef SUNSHINE_BUILD_WAYLAND
|
||||
std::vector<std::string> wl_display_names();
|
||||
std::shared_ptr<display_t> wl_display(mem_type_e hwdevice_type, const std::string &display_name, int framerate);
|
||||
|
||||
bool verify_wl() {
|
||||
return window_system == window_system_e::WAYLAND && !wl_display_names().empty();
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef SUNSHINE_BUILD_DRM
|
||||
std::vector<std::string> kms_display_names();
|
||||
std::shared_ptr<display_t> kms_display(mem_type_e hwdevice_type, const std::string &display_name, int framerate);
|
||||
|
||||
bool verify_kms() {
|
||||
return !kms_display_names().empty();
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef SUNSHINE_BUILD_X11
|
||||
std::vector<std::string> x11_display_names();
|
||||
std::shared_ptr<display_t> x11_display(mem_type_e hwdevice_type, const std::string &display_name, int framerate);
|
||||
|
||||
bool verify_x11() {
|
||||
return window_system == window_system_e::X11 && !x11_display_names().empty();
|
||||
}
|
||||
#endif
|
||||
|
||||
std::vector<std::string> display_names(mem_type_e hwdevice_type) {
|
||||
#ifdef SUNSHINE_BUILD_CUDA
|
||||
// display using NvFBC only supports mem_type_e::cuda
|
||||
if(sources[source::NVFBC] && hwdevice_type == mem_type_e::cuda) return nvfbc_display_names();
|
||||
#endif
|
||||
#ifdef SUNSHINE_BUILD_WAYLAND
|
||||
if(sources[source::WAYLAND]) return wl_display_names();
|
||||
#endif
|
||||
#ifdef SUNSHINE_BUILD_DRM
|
||||
if(sources[source::KMS]) return kms_display_names();
|
||||
#endif
|
||||
#ifdef SUNSHINE_BUILD_X11
|
||||
if(sources[source::X11]) return x11_display_names();
|
||||
#endif
|
||||
return {};
|
||||
}
|
||||
|
||||
std::shared_ptr<display_t> display(mem_type_e hwdevice_type, const std::string &display_name, int framerate) {
|
||||
#ifdef SUNSHINE_BUILD_CUDA
|
||||
if(sources[source::NVFBC] && hwdevice_type == mem_type_e::cuda) {
|
||||
BOOST_LOG(info) << "Screencasting with NvFBC"sv;
|
||||
return nvfbc_display(hwdevice_type, display_name, framerate);
|
||||
}
|
||||
#endif
|
||||
#ifdef SUNSHINE_BUILD_WAYLAND
|
||||
if(sources[source::WAYLAND]) {
|
||||
BOOST_LOG(info) << "Screencasting with Wayland's protocol"sv;
|
||||
return wl_display(hwdevice_type, display_name, framerate);
|
||||
}
|
||||
#endif
|
||||
#ifdef SUNSHINE_BUILD_DRM
|
||||
if(sources[source::KMS]) {
|
||||
BOOST_LOG(info) << "Screencasting with KMS"sv;
|
||||
return kms_display(hwdevice_type, display_name, framerate);
|
||||
}
|
||||
#endif
|
||||
#ifdef SUNSHINE_BUILD_X11
|
||||
if(sources[source::X11]) {
|
||||
BOOST_LOG(info) << "Screencasting with X11"sv;
|
||||
return x11_display(hwdevice_type, display_name, framerate);
|
||||
}
|
||||
#endif
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::unique_ptr<deinit_t> init() {
|
||||
// These are allowed to fail.
|
||||
gbm::init();
|
||||
va::init();
|
||||
|
||||
window_system = window_system_e::NONE;
|
||||
#ifdef SUNSHINE_BUILD_WAYLAND
|
||||
if(std::getenv("WAYLAND_DISPLAY")) {
|
||||
window_system = window_system_e::WAYLAND;
|
||||
}
|
||||
#endif
|
||||
#if defined(SUNSHINE_BUILD_X11) || defined(SUNSHINE_BUILD_CUDA)
|
||||
if(std::getenv("DISPLAY") && window_system != window_system_e::WAYLAND) {
|
||||
if(std::getenv("WAYLAND_DISPLAY")) {
|
||||
BOOST_LOG(warning) << "Wayland detected, yet sunshine will use X11 for screencasting, screencasting will only work on XWayland applications"sv;
|
||||
}
|
||||
|
||||
window_system = window_system_e::X11;
|
||||
}
|
||||
#endif
|
||||
#ifdef SUNSHINE_BUILD_CUDA
|
||||
if(verify_nvfbc()) {
|
||||
sources[source::NVFBC] = true;
|
||||
}
|
||||
#endif
|
||||
#ifdef SUNSHINE_BUILD_WAYLAND
|
||||
if(verify_wl()) {
|
||||
sources[source::WAYLAND] = true;
|
||||
}
|
||||
#endif
|
||||
#ifdef SUNSHINE_BUILD_DRM
|
||||
if(verify_kms()) {
|
||||
if(window_system == window_system_e::WAYLAND) {
|
||||
// On Wayland, using KMS, the cursor is unreliable.
|
||||
// Hide it by default
|
||||
display_cursor = false;
|
||||
}
|
||||
|
||||
sources[source::KMS] = true;
|
||||
}
|
||||
#endif
|
||||
#ifdef SUNSHINE_BUILD_X11
|
||||
if(verify_x11()) {
|
||||
sources[source::X11] = true;
|
||||
}
|
||||
#endif
|
||||
|
||||
if(sources.none()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if(!gladLoaderLoadEGL(EGL_NO_DISPLAY) || !eglGetPlatformDisplay) {
|
||||
BOOST_LOG(warning) << "Couldn't load EGL library"sv;
|
||||
}
|
||||
|
||||
return std::make_unique<deinit_t>();
|
||||
}
|
||||
} // namespace platf
|
||||
31
sunshine/platform/linux/misc.h
Normal file
31
sunshine/platform/linux/misc.h
Normal file
@@ -0,0 +1,31 @@
|
||||
#ifndef SUNSHINE_PLATFORM_MISC_H
|
||||
#define SUNSHINE_PLATFORM_MISC_H
|
||||
|
||||
#include <unistd.h>
|
||||
#include <vector>
|
||||
|
||||
#include "sunshine/utility.h"
|
||||
|
||||
KITTY_USING_MOVE_T(file_t, int, -1, {
|
||||
if(el >= 0) {
|
||||
close(el);
|
||||
}
|
||||
});
|
||||
|
||||
enum class window_system_e {
|
||||
NONE,
|
||||
X11,
|
||||
WAYLAND,
|
||||
};
|
||||
|
||||
extern window_system_e window_system;
|
||||
|
||||
namespace dyn {
|
||||
typedef void (*apiproc)(void);
|
||||
|
||||
int load(void *handle, const std::vector<std::tuple<apiproc *, const char *>> &funcs, bool strict = true);
|
||||
void *handle(const std::vector<const char *> &libs);
|
||||
|
||||
} // namespace dyn
|
||||
|
||||
#endif
|
||||
429
sunshine/platform/linux/publish.cpp
Normal file
429
sunshine/platform/linux/publish.cpp
Normal file
@@ -0,0 +1,429 @@
|
||||
|
||||
// adapted from https://www.avahi.org/doxygen/html/client-publish-service_8c-example.html
|
||||
#include <thread>
|
||||
|
||||
#include "misc.h"
|
||||
#include "sunshine/main.h"
|
||||
#include "sunshine/nvhttp.h"
|
||||
#include "sunshine/platform/common.h"
|
||||
#include "sunshine/utility.h"
|
||||
|
||||
using namespace std::literals;
|
||||
|
||||
namespace avahi {
|
||||
|
||||
/** Error codes used by avahi */
|
||||
enum err_e {
|
||||
OK = 0, /**< OK */
|
||||
ERR_FAILURE = -1, /**< Generic error code */
|
||||
ERR_BAD_STATE = -2, /**< Object was in a bad state */
|
||||
ERR_INVALID_HOST_NAME = -3, /**< Invalid host name */
|
||||
ERR_INVALID_DOMAIN_NAME = -4, /**< Invalid domain name */
|
||||
ERR_NO_NETWORK = -5, /**< No suitable network protocol available */
|
||||
ERR_INVALID_TTL = -6, /**< Invalid DNS TTL */
|
||||
ERR_IS_PATTERN = -7, /**< RR key is pattern */
|
||||
ERR_COLLISION = -8, /**< Name collision */
|
||||
ERR_INVALID_RECORD = -9, /**< Invalid RR */
|
||||
|
||||
ERR_INVALID_SERVICE_NAME = -10, /**< Invalid service name */
|
||||
ERR_INVALID_SERVICE_TYPE = -11, /**< Invalid service type */
|
||||
ERR_INVALID_PORT = -12, /**< Invalid port number */
|
||||
ERR_INVALID_KEY = -13, /**< Invalid key */
|
||||
ERR_INVALID_ADDRESS = -14, /**< Invalid address */
|
||||
ERR_TIMEOUT = -15, /**< Timeout reached */
|
||||
ERR_TOO_MANY_CLIENTS = -16, /**< Too many clients */
|
||||
ERR_TOO_MANY_OBJECTS = -17, /**< Too many objects */
|
||||
ERR_TOO_MANY_ENTRIES = -18, /**< Too many entries */
|
||||
ERR_OS = -19, /**< OS error */
|
||||
|
||||
ERR_ACCESS_DENIED = -20, /**< Access denied */
|
||||
ERR_INVALID_OPERATION = -21, /**< Invalid operation */
|
||||
ERR_DBUS_ERROR = -22, /**< An unexpected D-Bus error occurred */
|
||||
ERR_DISCONNECTED = -23, /**< Daemon connection failed */
|
||||
ERR_NO_MEMORY = -24, /**< Memory exhausted */
|
||||
ERR_INVALID_OBJECT = -25, /**< The object passed to this function was invalid */
|
||||
ERR_NO_DAEMON = -26, /**< Daemon not running */
|
||||
ERR_INVALID_INTERFACE = -27, /**< Invalid interface */
|
||||
ERR_INVALID_PROTOCOL = -28, /**< Invalid protocol */
|
||||
ERR_INVALID_FLAGS = -29, /**< Invalid flags */
|
||||
|
||||
ERR_NOT_FOUND = -30, /**< Not found */
|
||||
ERR_INVALID_CONFIG = -31, /**< Configuration error */
|
||||
ERR_VERSION_MISMATCH = -32, /**< Verson mismatch */
|
||||
ERR_INVALID_SERVICE_SUBTYPE = -33, /**< Invalid service subtype */
|
||||
ERR_INVALID_PACKET = -34, /**< Invalid packet */
|
||||
ERR_INVALID_DNS_ERROR = -35, /**< Invlaid DNS return code */
|
||||
ERR_DNS_FORMERR = -36, /**< DNS Error: Form error */
|
||||
ERR_DNS_SERVFAIL = -37, /**< DNS Error: Server Failure */
|
||||
ERR_DNS_NXDOMAIN = -38, /**< DNS Error: No such domain */
|
||||
ERR_DNS_NOTIMP = -39, /**< DNS Error: Not implemented */
|
||||
|
||||
ERR_DNS_REFUSED = -40, /**< DNS Error: Operation refused */
|
||||
ERR_DNS_YXDOMAIN = -41,
|
||||
ERR_DNS_YXRRSET = -42,
|
||||
ERR_DNS_NXRRSET = -43,
|
||||
ERR_DNS_NOTAUTH = -44, /**< DNS Error: Not authorized */
|
||||
ERR_DNS_NOTZONE = -45,
|
||||
ERR_INVALID_RDATA = -46, /**< Invalid RDATA */
|
||||
ERR_INVALID_DNS_CLASS = -47, /**< Invalid DNS class */
|
||||
ERR_INVALID_DNS_TYPE = -48, /**< Invalid DNS type */
|
||||
ERR_NOT_SUPPORTED = -49, /**< Not supported */
|
||||
|
||||
ERR_NOT_PERMITTED = -50, /**< Operation not permitted */
|
||||
ERR_INVALID_ARGUMENT = -51, /**< Invalid argument */
|
||||
ERR_IS_EMPTY = -52, /**< Is empty */
|
||||
ERR_NO_CHANGE = -53, /**< The requested operation is invalid because it is redundant */
|
||||
|
||||
ERR_MAX = -54
|
||||
};
|
||||
|
||||
constexpr auto IF_UNSPEC = -1;
|
||||
enum proto {
|
||||
PROTO_INET = 0, /**< IPv4 */
|
||||
PROTO_INET6 = 1, /**< IPv6 */
|
||||
PROTO_UNSPEC = -1 /**< Unspecified/all protocol(s) */
|
||||
};
|
||||
|
||||
enum ServerState {
|
||||
SERVER_INVALID, /**< Invalid state (initial) */
|
||||
SERVER_REGISTERING, /**< Host RRs are being registered */
|
||||
SERVER_RUNNING, /**< All host RRs have been established */
|
||||
SERVER_COLLISION, /**< There is a collision with a host RR. All host RRs have been withdrawn, the user should set a new host name via avahi_server_set_host_name() */
|
||||
SERVER_FAILURE /**< Some fatal failure happened, the server is unable to proceed */
|
||||
};
|
||||
|
||||
enum ClientState {
|
||||
CLIENT_S_REGISTERING = SERVER_REGISTERING, /**< Server state: REGISTERING */
|
||||
CLIENT_S_RUNNING = SERVER_RUNNING, /**< Server state: RUNNING */
|
||||
CLIENT_S_COLLISION = SERVER_COLLISION, /**< Server state: COLLISION */
|
||||
CLIENT_FAILURE = 100, /**< Some kind of error happened on the client side */
|
||||
CLIENT_CONNECTING = 101 /**< We're still connecting. This state is only entered when AVAHI_CLIENT_NO_FAIL has been passed to avahi_client_new() and the daemon is not yet available. */
|
||||
};
|
||||
|
||||
enum EntryGroupState {
|
||||
ENTRY_GROUP_UNCOMMITED, /**< The group has not yet been commited, the user must still call avahi_entry_group_commit() */
|
||||
ENTRY_GROUP_REGISTERING, /**< The entries of the group are currently being registered */
|
||||
ENTRY_GROUP_ESTABLISHED, /**< The entries have successfully been established */
|
||||
ENTRY_GROUP_COLLISION, /**< A name collision for one of the entries in the group has been detected, the entries have been withdrawn */
|
||||
ENTRY_GROUP_FAILURE /**< Some kind of failure happened, the entries have been withdrawn */
|
||||
};
|
||||
|
||||
enum ClientFlags {
|
||||
CLIENT_IGNORE_USER_CONFIG = 1, /**< Don't read user configuration */
|
||||
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 */
|
||||
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 */
|
||||
PUBLISH_NO_ANNOUNCE = 4, /**< For raw records: Do not announce this RR to other hosts */
|
||||
PUBLISH_ALLOW_MULTIPLE = 8, /**< For raw records: Allow multiple local records of this type, even if they are intended to be unique */
|
||||
/** \cond fulldocs */
|
||||
PUBLISH_NO_REVERSE = 16, /**< For address records: don't create a reverse (PTR) entry */
|
||||
PUBLISH_NO_COOKIE = 32, /**< For service records: do not implicitly add the local service cookie to TXT data */
|
||||
/** \endcond */
|
||||
PUBLISH_UPDATE = 64, /**< Update existing records instead of adding new ones */
|
||||
/** \cond fulldocs */
|
||||
PUBLISH_USE_WIDE_AREA = 128, /**< Register the record using wide area DNS (i.e. unicast DNS update) */
|
||||
PUBLISH_USE_MULTICAST = 256 /**< Register the record using multicast DNS */
|
||||
/** \endcond */
|
||||
};
|
||||
|
||||
using IfIndex = int;
|
||||
using Protocol = int;
|
||||
|
||||
struct EntryGroup;
|
||||
struct Poll;
|
||||
struct SimplePoll;
|
||||
struct Client;
|
||||
|
||||
typedef void (*ClientCallback)(Client *, ClientState, void *userdata);
|
||||
typedef void (*EntryGroupCallback)(EntryGroup *g, EntryGroupState state, void *userdata);
|
||||
|
||||
typedef void (*free_fn)(void *);
|
||||
|
||||
typedef Client *(*client_new_fn)(const Poll *poll_api, ClientFlags flags, ClientCallback callback, void *userdata, int *error);
|
||||
typedef void (*client_free_fn)(Client *);
|
||||
typedef char *(*alternative_service_name_fn)(char *);
|
||||
|
||||
typedef Client *(*entry_group_get_client_fn)(EntryGroup *);
|
||||
|
||||
typedef EntryGroup *(*entry_group_new_fn)(Client *, EntryGroupCallback, void *userdata);
|
||||
typedef int (*entry_group_add_service_fn)(
|
||||
EntryGroup *group,
|
||||
IfIndex interface,
|
||||
Protocol protocol,
|
||||
PublishFlags flags,
|
||||
const char *name,
|
||||
const char *type,
|
||||
const char *domain,
|
||||
const char *host,
|
||||
uint16_t port,
|
||||
...);
|
||||
|
||||
typedef int (*entry_group_is_empty_fn)(EntryGroup *);
|
||||
typedef int (*entry_group_reset_fn)(EntryGroup *);
|
||||
typedef int (*entry_group_commit_fn)(EntryGroup *);
|
||||
|
||||
typedef char *(*strdup_fn)(const char *);
|
||||
typedef char *(*strerror_fn)(int);
|
||||
typedef int (*client_errno_fn)(Client *);
|
||||
|
||||
typedef Poll *(*simple_poll_get_fn)(SimplePoll *);
|
||||
typedef int (*simple_poll_loop_fn)(SimplePoll *);
|
||||
typedef void (*simple_poll_quit_fn)(SimplePoll *);
|
||||
typedef SimplePoll *(*simple_poll_new_fn)();
|
||||
typedef void (*simple_poll_free_fn)(SimplePoll *);
|
||||
|
||||
free_fn free;
|
||||
client_new_fn client_new;
|
||||
client_free_fn client_free;
|
||||
alternative_service_name_fn alternative_service_name;
|
||||
entry_group_get_client_fn entry_group_get_client;
|
||||
entry_group_new_fn entry_group_new;
|
||||
entry_group_add_service_fn entry_group_add_service;
|
||||
entry_group_is_empty_fn entry_group_is_empty;
|
||||
entry_group_reset_fn entry_group_reset;
|
||||
entry_group_commit_fn entry_group_commit;
|
||||
strdup_fn strdup;
|
||||
strerror_fn strerror;
|
||||
client_errno_fn client_errno;
|
||||
simple_poll_get_fn simple_poll_get;
|
||||
simple_poll_loop_fn simple_poll_loop;
|
||||
simple_poll_quit_fn simple_poll_quit;
|
||||
simple_poll_new_fn simple_poll_new;
|
||||
simple_poll_free_fn simple_poll_free;
|
||||
|
||||
|
||||
int init_common() {
|
||||
static void *handle { nullptr };
|
||||
static bool funcs_loaded = false;
|
||||
|
||||
if(funcs_loaded) return 0;
|
||||
|
||||
if(!handle) {
|
||||
handle = dyn::handle({ "libavahi-common.so.3", "libavahi-common.so" });
|
||||
if(!handle) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::tuple<dyn::apiproc *, const char *>> funcs {
|
||||
{ (dyn::apiproc *)&alternative_service_name, "avahi_alternative_service_name" },
|
||||
{ (dyn::apiproc *)&free, "avahi_free" },
|
||||
{ (dyn::apiproc *)&strdup, "avahi_strdup" },
|
||||
{ (dyn::apiproc *)&strerror, "avahi_strerror" },
|
||||
{ (dyn::apiproc *)&simple_poll_get, "avahi_simple_poll_get" },
|
||||
{ (dyn::apiproc *)&simple_poll_loop, "avahi_simple_poll_loop" },
|
||||
{ (dyn::apiproc *)&simple_poll_quit, "avahi_simple_poll_quit" },
|
||||
{ (dyn::apiproc *)&simple_poll_new, "avahi_simple_poll_new" },
|
||||
{ (dyn::apiproc *)&simple_poll_free, "avahi_simple_poll_free" },
|
||||
};
|
||||
|
||||
if(dyn::load(handle, funcs)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
funcs_loaded = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int init_client() {
|
||||
if(init_common()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void *handle { nullptr };
|
||||
static bool funcs_loaded = false;
|
||||
|
||||
if(funcs_loaded) return 0;
|
||||
|
||||
if(!handle) {
|
||||
handle = dyn::handle({ "libavahi-client.so.3", "libavahi-client.so" });
|
||||
if(!handle) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::tuple<dyn::apiproc *, const char *>> funcs {
|
||||
{ (dyn::apiproc *)&client_new, "avahi_client_new" },
|
||||
{ (dyn::apiproc *)&client_free, "avahi_client_free" },
|
||||
{ (dyn::apiproc *)&entry_group_get_client, "avahi_entry_group_get_client" },
|
||||
{ (dyn::apiproc *)&entry_group_new, "avahi_entry_group_new" },
|
||||
{ (dyn::apiproc *)&entry_group_add_service, "avahi_entry_group_add_service" },
|
||||
{ (dyn::apiproc *)&entry_group_is_empty, "avahi_entry_group_is_empty" },
|
||||
{ (dyn::apiproc *)&entry_group_reset, "avahi_entry_group_reset" },
|
||||
{ (dyn::apiproc *)&entry_group_commit, "avahi_entry_group_commit" },
|
||||
{ (dyn::apiproc *)&client_errno, "avahi_client_errno" },
|
||||
};
|
||||
|
||||
if(dyn::load(handle, funcs)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
funcs_loaded = true;
|
||||
return 0;
|
||||
}
|
||||
} // namespace avahi
|
||||
|
||||
namespace platf::publish {
|
||||
|
||||
template<class T>
|
||||
void free(T *p) {
|
||||
avahi::free(p);
|
||||
}
|
||||
|
||||
template<class T>
|
||||
using ptr_t = util::safe_ptr<T, free<T>>;
|
||||
using client_t = util::dyn_safe_ptr<avahi::Client, &avahi::client_free>;
|
||||
using poll_t = util::dyn_safe_ptr<avahi::SimplePoll, &avahi::simple_poll_free>;
|
||||
|
||||
avahi::EntryGroup *group = nullptr;
|
||||
|
||||
poll_t poll;
|
||||
client_t client;
|
||||
|
||||
ptr_t<char> name;
|
||||
|
||||
void create_services(avahi::Client *c);
|
||||
|
||||
void entry_group_callback(avahi::EntryGroup *g, avahi::EntryGroupState state, void *) {
|
||||
group = g;
|
||||
|
||||
switch(state) {
|
||||
case avahi::ENTRY_GROUP_ESTABLISHED:
|
||||
BOOST_LOG(info) << "Avahi service " << name.get() << " successfully established.";
|
||||
break;
|
||||
case avahi::ENTRY_GROUP_COLLISION:
|
||||
name.reset(avahi::alternative_service_name(name.get()));
|
||||
|
||||
BOOST_LOG(info) << "Avahi service name collision, renaming service to " << name.get();
|
||||
|
||||
create_services(avahi::entry_group_get_client(g));
|
||||
break;
|
||||
case avahi::ENTRY_GROUP_FAILURE:
|
||||
BOOST_LOG(error) << "Avahi entry group failure: " << avahi::strerror(avahi::client_errno(avahi::entry_group_get_client(g)));
|
||||
avahi::simple_poll_quit(poll.get());
|
||||
break;
|
||||
case avahi::ENTRY_GROUP_UNCOMMITED:
|
||||
case avahi::ENTRY_GROUP_REGISTERING:;
|
||||
}
|
||||
}
|
||||
|
||||
void create_services(avahi::Client *c) {
|
||||
int ret;
|
||||
|
||||
auto fg = util::fail_guard([]() {
|
||||
avahi::simple_poll_quit(poll.get());
|
||||
});
|
||||
|
||||
if(!group) {
|
||||
if(!(group = avahi::entry_group_new(c, entry_group_callback, nullptr))) {
|
||||
BOOST_LOG(error) << "avahi::entry_group_new() failed: "sv << avahi::strerror(avahi::client_errno(c));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if(avahi::entry_group_is_empty(group)) {
|
||||
BOOST_LOG(info) << "Adding avahi service "sv << name.get();
|
||||
|
||||
ret = avahi::entry_group_add_service(
|
||||
group,
|
||||
avahi::IF_UNSPEC, avahi::PROTO_UNSPEC,
|
||||
avahi::PublishFlags(0),
|
||||
name.get(),
|
||||
SERVICE_TYPE,
|
||||
nullptr, nullptr,
|
||||
map_port(nvhttp::PORT_HTTP),
|
||||
nullptr);
|
||||
|
||||
if(ret < 0) {
|
||||
if(ret == avahi::ERR_COLLISION) {
|
||||
// A service name collision with a local service happened. Let's pick a new name
|
||||
name.reset(avahi::alternative_service_name(name.get()));
|
||||
BOOST_LOG(info) << "Service name collision, renaming service to "sv << name.get();
|
||||
|
||||
avahi::entry_group_reset(group);
|
||||
|
||||
create_services(c);
|
||||
|
||||
fg.disable();
|
||||
return;
|
||||
}
|
||||
|
||||
BOOST_LOG(error) << "Failed to add "sv << SERVICE_TYPE << " service: "sv << avahi::strerror(ret);
|
||||
return;
|
||||
}
|
||||
|
||||
ret = avahi::entry_group_commit(group);
|
||||
if(ret < 0) {
|
||||
BOOST_LOG(error) << "Failed to commit entry group: "sv << avahi::strerror(ret);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
fg.disable();
|
||||
}
|
||||
|
||||
void client_callback(avahi::Client *c, avahi::ClientState state, void *) {
|
||||
switch(state) {
|
||||
case avahi::CLIENT_S_RUNNING:
|
||||
create_services(c);
|
||||
break;
|
||||
case avahi::CLIENT_FAILURE:
|
||||
BOOST_LOG(error) << "Client failure: "sv << avahi::strerror(avahi::client_errno(c));
|
||||
avahi::simple_poll_quit(poll.get());
|
||||
break;
|
||||
case avahi::CLIENT_S_COLLISION:
|
||||
case avahi::CLIENT_S_REGISTERING:
|
||||
if(group)
|
||||
avahi::entry_group_reset(group);
|
||||
break;
|
||||
case avahi::CLIENT_CONNECTING:;
|
||||
}
|
||||
}
|
||||
|
||||
class deinit_t : public ::platf::deinit_t {
|
||||
public:
|
||||
std::thread poll_thread;
|
||||
|
||||
deinit_t(std::thread poll_thread) : poll_thread { std::move(poll_thread) } {}
|
||||
|
||||
~deinit_t() override {
|
||||
if(avahi::simple_poll_quit && poll) {
|
||||
avahi::simple_poll_quit(poll.get());
|
||||
}
|
||||
|
||||
if(poll_thread.joinable()) {
|
||||
poll_thread.join();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
[[nodiscard]] std::unique_ptr<::platf::deinit_t> start() {
|
||||
if(avahi::init_client()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int avhi_error;
|
||||
|
||||
poll.reset(avahi::simple_poll_new());
|
||||
if(!poll) {
|
||||
BOOST_LOG(error) << "Failed to create simple poll object."sv;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
name.reset(avahi::strdup(SERVICE_NAME));
|
||||
|
||||
client.reset(
|
||||
avahi::client_new(avahi::simple_poll_get(poll.get()), avahi::ClientFlags(0), client_callback, nullptr, &avhi_error));
|
||||
|
||||
if(!client) {
|
||||
BOOST_LOG(error) << "Failed to create client: "sv << avahi::strerror(avhi_error);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return std::make_unique<deinit_t>(std::thread { avahi::simple_poll_loop, poll.get() });
|
||||
}
|
||||
} // namespace platf::publish
|
||||
651
sunshine/platform/linux/vaapi.cpp
Normal file
651
sunshine/platform/linux/vaapi.cpp
Normal file
@@ -0,0 +1,651 @@
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
#include <fcntl.h>
|
||||
|
||||
extern "C" {
|
||||
#include <libavcodec/avcodec.h>
|
||||
}
|
||||
|
||||
#include "graphics.h"
|
||||
#include "misc.h"
|
||||
#include "sunshine/config.h"
|
||||
#include "sunshine/main.h"
|
||||
#include "sunshine/platform/common.h"
|
||||
#include "sunshine/utility.h"
|
||||
|
||||
using namespace std::literals;
|
||||
|
||||
extern "C" struct AVBufferRef;
|
||||
|
||||
namespace va {
|
||||
constexpr auto SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2 = 0x40000000;
|
||||
constexpr auto EXPORT_SURFACE_WRITE_ONLY = 0x0002;
|
||||
constexpr auto EXPORT_SURFACE_COMPOSED_LAYERS = 0x0008;
|
||||
|
||||
using VADisplay = void *;
|
||||
using VAStatus = int;
|
||||
using VAGenericID = unsigned int;
|
||||
using VASurfaceID = VAGenericID;
|
||||
|
||||
struct DRMPRIMESurfaceDescriptor {
|
||||
// VA Pixel format fourcc of the whole surface (VA_FOURCC_*).
|
||||
uint32_t fourcc;
|
||||
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
|
||||
// Number of distinct DRM objects making up the surface.
|
||||
uint32_t num_objects;
|
||||
|
||||
struct {
|
||||
// DRM PRIME file descriptor for this object.
|
||||
// Needs to be closed manually
|
||||
int fd;
|
||||
|
||||
/*
|
||||
* 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;
|
||||
} objects[4];
|
||||
|
||||
// Number of layers making up the surface.
|
||||
uint32_t num_layers;
|
||||
struct {
|
||||
// DRM format fourcc of this layer (DRM_FOURCC_*).
|
||||
uint32_t drm_format;
|
||||
|
||||
// Number of planes in this layer.
|
||||
uint32_t num_planes;
|
||||
|
||||
// references objects --> DRMPRIMESurfaceDescriptor.objects[object_index[0]]
|
||||
uint32_t object_index[4];
|
||||
|
||||
// Offset within the object of each plane.
|
||||
uint32_t offset[4];
|
||||
|
||||
// Pitch of each plane.
|
||||
uint32_t pitch[4];
|
||||
} layers[4];
|
||||
};
|
||||
|
||||
/** Currently defined profiles */
|
||||
enum class profile_e {
|
||||
// Profile ID used for video processing.
|
||||
ProfileNone = -1,
|
||||
MPEG2Simple = 0,
|
||||
MPEG2Main = 1,
|
||||
MPEG4Simple = 2,
|
||||
MPEG4AdvancedSimple = 3,
|
||||
MPEG4Main = 4,
|
||||
H264Baseline = 5,
|
||||
H264Main = 6,
|
||||
H264High = 7,
|
||||
VC1Simple = 8,
|
||||
VC1Main = 9,
|
||||
VC1Advanced = 10,
|
||||
H263Baseline = 11,
|
||||
JPEGBaseline = 12,
|
||||
H264ConstrainedBaseline = 13,
|
||||
VP8Version0_3 = 14,
|
||||
H264MultiviewHigh = 15,
|
||||
H264StereoHigh = 16,
|
||||
HEVCMain = 17,
|
||||
HEVCMain10 = 18,
|
||||
VP9Profile0 = 19,
|
||||
VP9Profile1 = 20,
|
||||
VP9Profile2 = 21,
|
||||
VP9Profile3 = 22,
|
||||
HEVCMain12 = 23,
|
||||
HEVCMain422_10 = 24,
|
||||
HEVCMain422_12 = 25,
|
||||
HEVCMain444 = 26,
|
||||
HEVCMain444_10 = 27,
|
||||
HEVCMain444_12 = 28,
|
||||
HEVCSccMain = 29,
|
||||
HEVCSccMain10 = 30,
|
||||
HEVCSccMain444 = 31,
|
||||
AV1Profile0 = 32,
|
||||
AV1Profile1 = 33,
|
||||
HEVCSccMain444_10 = 34,
|
||||
|
||||
// Profile ID used for protected video playback.
|
||||
Protected = 35
|
||||
};
|
||||
|
||||
enum class entry_e {
|
||||
VLD = 1,
|
||||
IZZ = 2,
|
||||
IDCT = 3,
|
||||
MoComp = 4,
|
||||
Deblocking = 5,
|
||||
EncSlice = 6, /* slice level encode */
|
||||
EncPicture = 7, /* pictuer 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
|
||||
* available with this entrypoint (e.g. interlace, MBAFF) and the
|
||||
* application can query the encoding configuration attributes to find
|
||||
* out more details if this entrypoint is supported.
|
||||
*/
|
||||
EncSliceLP = 8,
|
||||
VideoProc = 10, /**< Video pre/post-processing. */
|
||||
/**
|
||||
* \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.
|
||||
* The application can optionally provide input to ENC for extra encode control
|
||||
* and get the output from ENC. Application can chose to modify the ENC
|
||||
* output/PAK input during encoding, but the performance impact is significant.
|
||||
*
|
||||
* On top of the existing buffers for normal encode, there will be
|
||||
* one extra input buffer (VAEncMiscParameterFEIFrameControl) and
|
||||
* three extra output buffers (VAEncFEIMVBufferType, VAEncFEIMBModeBufferType
|
||||
* 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
|
||||
*
|
||||
* 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
|
||||
* optionally call the statistics function to get motion vectors and statistics like
|
||||
* variances, distortions before calling Encode function via this entry point.
|
||||
*
|
||||
* Checking whether Statistics is supported can be performed with vaQueryConfigEntrypoints().
|
||||
* If Statistics entry point is supported, then the list of returned entry-points will
|
||||
* include #Stats. Supported pixel format, maximum resolution and statistics
|
||||
* specific attributes can be obtained via normal attribute query. One input buffer
|
||||
* (VAStatsStatisticsParameterBufferType) and one or two output buffers
|
||||
* (VAStatsStatisticsBufferType, VAStatsStatisticsBottomFieldBufferType (for interlace only)
|
||||
* and VAStatsMVBufferType) are needed for this entry point.
|
||||
**/
|
||||
Stats = 12,
|
||||
/**
|
||||
* \brief ProtectedTEEComm
|
||||
*
|
||||
* A function for communicating with TEE (Trusted Execution Environment).
|
||||
**/
|
||||
ProtectedTEEComm = 13,
|
||||
/**
|
||||
* \brief ProtectedContent
|
||||
*
|
||||
* A function for protected content to decrypt encrypted content.
|
||||
**/
|
||||
ProtectedContent = 14,
|
||||
};
|
||||
|
||||
|
||||
typedef VAStatus (*queryConfigEntrypoints_fn)(VADisplay dpy, profile_e profile, entry_e *entrypoint_list, int *num_entrypoints);
|
||||
typedef int (*maxNumEntrypoints_fn)(VADisplay dpy);
|
||||
typedef VADisplay (*getDisplayDRM_fn)(int fd);
|
||||
typedef VAStatus (*terminate_fn)(VADisplay dpy);
|
||||
typedef VAStatus (*initialize_fn)(VADisplay dpy, int *major_version, int *minor_version);
|
||||
typedef const char *(*errorStr_fn)(VAStatus error_status);
|
||||
typedef void (*VAMessageCallback)(void *user_context, const char *message);
|
||||
typedef VAMessageCallback (*setErrorCallback_fn)(VADisplay dpy, VAMessageCallback callback, void *user_context);
|
||||
typedef VAMessageCallback (*setInfoCallback_fn)(VADisplay dpy, VAMessageCallback callback, void *user_context);
|
||||
typedef const char *(*queryVendorString_fn)(VADisplay dpy);
|
||||
typedef VAStatus (*exportSurfaceHandle_fn)(
|
||||
VADisplay dpy, VASurfaceID surface_id,
|
||||
uint32_t mem_type, uint32_t flags,
|
||||
void *descriptor);
|
||||
|
||||
static maxNumEntrypoints_fn maxNumEntrypoints;
|
||||
static queryConfigEntrypoints_fn queryConfigEntrypoints;
|
||||
static getDisplayDRM_fn getDisplayDRM;
|
||||
static terminate_fn terminate;
|
||||
static initialize_fn initialize;
|
||||
static errorStr_fn errorStr;
|
||||
static setErrorCallback_fn setErrorCallback;
|
||||
static setInfoCallback_fn setInfoCallback;
|
||||
static queryVendorString_fn queryVendorString;
|
||||
static exportSurfaceHandle_fn exportSurfaceHandle;
|
||||
|
||||
using display_t = util::dyn_safe_ptr_v2<void, VAStatus, &terminate>;
|
||||
|
||||
int init_main_va() {
|
||||
static void *handle { nullptr };
|
||||
static bool funcs_loaded = false;
|
||||
|
||||
if(funcs_loaded) return 0;
|
||||
|
||||
if(!handle) {
|
||||
handle = dyn::handle({ "libva.so.2", "libva.so" });
|
||||
if(!handle) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::tuple<dyn::apiproc *, const char *>> funcs {
|
||||
{ (dyn::apiproc *)&maxNumEntrypoints, "vaMaxNumEntrypoints" },
|
||||
{ (dyn::apiproc *)&queryConfigEntrypoints, "vaQueryConfigEntrypoints" },
|
||||
{ (dyn::apiproc *)&terminate, "vaTerminate" },
|
||||
{ (dyn::apiproc *)&initialize, "vaInitialize" },
|
||||
{ (dyn::apiproc *)&errorStr, "vaErrorStr" },
|
||||
{ (dyn::apiproc *)&setErrorCallback, "vaSetErrorCallback" },
|
||||
{ (dyn::apiproc *)&setInfoCallback, "vaSetInfoCallback" },
|
||||
{ (dyn::apiproc *)&queryVendorString, "vaQueryVendorString" },
|
||||
{ (dyn::apiproc *)&exportSurfaceHandle, "vaExportSurfaceHandle" },
|
||||
};
|
||||
|
||||
if(dyn::load(handle, funcs)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
funcs_loaded = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int init() {
|
||||
if(init_main_va()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void *handle { nullptr };
|
||||
static bool funcs_loaded = false;
|
||||
|
||||
if(funcs_loaded) return 0;
|
||||
|
||||
if(!handle) {
|
||||
handle = dyn::handle({ "libva-drm.so.2", "libva-drm.so" });
|
||||
if(!handle) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::tuple<dyn::apiproc *, const char *>> funcs {
|
||||
{ (dyn::apiproc *)&getDisplayDRM, "vaGetDisplayDRM" },
|
||||
};
|
||||
|
||||
if(dyn::load(handle, funcs)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
funcs_loaded = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int vaapi_make_hwdevice_ctx(platf::hwdevice_t *base, AVBufferRef **hw_device_buf);
|
||||
|
||||
class va_t : public platf::hwdevice_t {
|
||||
public:
|
||||
int init(int in_width, int in_height, file_t &&render_device) {
|
||||
file = std::move(render_device);
|
||||
|
||||
if(!va::initialize || !gbm::create_device) {
|
||||
if(!va::initialize) BOOST_LOG(warning) << "libva not initialized"sv;
|
||||
if(!gbm::create_device) BOOST_LOG(warning) << "libgbm not initialized"sv;
|
||||
return -1;
|
||||
}
|
||||
|
||||
this->data = (void *)vaapi_make_hwdevice_ctx;
|
||||
|
||||
gbm.reset(gbm::create_device(file.el));
|
||||
if(!gbm) {
|
||||
char string[1024];
|
||||
BOOST_LOG(error) << "Couldn't create GBM device: ["sv << strerror_r(errno, string, sizeof(string)) << ']';
|
||||
return -1;
|
||||
}
|
||||
|
||||
display = egl::make_display(gbm.get());
|
||||
if(!display) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
auto ctx_opt = egl::make_ctx(display.get());
|
||||
if(!ctx_opt) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
ctx = std::move(*ctx_opt);
|
||||
|
||||
width = in_width;
|
||||
height = in_height;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int set_frame(AVFrame *frame) override {
|
||||
this->hwframe.reset(frame);
|
||||
this->frame = frame;
|
||||
|
||||
if(av_hwframe_get_buffer(frame->hw_frames_ctx, frame, 0)) {
|
||||
BOOST_LOG(error) << "Couldn't get hwframe for VAAPI"sv;
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
va::DRMPRIMESurfaceDescriptor prime;
|
||||
va::VASurfaceID surface = (std::uintptr_t)frame->data[3];
|
||||
|
||||
auto status = va::exportSurfaceHandle(
|
||||
this->va_display,
|
||||
surface,
|
||||
va::SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2,
|
||||
va::EXPORT_SURFACE_WRITE_ONLY | va::EXPORT_SURFACE_COMPOSED_LAYERS,
|
||||
&prime);
|
||||
if(status) {
|
||||
|
||||
BOOST_LOG(error) << "Couldn't export va surface handle: ["sv << (int)surface << "]: "sv << va::errorStr(status);
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Keep track of file descriptors
|
||||
std::array<file_t, egl::nv12_img_t::num_fds> fds;
|
||||
for(int x = 0; x < prime.num_objects; ++x) {
|
||||
fds[x] = prime.objects[x].fd;
|
||||
}
|
||||
|
||||
auto nv12_opt = egl::import_target(
|
||||
display.get(),
|
||||
std::move(fds),
|
||||
{ (int)prime.width,
|
||||
(int)prime.height,
|
||||
{ prime.objects[prime.layers[0].object_index[0]].fd, -1, -1, -1 },
|
||||
0,
|
||||
0,
|
||||
{ prime.layers[0].pitch[0] },
|
||||
{ prime.layers[0].offset[0] } },
|
||||
{ (int)prime.width / 2,
|
||||
(int)prime.height / 2,
|
||||
{ prime.objects[prime.layers[0].object_index[1]].fd, -1, -1, -1 },
|
||||
0,
|
||||
0,
|
||||
{ prime.layers[0].pitch[1] },
|
||||
{ prime.layers[0].offset[1] } });
|
||||
|
||||
if(!nv12_opt) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
auto sws_opt = egl::sws_t::make(width, height, frame->width, frame->height);
|
||||
if(!sws_opt) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
this->sws = std::move(*sws_opt);
|
||||
this->nv12 = std::move(*nv12_opt);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void set_colorspace(std::uint32_t colorspace, std::uint32_t color_range) override {
|
||||
sws.set_colorspace(colorspace, color_range);
|
||||
}
|
||||
|
||||
va::display_t::pointer va_display;
|
||||
file_t file;
|
||||
|
||||
frame_t hwframe;
|
||||
|
||||
gbm::gbm_t gbm;
|
||||
egl::display_t display;
|
||||
egl::ctx_t ctx;
|
||||
|
||||
egl::sws_t sws;
|
||||
egl::nv12_t nv12;
|
||||
|
||||
int width, height;
|
||||
};
|
||||
|
||||
class va_ram_t : public va_t {
|
||||
public:
|
||||
int convert(platf::img_t &img) override {
|
||||
sws.load_ram(img);
|
||||
|
||||
sws.convert(nv12->buf);
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
class va_vram_t : public va_t {
|
||||
public:
|
||||
int convert(platf::img_t &img) override {
|
||||
auto &descriptor = (egl::img_descriptor_t &)img;
|
||||
|
||||
if(descriptor.sequence > sequence) {
|
||||
sequence = descriptor.sequence;
|
||||
|
||||
rgb = egl::rgb_t {};
|
||||
|
||||
auto rgb_opt = egl::import_source(display.get(), descriptor.sd);
|
||||
|
||||
if(!rgb_opt) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
rgb = std::move(*rgb_opt);
|
||||
}
|
||||
|
||||
sws.load_vram(descriptor, offset_x, offset_y, rgb->tex[0]);
|
||||
|
||||
sws.convert(nv12->buf);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int init(int in_width, int in_height, file_t &&render_device, int offset_x, int offset_y) {
|
||||
if(va_t::init(in_width, in_height, std::move(render_device))) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
sequence = 0;
|
||||
|
||||
this->offset_x = offset_x;
|
||||
this->offset_y = offset_y;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::uint64_t sequence;
|
||||
egl::rgb_t rgb;
|
||||
|
||||
int offset_x, offset_y;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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;
|
||||
int fd;
|
||||
} drm;
|
||||
int drm_fd;
|
||||
} VAAPIDevicePriv;
|
||||
|
||||
/**
|
||||
* VAAPI connection details.
|
||||
*
|
||||
* Allocated as AVHWDeviceContext.hwctx
|
||||
*/
|
||||
typedef struct AVVAAPIDeviceContext {
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
unsigned int driver_quirks;
|
||||
} AVVAAPIDeviceContext;
|
||||
|
||||
static void __log(void *level, const char *msg) {
|
||||
BOOST_LOG(*(boost::log::sources::severity_logger<int> *)level) << msg;
|
||||
}
|
||||
|
||||
int vaapi_make_hwdevice_ctx(platf::hwdevice_t *base, AVBufferRef **hw_device_buf) {
|
||||
if(!va::initialize) {
|
||||
BOOST_LOG(warning) << "libva not loaded"sv;
|
||||
return -1;
|
||||
}
|
||||
|
||||
if(!va::getDisplayDRM) {
|
||||
BOOST_LOG(warning) << "libva-drm not loaded"sv;
|
||||
return -1;
|
||||
}
|
||||
|
||||
auto va = (va::va_t *)base;
|
||||
auto fd = dup(va->file.el);
|
||||
|
||||
auto *priv = (VAAPIDevicePriv *)av_mallocz(sizeof(VAAPIDevicePriv));
|
||||
priv->drm_fd = fd;
|
||||
priv->drm.fd = fd;
|
||||
|
||||
auto fg = util::fail_guard([fd, priv]() {
|
||||
close(fd);
|
||||
av_free(priv);
|
||||
});
|
||||
|
||||
va::display_t display { va::getDisplayDRM(fd) };
|
||||
if(!display) {
|
||||
auto render_device = config::video.adapter_name.empty() ? "/dev/dri/renderD128" : config::video.adapter_name.c_str();
|
||||
|
||||
BOOST_LOG(error) << "Couldn't open a va display from DRM with device: "sv << render_device;
|
||||
return -1;
|
||||
}
|
||||
|
||||
va->va_display = display.get();
|
||||
|
||||
va::setErrorCallback(display.get(), __log, &error);
|
||||
va::setErrorCallback(display.get(), __log, &info);
|
||||
|
||||
int major, minor;
|
||||
auto status = va::initialize(display.get(), &major, &minor);
|
||||
if(status) {
|
||||
BOOST_LOG(error) << "Couldn't initialize va display: "sv << va::errorStr(status);
|
||||
return -1;
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
fg.disable();
|
||||
|
||||
auto err = av_hwdevice_ctx_init(*hw_device_buf);
|
||||
if(err) {
|
||||
char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 };
|
||||
BOOST_LOG(error) << "Failed to create FFMpeg hardware device context: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err);
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static bool query(display_t::pointer display, profile_e profile) {
|
||||
std::vector<entry_e> entrypoints;
|
||||
entrypoints.resize(maxNumEntrypoints(display));
|
||||
|
||||
int count;
|
||||
auto status = queryConfigEntrypoints(display, profile, entrypoints.data(), &count);
|
||||
if(status) {
|
||||
BOOST_LOG(error) << "Couldn't query entrypoints: "sv << va::errorStr(status);
|
||||
return false;
|
||||
}
|
||||
entrypoints.resize(count);
|
||||
|
||||
for(auto entrypoint : entrypoints) {
|
||||
if(entrypoint == entry_e::EncSlice || entrypoint == entry_e::EncSliceLP) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool validate(int fd) {
|
||||
if(init()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
va::display_t display { va::getDisplayDRM(fd) };
|
||||
if(!display) {
|
||||
char string[1024];
|
||||
|
||||
auto bytes = readlink(("/proc/self/fd/" + std::to_string(fd)).c_str(), string, sizeof(string));
|
||||
|
||||
std::string_view render_device { string, (std::size_t)bytes };
|
||||
|
||||
BOOST_LOG(error) << "Couldn't open a va display from DRM with device: "sv << render_device;
|
||||
return false;
|
||||
}
|
||||
|
||||
int major, minor;
|
||||
auto status = initialize(display.get(), &major, &minor);
|
||||
if(status) {
|
||||
BOOST_LOG(error) << "Couldn't initialize va display: "sv << va::errorStr(status);
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!query(display.get(), profile_e::H264Main)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(config::video.hevc_mode > 1 && !query(display.get(), profile_e::HEVCMain)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(config::video.hevc_mode > 2 && !query(display.get(), profile_e::HEVCMain10)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::shared_ptr<platf::hwdevice_t> make_hwdevice(int width, int height, file_t &&card, int offset_x, int offset_y, bool vram) {
|
||||
if(vram) {
|
||||
auto egl = std::make_shared<va::va_vram_t>();
|
||||
if(egl->init(width, height, std::move(card), offset_x, offset_y)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return egl;
|
||||
}
|
||||
|
||||
else {
|
||||
auto egl = std::make_shared<va::va_ram_t>();
|
||||
if(egl->init(width, height, std::move(card))) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return egl;
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<platf::hwdevice_t> make_hwdevice(int width, int height, int offset_x, int offset_y, bool vram) {
|
||||
auto render_device = config::video.adapter_name.empty() ? "/dev/dri/renderD128" : config::video.adapter_name.c_str();
|
||||
|
||||
file_t file = open(render_device, O_RDWR);
|
||||
if(file.el < 0) {
|
||||
char string[1024];
|
||||
BOOST_LOG(error) << "Couldn't open "sv << render_device << ": " << strerror_r(errno, string, sizeof(string));
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return make_hwdevice(width, height, std::move(file), offset_x, offset_y, vram);
|
||||
}
|
||||
|
||||
std::shared_ptr<platf::hwdevice_t> make_hwdevice(int width, int height, bool vram) {
|
||||
return make_hwdevice(width, height, 0, 0, vram);
|
||||
}
|
||||
} // namespace va
|
||||
27
sunshine/platform/linux/vaapi.h
Normal file
27
sunshine/platform/linux/vaapi.h
Normal file
@@ -0,0 +1,27 @@
|
||||
#ifndef SUNSHINE_VAAPI_H
|
||||
#define SUNSHINE_VAAPI_H
|
||||
|
||||
#include "misc.h"
|
||||
#include "sunshine/platform/common.h"
|
||||
|
||||
namespace egl {
|
||||
struct surface_descriptor_t;
|
||||
}
|
||||
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
|
||||
*/
|
||||
std::shared_ptr<platf::hwdevice_t> make_hwdevice(int width, int height, bool vram);
|
||||
std::shared_ptr<platf::hwdevice_t> make_hwdevice(int width, int height, int offset_x, int offset_y, bool vram);
|
||||
std::shared_ptr<platf::hwdevice_t> make_hwdevice(int width, int height, file_t &&card, int offset_x, int offset_y, bool vram);
|
||||
|
||||
// Ensure the render device pointed to by fd is capable of encoding h264 with the hevc_mode configured
|
||||
bool validate(int fd);
|
||||
|
||||
int init();
|
||||
} // namespace va
|
||||
#endif
|
||||
268
sunshine/platform/linux/wayland.cpp
Normal file
268
sunshine/platform/linux/wayland.cpp
Normal file
@@ -0,0 +1,268 @@
|
||||
#include <wayland-client.h>
|
||||
#include <wayland-util.h>
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
#include "graphics.h"
|
||||
#include "sunshine/main.h"
|
||||
#include "sunshine/platform/common.h"
|
||||
#include "sunshine/round_robin.h"
|
||||
#include "sunshine/utility.h"
|
||||
#include "wayland.h"
|
||||
|
||||
extern const wl_interface wl_output_interface;
|
||||
|
||||
using namespace std::literals;
|
||||
|
||||
// Disable warning for converting incompatible functions
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wpedantic"
|
||||
#pragma GCC diagnostic ignored "-Wpmf-conversions"
|
||||
|
||||
namespace wl {
|
||||
int display_t::init(const char *display_name) {
|
||||
if(!display_name) {
|
||||
display_name = std::getenv("WAYLAND_DISPLAY");
|
||||
}
|
||||
|
||||
if(!display_name) {
|
||||
BOOST_LOG(error) << "Environment variable WAYLAND_DISPLAY has not been defined"sv;
|
||||
return -1;
|
||||
}
|
||||
|
||||
display_internal.reset(wl_display_connect(display_name));
|
||||
if(!display_internal) {
|
||||
BOOST_LOG(error) << "Couldn't connect to Wayland display: "sv << display_name;
|
||||
return -1;
|
||||
}
|
||||
|
||||
BOOST_LOG(info) << "Found display ["sv << display_name << ']';
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void display_t::roundtrip() {
|
||||
wl_display_roundtrip(display_internal.get());
|
||||
}
|
||||
|
||||
wl_registry *display_t::registry() {
|
||||
return wl_display_get_registry(display_internal.get());
|
||||
}
|
||||
|
||||
inline monitor_t::monitor_t(wl_output *output) : output { output } {}
|
||||
|
||||
inline void monitor_t::xdg_name(zxdg_output_v1 *, const char *name) {
|
||||
this->name = name;
|
||||
|
||||
BOOST_LOG(info) << "Name: "sv << this->name;
|
||||
}
|
||||
|
||||
void monitor_t::xdg_description(zxdg_output_v1 *, const char *description) {
|
||||
this->description = description;
|
||||
|
||||
BOOST_LOG(info) << "Found monitor: "sv << this->description;
|
||||
}
|
||||
|
||||
void monitor_t::xdg_position(zxdg_output_v1 *, std::int32_t x, std::int32_t y) {
|
||||
viewport.offset_x = x;
|
||||
viewport.offset_y = y;
|
||||
|
||||
BOOST_LOG(info) << "Offset: "sv << x << 'x' << y;
|
||||
}
|
||||
|
||||
void monitor_t::xdg_size(zxdg_output_v1 *, std::int32_t width, std::int32_t height) {
|
||||
viewport.width = width;
|
||||
viewport.height = height;
|
||||
|
||||
BOOST_LOG(info) << "Resolution: "sv << width << 'x' << height;
|
||||
}
|
||||
|
||||
void monitor_t::xdg_done(zxdg_output_v1 *) {
|
||||
BOOST_LOG(info) << "All info about monitor ["sv << name << "] has been send"sv;
|
||||
}
|
||||
|
||||
void monitor_t::listen(zxdg_output_manager_v1 *output_manager) {
|
||||
auto xdg_output = zxdg_output_manager_v1_get_xdg_output(output_manager, output);
|
||||
|
||||
#define CLASS_CALL(x, y) x = (decltype(x))&y
|
||||
|
||||
CLASS_CALL(listener.name, monitor_t::xdg_name);
|
||||
CLASS_CALL(listener.logical_size, monitor_t::xdg_size);
|
||||
CLASS_CALL(listener.logical_position, monitor_t::xdg_position);
|
||||
CLASS_CALL(listener.done, monitor_t::xdg_done);
|
||||
CLASS_CALL(listener.description, monitor_t::xdg_description);
|
||||
|
||||
#undef CLASS_CALL
|
||||
zxdg_output_v1_add_listener(xdg_output, &listener, this);
|
||||
}
|
||||
|
||||
interface_t::interface_t() noexcept
|
||||
: output_manager { nullptr }, listener {
|
||||
(decltype(wl_registry_listener::global))&interface_t::add_interface,
|
||||
(decltype(wl_registry_listener::global_remove))&interface_t::del_interface,
|
||||
} {}
|
||||
|
||||
void interface_t::listen(wl_registry *registry) {
|
||||
wl_registry_add_listener(registry, &listener, this);
|
||||
}
|
||||
|
||||
void interface_t::add_interface(wl_registry *registry, std::uint32_t id, const char *interface, std::uint32_t version) {
|
||||
BOOST_LOG(debug) << "Available interface: "sv << interface << '(' << id << ") version "sv << version;
|
||||
|
||||
if(!std::strcmp(interface, wl_output_interface.name)) {
|
||||
BOOST_LOG(info) << "Found interface: "sv << interface << '(' << id << ") version "sv << version;
|
||||
monitors.emplace_back(
|
||||
std::make_unique<monitor_t>(
|
||||
(wl_output *)wl_registry_bind(registry, id, &wl_output_interface, version)));
|
||||
}
|
||||
else if(!std::strcmp(interface, zxdg_output_manager_v1_interface.name)) {
|
||||
BOOST_LOG(info) << "Found interface: "sv << interface << '(' << id << ") version "sv << version;
|
||||
output_manager = (zxdg_output_manager_v1 *)wl_registry_bind(registry, id, &zxdg_output_manager_v1_interface, version);
|
||||
|
||||
this->interface[XDG_OUTPUT] = true;
|
||||
}
|
||||
else if(!std::strcmp(interface, zwlr_export_dmabuf_manager_v1_interface.name)) {
|
||||
BOOST_LOG(info) << "Found interface: "sv << interface << '(' << id << ") version "sv << version;
|
||||
dmabuf_manager = (zwlr_export_dmabuf_manager_v1 *)wl_registry_bind(registry, id, &zwlr_export_dmabuf_manager_v1_interface, version);
|
||||
|
||||
this->interface[WLR_EXPORT_DMABUF] = true;
|
||||
}
|
||||
}
|
||||
|
||||
void interface_t::del_interface(wl_registry *registry, uint32_t id) {
|
||||
BOOST_LOG(info) << "Delete: "sv << id;
|
||||
}
|
||||
|
||||
dmabuf_t::dmabuf_t()
|
||||
: status { READY }, frames {}, current_frame { &frames[0] }, listener {
|
||||
(decltype(zwlr_export_dmabuf_frame_v1_listener::frame))&dmabuf_t::frame,
|
||||
(decltype(zwlr_export_dmabuf_frame_v1_listener::object))&dmabuf_t::object,
|
||||
(decltype(zwlr_export_dmabuf_frame_v1_listener::ready))&dmabuf_t::ready,
|
||||
(decltype(zwlr_export_dmabuf_frame_v1_listener::cancel))&dmabuf_t::cancel,
|
||||
} {
|
||||
}
|
||||
|
||||
void dmabuf_t::listen(zwlr_export_dmabuf_manager_v1 *dmabuf_manager, wl_output *output, bool blend_cursor) {
|
||||
auto frame = zwlr_export_dmabuf_manager_v1_capture_output(dmabuf_manager, blend_cursor, output);
|
||||
zwlr_export_dmabuf_frame_v1_add_listener(frame, &listener, this);
|
||||
|
||||
status = WAITING;
|
||||
}
|
||||
|
||||
dmabuf_t::~dmabuf_t() {
|
||||
for(auto &frame : frames) {
|
||||
frame.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
void dmabuf_t::frame(
|
||||
zwlr_export_dmabuf_frame_v1 *frame,
|
||||
std::uint32_t width, std::uint32_t height,
|
||||
std::uint32_t x, std::uint32_t y,
|
||||
std::uint32_t buffer_flags, std::uint32_t flags,
|
||||
std::uint32_t format,
|
||||
std::uint32_t high, std::uint32_t low,
|
||||
std::uint32_t obj_count) {
|
||||
auto next_frame = get_next_frame();
|
||||
|
||||
next_frame->sd.fourcc = format;
|
||||
next_frame->sd.width = width;
|
||||
next_frame->sd.height = height;
|
||||
next_frame->sd.modifier = (((std::uint64_t)high) << 32) | low;
|
||||
}
|
||||
|
||||
void dmabuf_t::object(
|
||||
zwlr_export_dmabuf_frame_v1 *frame,
|
||||
std::uint32_t index,
|
||||
std::int32_t fd,
|
||||
std::uint32_t size,
|
||||
std::uint32_t offset,
|
||||
std::uint32_t stride,
|
||||
std::uint32_t plane_index) {
|
||||
auto next_frame = get_next_frame();
|
||||
|
||||
next_frame->sd.fds[plane_index] = fd;
|
||||
next_frame->sd.pitches[plane_index] = stride;
|
||||
next_frame->sd.offsets[plane_index] = offset;
|
||||
}
|
||||
|
||||
void dmabuf_t::ready(
|
||||
zwlr_export_dmabuf_frame_v1 *frame,
|
||||
std::uint32_t tv_sec_hi, std::uint32_t tv_sec_lo, std::uint32_t tv_nsec) {
|
||||
|
||||
zwlr_export_dmabuf_frame_v1_destroy(frame);
|
||||
|
||||
current_frame->destroy();
|
||||
current_frame = get_next_frame();
|
||||
|
||||
status = READY;
|
||||
}
|
||||
|
||||
void dmabuf_t::cancel(
|
||||
zwlr_export_dmabuf_frame_v1 *frame,
|
||||
zwlr_export_dmabuf_frame_v1_cancel_reason reason) {
|
||||
|
||||
zwlr_export_dmabuf_frame_v1_destroy(frame);
|
||||
|
||||
auto next_frame = get_next_frame();
|
||||
next_frame->destroy();
|
||||
|
||||
status = REINIT;
|
||||
}
|
||||
|
||||
void frame_t::destroy() {
|
||||
for(auto x = 0; x < 4; ++x) {
|
||||
if(sd.fds[x] >= 0) {
|
||||
close(sd.fds[x]);
|
||||
|
||||
sd.fds[x] = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
frame_t::frame_t() {
|
||||
// File descriptors aren't open
|
||||
std::fill_n(sd.fds, 4, -1);
|
||||
};
|
||||
|
||||
std::vector<std::unique_ptr<monitor_t>> monitors(const char *display_name) {
|
||||
display_t display;
|
||||
|
||||
if(display.init(display_name)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
interface_t interface;
|
||||
interface.listen(display.registry());
|
||||
|
||||
display.roundtrip();
|
||||
|
||||
if(!interface[interface_t::XDG_OUTPUT]) {
|
||||
BOOST_LOG(error) << "Missing Wayland wire XDG_OUTPUT"sv;
|
||||
return {};
|
||||
}
|
||||
|
||||
for(auto &monitor : interface.monitors) {
|
||||
monitor->listen(interface.output_manager);
|
||||
}
|
||||
|
||||
display.roundtrip();
|
||||
|
||||
return std::move(interface.monitors);
|
||||
}
|
||||
|
||||
static bool validate() {
|
||||
display_t display;
|
||||
|
||||
return display.init() == 0;
|
||||
}
|
||||
|
||||
int init() {
|
||||
static bool validated = validate();
|
||||
|
||||
return !validated;
|
||||
}
|
||||
|
||||
} // namespace wl
|
||||
|
||||
#pragma GCC diagnostic pop
|
||||
216
sunshine/platform/linux/wayland.h
Normal file
216
sunshine/platform/linux/wayland.h
Normal file
@@ -0,0 +1,216 @@
|
||||
#ifndef SUNSHINE_WAYLAND_H
|
||||
#define SUNSHINE_WAYLAND_H
|
||||
|
||||
#include <bitset>
|
||||
|
||||
#ifdef SUNSHINE_BUILD_WAYLAND
|
||||
#include <wlr-export-dmabuf-unstable-v1.h>
|
||||
#include <xdg-output-unstable-v1.h>
|
||||
#endif
|
||||
|
||||
#include "graphics.h"
|
||||
|
||||
/**
|
||||
* The classes defined in this macro block should only be used by
|
||||
* cpp files whose compilation depends on SUNSHINE_BUILD_WAYLAND
|
||||
*/
|
||||
#ifdef SUNSHINE_BUILD_WAYLAND
|
||||
|
||||
namespace wl {
|
||||
using display_internal_t = util::safe_ptr<wl_display, wl_display_disconnect>;
|
||||
|
||||
class frame_t {
|
||||
public:
|
||||
frame_t();
|
||||
egl::surface_descriptor_t sd;
|
||||
|
||||
void destroy();
|
||||
};
|
||||
|
||||
class dmabuf_t {
|
||||
public:
|
||||
enum status_e {
|
||||
WAITING,
|
||||
READY,
|
||||
REINIT,
|
||||
};
|
||||
|
||||
dmabuf_t(dmabuf_t &&) = delete;
|
||||
dmabuf_t(const dmabuf_t &) = delete;
|
||||
|
||||
dmabuf_t &operator=(const dmabuf_t &) = delete;
|
||||
dmabuf_t &operator=(dmabuf_t &&) = delete;
|
||||
|
||||
dmabuf_t();
|
||||
|
||||
void listen(zwlr_export_dmabuf_manager_v1 *dmabuf_manager, wl_output *output, bool blend_cursor = false);
|
||||
|
||||
~dmabuf_t();
|
||||
|
||||
void frame(
|
||||
zwlr_export_dmabuf_frame_v1 *frame,
|
||||
std::uint32_t width, std::uint32_t height,
|
||||
std::uint32_t x, std::uint32_t y,
|
||||
std::uint32_t buffer_flags, std::uint32_t flags,
|
||||
std::uint32_t format,
|
||||
std::uint32_t high, std::uint32_t low,
|
||||
std::uint32_t obj_count);
|
||||
|
||||
void object(
|
||||
zwlr_export_dmabuf_frame_v1 *frame,
|
||||
std::uint32_t index,
|
||||
std::int32_t fd,
|
||||
std::uint32_t size,
|
||||
std::uint32_t offset,
|
||||
std::uint32_t stride,
|
||||
std::uint32_t plane_index);
|
||||
|
||||
void ready(
|
||||
zwlr_export_dmabuf_frame_v1 *frame,
|
||||
std::uint32_t tv_sec_hi, std::uint32_t tv_sec_lo, std::uint32_t tv_nsec);
|
||||
|
||||
void cancel(
|
||||
zwlr_export_dmabuf_frame_v1 *frame,
|
||||
zwlr_export_dmabuf_frame_v1_cancel_reason reason);
|
||||
|
||||
inline frame_t *get_next_frame() {
|
||||
return current_frame == &frames[0] ? &frames[1] : &frames[0];
|
||||
}
|
||||
|
||||
status_e status;
|
||||
|
||||
std::array<frame_t, 2> frames;
|
||||
frame_t *current_frame;
|
||||
|
||||
zwlr_export_dmabuf_frame_v1_listener listener;
|
||||
};
|
||||
|
||||
class monitor_t {
|
||||
public:
|
||||
monitor_t(monitor_t &&) = delete;
|
||||
monitor_t(const monitor_t &) = delete;
|
||||
|
||||
monitor_t &operator=(const monitor_t &) = delete;
|
||||
monitor_t &operator=(monitor_t &&) = delete;
|
||||
|
||||
monitor_t(wl_output *output);
|
||||
|
||||
void xdg_name(zxdg_output_v1 *, const char *name);
|
||||
void xdg_description(zxdg_output_v1 *, const char *description);
|
||||
void xdg_position(zxdg_output_v1 *, std::int32_t x, std::int32_t y);
|
||||
void xdg_size(zxdg_output_v1 *, std::int32_t width, std::int32_t height);
|
||||
void xdg_done(zxdg_output_v1 *);
|
||||
|
||||
void listen(zxdg_output_manager_v1 *output_manager);
|
||||
|
||||
wl_output *output;
|
||||
|
||||
std::string name;
|
||||
std::string description;
|
||||
|
||||
platf::touch_port_t viewport;
|
||||
|
||||
zxdg_output_v1_listener listener;
|
||||
};
|
||||
|
||||
class interface_t {
|
||||
struct bind_t {
|
||||
std::uint32_t id;
|
||||
std::uint32_t version;
|
||||
};
|
||||
|
||||
public:
|
||||
enum interface_e {
|
||||
XDG_OUTPUT,
|
||||
WLR_EXPORT_DMABUF,
|
||||
MAX_INTERFACES,
|
||||
};
|
||||
|
||||
interface_t(interface_t &&) = delete;
|
||||
interface_t(const interface_t &) = delete;
|
||||
|
||||
interface_t &operator=(const interface_t &) = delete;
|
||||
interface_t &operator=(interface_t &&) = delete;
|
||||
|
||||
interface_t() noexcept;
|
||||
|
||||
void listen(wl_registry *registry);
|
||||
|
||||
std::vector<std::unique_ptr<monitor_t>> monitors;
|
||||
|
||||
zwlr_export_dmabuf_manager_v1 *dmabuf_manager;
|
||||
zxdg_output_manager_v1 *output_manager;
|
||||
|
||||
bool operator[](interface_e bit) const {
|
||||
return interface[bit];
|
||||
}
|
||||
|
||||
private:
|
||||
void add_interface(wl_registry *registry, std::uint32_t id, const char *interface, std::uint32_t version);
|
||||
void del_interface(wl_registry *registry, uint32_t id);
|
||||
|
||||
std::bitset<MAX_INTERFACES> interface;
|
||||
|
||||
wl_registry_listener listener;
|
||||
};
|
||||
|
||||
class display_t {
|
||||
public:
|
||||
/**
|
||||
* Initialize display with display_name
|
||||
* If display_name == nullptr -> display_name = std::getenv("WAYLAND_DISPLAY")
|
||||
*/
|
||||
int init(const char *display_name = nullptr);
|
||||
|
||||
// Roundtrip with Wayland connection
|
||||
void roundtrip();
|
||||
|
||||
// Get the registry associated with the display
|
||||
// No need to manually free the registry
|
||||
wl_registry *registry();
|
||||
|
||||
inline display_internal_t::pointer get() {
|
||||
return display_internal.get();
|
||||
}
|
||||
|
||||
private:
|
||||
display_internal_t display_internal;
|
||||
};
|
||||
|
||||
std::vector<std::unique_ptr<monitor_t>> monitors(const char *display_name = nullptr);
|
||||
|
||||
int init();
|
||||
} // namespace wl
|
||||
#else
|
||||
|
||||
struct wl_output;
|
||||
struct zxdg_output_manager_v1;
|
||||
|
||||
namespace wl {
|
||||
class monitor_t {
|
||||
public:
|
||||
monitor_t(monitor_t &&) = delete;
|
||||
monitor_t(const monitor_t &) = delete;
|
||||
|
||||
monitor_t &operator=(const monitor_t &) = delete;
|
||||
monitor_t &operator=(monitor_t &&) = delete;
|
||||
|
||||
monitor_t(wl_output *output);
|
||||
|
||||
void listen(zxdg_output_manager_v1 *output_manager);
|
||||
|
||||
wl_output *output;
|
||||
|
||||
std::string name;
|
||||
std::string description;
|
||||
|
||||
platf::touch_port_t viewport;
|
||||
};
|
||||
|
||||
inline std::vector<std::unique_ptr<monitor_t>> monitors(const char *display_name = nullptr) { return {}; }
|
||||
|
||||
inline int init() { return -1; }
|
||||
} // namespace wl
|
||||
#endif
|
||||
|
||||
#endif
|
||||
369
sunshine/platform/linux/wlgrab.cpp
Normal file
369
sunshine/platform/linux/wlgrab.cpp
Normal file
@@ -0,0 +1,369 @@
|
||||
#include "sunshine/platform/common.h"
|
||||
|
||||
#include "sunshine/main.h"
|
||||
#include "vaapi.h"
|
||||
#include "wayland.h"
|
||||
|
||||
using namespace std::literals;
|
||||
namespace wl {
|
||||
static int env_width;
|
||||
static int env_height;
|
||||
|
||||
struct img_t : public platf::img_t {
|
||||
~img_t() override {
|
||||
delete[] data;
|
||||
data = nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
class wlr_t : public platf::display_t {
|
||||
public:
|
||||
int init(platf::mem_type_e hwdevice_type, const std::string &display_name, int framerate) {
|
||||
delay = std::chrono::nanoseconds { 1s } / framerate;
|
||||
mem_type = hwdevice_type;
|
||||
|
||||
if(display.init()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
interface.listen(display.registry());
|
||||
|
||||
display.roundtrip();
|
||||
|
||||
if(!interface[wl::interface_t::XDG_OUTPUT]) {
|
||||
BOOST_LOG(error) << "Missing Wayland wire for xdg_output"sv;
|
||||
return -1;
|
||||
}
|
||||
|
||||
if(!interface[wl::interface_t::WLR_EXPORT_DMABUF]) {
|
||||
BOOST_LOG(error) << "Missing Wayland wire for wlr-export-dmabuf"sv;
|
||||
return -1;
|
||||
}
|
||||
|
||||
auto monitor = interface.monitors[0].get();
|
||||
|
||||
if(!display_name.empty()) {
|
||||
auto streamedMonitor = util::from_view(display_name);
|
||||
|
||||
if(streamedMonitor >= 0 && streamedMonitor < interface.monitors.size()) {
|
||||
monitor = interface.monitors[streamedMonitor].get();
|
||||
}
|
||||
}
|
||||
|
||||
monitor->listen(interface.output_manager);
|
||||
|
||||
display.roundtrip();
|
||||
|
||||
output = monitor->output;
|
||||
|
||||
offset_x = monitor->viewport.offset_x;
|
||||
offset_y = monitor->viewport.offset_y;
|
||||
width = monitor->viewport.width;
|
||||
height = monitor->viewport.height;
|
||||
|
||||
this->env_width = ::wl::env_width;
|
||||
this->env_height = ::wl::env_height;
|
||||
|
||||
BOOST_LOG(info) << "Selected monitor ["sv << monitor->description << "] for streaming"sv;
|
||||
BOOST_LOG(debug) << "Offset: "sv << offset_x << 'x' << offset_y;
|
||||
BOOST_LOG(debug) << "Resolution: "sv << width << 'x' << height;
|
||||
BOOST_LOG(debug) << "Desktop Resolution: "sv << env_width << 'x' << env_height;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int dummy_img(platf::img_t *img) override {
|
||||
return 0;
|
||||
}
|
||||
|
||||
inline platf::capture_e snapshot(platf::img_t *img_out_base, std::chrono::milliseconds timeout, bool cursor) {
|
||||
auto to = std::chrono::steady_clock::now() + timeout;
|
||||
|
||||
dmabuf.listen(interface.dmabuf_manager, output, cursor);
|
||||
do {
|
||||
display.roundtrip();
|
||||
|
||||
if(to < std::chrono::steady_clock::now()) {
|
||||
return platf::capture_e::timeout;
|
||||
}
|
||||
} while(dmabuf.status == dmabuf_t::WAITING);
|
||||
|
||||
auto current_frame = dmabuf.current_frame;
|
||||
|
||||
if(
|
||||
dmabuf.status == dmabuf_t::REINIT ||
|
||||
current_frame->sd.width != width ||
|
||||
current_frame->sd.height != height) {
|
||||
|
||||
return platf::capture_e::reinit;
|
||||
}
|
||||
|
||||
return platf::capture_e::ok;
|
||||
}
|
||||
|
||||
platf::mem_type_e mem_type;
|
||||
|
||||
std::chrono::nanoseconds delay;
|
||||
|
||||
wl::display_t display;
|
||||
interface_t interface;
|
||||
dmabuf_t dmabuf;
|
||||
|
||||
wl_output *output;
|
||||
};
|
||||
|
||||
class wlr_ram_t : public wlr_t {
|
||||
public:
|
||||
platf::capture_e capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr<platf::img_t> img, bool *cursor) override {
|
||||
auto next_frame = std::chrono::steady_clock::now();
|
||||
|
||||
while(img) {
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
|
||||
if(next_frame > now) {
|
||||
std::this_thread::sleep_for((next_frame - now) / 3 * 2);
|
||||
}
|
||||
while(next_frame > now) {
|
||||
now = std::chrono::steady_clock::now();
|
||||
}
|
||||
next_frame = now + delay;
|
||||
|
||||
auto status = snapshot(img.get(), 1000ms, *cursor);
|
||||
switch(status) {
|
||||
case platf::capture_e::reinit:
|
||||
case platf::capture_e::error:
|
||||
return status;
|
||||
case platf::capture_e::timeout:
|
||||
continue;
|
||||
case platf::capture_e::ok:
|
||||
img = snapshot_cb(img);
|
||||
break;
|
||||
default:
|
||||
BOOST_LOG(error) << "Unrecognized capture status ["sv << (int)status << ']';
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
return platf::capture_e::ok;
|
||||
}
|
||||
|
||||
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);
|
||||
if(status != platf::capture_e::ok) {
|
||||
return status;
|
||||
}
|
||||
|
||||
auto current_frame = dmabuf.current_frame;
|
||||
|
||||
auto rgb_opt = egl::import_source(egl_display.get(), current_frame->sd);
|
||||
|
||||
if(!rgb_opt) {
|
||||
return platf::capture_e::reinit;
|
||||
}
|
||||
|
||||
gl::ctx.BindTexture(GL_TEXTURE_2D, (*rgb_opt)->tex[0]);
|
||||
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.BindTexture(GL_TEXTURE_2D, 0);
|
||||
|
||||
return platf::capture_e::ok;
|
||||
}
|
||||
|
||||
int init(platf::mem_type_e hwdevice_type, const std::string &display_name, int framerate) {
|
||||
if(wlr_t::init(hwdevice_type, display_name, framerate)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
egl_display = egl::make_display(display.get());
|
||||
if(!egl_display) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
auto ctx_opt = egl::make_ctx(egl_display.get());
|
||||
if(!ctx_opt) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
ctx = std::move(*ctx_opt);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::shared_ptr<platf::hwdevice_t> make_hwdevice(platf::pix_fmt_e pix_fmt) override {
|
||||
if(mem_type == platf::mem_type_e::vaapi) {
|
||||
return va::make_hwdevice(width, height, false);
|
||||
}
|
||||
|
||||
return std::make_shared<platf::hwdevice_t>();
|
||||
}
|
||||
|
||||
std::shared_ptr<platf::img_t> alloc_img() override {
|
||||
auto img = std::make_shared<img_t>();
|
||||
img->width = width;
|
||||
img->height = height;
|
||||
img->pixel_pitch = 4;
|
||||
img->row_pitch = img->pixel_pitch * width;
|
||||
img->data = new std::uint8_t[height * img->row_pitch];
|
||||
|
||||
return img;
|
||||
}
|
||||
|
||||
egl::display_t egl_display;
|
||||
egl::ctx_t ctx;
|
||||
};
|
||||
|
||||
class wlr_vram_t : public wlr_t {
|
||||
public:
|
||||
platf::capture_e capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr<platf::img_t> img, bool *cursor) override {
|
||||
auto next_frame = std::chrono::steady_clock::now();
|
||||
|
||||
while(img) {
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
|
||||
if(next_frame > now) {
|
||||
std::this_thread::sleep_for((next_frame - now) / 3 * 2);
|
||||
}
|
||||
while(next_frame > now) {
|
||||
now = std::chrono::steady_clock::now();
|
||||
}
|
||||
next_frame = now + delay;
|
||||
|
||||
auto status = snapshot(img.get(), 1000ms, *cursor);
|
||||
switch(status) {
|
||||
case platf::capture_e::reinit:
|
||||
case platf::capture_e::error:
|
||||
return status;
|
||||
case platf::capture_e::timeout:
|
||||
continue;
|
||||
case platf::capture_e::ok:
|
||||
img = snapshot_cb(img);
|
||||
break;
|
||||
default:
|
||||
BOOST_LOG(error) << "Unrecognized capture status ["sv << (int)status << ']';
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
return platf::capture_e::ok;
|
||||
}
|
||||
|
||||
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);
|
||||
if(status != platf::capture_e::ok) {
|
||||
return status;
|
||||
}
|
||||
|
||||
auto img = (egl::img_descriptor_t *)img_out_base;
|
||||
img->reset();
|
||||
|
||||
auto current_frame = dmabuf.current_frame;
|
||||
|
||||
++sequence;
|
||||
img->sequence = sequence;
|
||||
|
||||
img->sd = current_frame->sd;
|
||||
|
||||
// Prevent dmabuf from closing the file descriptors.
|
||||
std::fill_n(current_frame->sd.fds, 4, -1);
|
||||
|
||||
return platf::capture_e::ok;
|
||||
}
|
||||
|
||||
std::shared_ptr<platf::img_t> alloc_img() override {
|
||||
auto img = std::make_shared<egl::img_descriptor_t>();
|
||||
|
||||
img->sequence = 0;
|
||||
img->serial = std::numeric_limits<decltype(img->serial)>::max();
|
||||
img->data = nullptr;
|
||||
|
||||
// File descriptors aren't open
|
||||
std::fill_n(img->sd.fds, 4, -1);
|
||||
|
||||
return img;
|
||||
}
|
||||
|
||||
std::shared_ptr<platf::hwdevice_t> make_hwdevice(platf::pix_fmt_e pix_fmt) override {
|
||||
if(mem_type == platf::mem_type_e::vaapi) {
|
||||
return va::make_hwdevice(width, height, 0, 0, true);
|
||||
}
|
||||
|
||||
return std::make_shared<platf::hwdevice_t>();
|
||||
}
|
||||
|
||||
int dummy_img(platf::img_t *img) override {
|
||||
return snapshot(img, 1000ms, false) != platf::capture_e::ok;
|
||||
}
|
||||
|
||||
std::uint64_t sequence {};
|
||||
};
|
||||
|
||||
} // namespace wl
|
||||
|
||||
namespace platf {
|
||||
std::shared_ptr<display_t> wl_display(mem_type_e hwdevice_type, const std::string &display_name, int framerate) {
|
||||
if(hwdevice_type != platf::mem_type_e::system && hwdevice_type != platf::mem_type_e::vaapi && hwdevice_type != platf::mem_type_e::cuda) {
|
||||
BOOST_LOG(error) << "Could not initialize display with the given hw device type."sv;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if(hwdevice_type == platf::mem_type_e::vaapi) {
|
||||
auto wlr = std::make_shared<wl::wlr_vram_t>();
|
||||
if(wlr->init(hwdevice_type, display_name, framerate)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return wlr;
|
||||
}
|
||||
|
||||
auto wlr = std::make_shared<wl::wlr_ram_t>();
|
||||
if(wlr->init(hwdevice_type, display_name, framerate)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return wlr;
|
||||
}
|
||||
|
||||
std::vector<std::string> wl_display_names() {
|
||||
std::vector<std::string> display_names;
|
||||
|
||||
wl::display_t display;
|
||||
if(display.init()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
wl::interface_t interface;
|
||||
interface.listen(display.registry());
|
||||
|
||||
display.roundtrip();
|
||||
|
||||
if(!interface[wl::interface_t::XDG_OUTPUT]) {
|
||||
BOOST_LOG(warning) << "Missing Wayland wire for xdg_output"sv;
|
||||
return {};
|
||||
}
|
||||
|
||||
if(!interface[wl::interface_t::WLR_EXPORT_DMABUF]) {
|
||||
BOOST_LOG(warning) << "Missing Wayland wire for wlr-export-dmabuf"sv;
|
||||
return {};
|
||||
}
|
||||
|
||||
wl::env_width = 0;
|
||||
wl::env_height = 0;
|
||||
|
||||
for(auto &monitor : interface.monitors) {
|
||||
monitor->listen(interface.output_manager);
|
||||
}
|
||||
|
||||
display.roundtrip();
|
||||
|
||||
for(int x = 0; x < interface.monitors.size(); ++x) {
|
||||
auto monitor = interface.monitors[x].get();
|
||||
|
||||
wl::env_width = std::max(wl::env_width, (int)(monitor->viewport.offset_x + monitor->viewport.width));
|
||||
wl::env_height = std::max(wl::env_height, (int)(monitor->viewport.offset_y + monitor->viewport.height));
|
||||
|
||||
display_names.emplace_back(std::to_string(x));
|
||||
}
|
||||
|
||||
return display_names;
|
||||
}
|
||||
|
||||
} // namespace platf
|
||||
837
sunshine/platform/linux/x11grab.cpp
Normal file
837
sunshine/platform/linux/x11grab.cpp
Normal file
@@ -0,0 +1,837 @@
|
||||
//
|
||||
// Created by loki on 6/21/19.
|
||||
//
|
||||
|
||||
#include "sunshine/platform/common.h"
|
||||
|
||||
#include <fstream>
|
||||
|
||||
#include <X11/X.h>
|
||||
#include <X11/Xlib.h>
|
||||
#include <X11/Xutil.h>
|
||||
#include <X11/extensions/Xfixes.h>
|
||||
#include <X11/extensions/Xrandr.h>
|
||||
#include <sys/ipc.h>
|
||||
#include <sys/shm.h>
|
||||
#include <xcb/shm.h>
|
||||
#include <xcb/xfixes.h>
|
||||
|
||||
#include "sunshine/config.h"
|
||||
#include "sunshine/main.h"
|
||||
#include "sunshine/task_pool.h"
|
||||
|
||||
#include "cuda.h"
|
||||
#include "graphics.h"
|
||||
#include "misc.h"
|
||||
#include "vaapi.h"
|
||||
#include "x11grab.h"
|
||||
|
||||
using namespace std::literals;
|
||||
|
||||
namespace platf {
|
||||
int load_xcb();
|
||||
int load_x11();
|
||||
|
||||
namespace x11 {
|
||||
#define _FN(x, ret, args) \
|
||||
typedef ret(*x##_fn) args; \
|
||||
static x##_fn x
|
||||
|
||||
_FN(GetImage, XImage *,
|
||||
(
|
||||
Display * display,
|
||||
Drawable d,
|
||||
int x, int y,
|
||||
unsigned int width, unsigned int height,
|
||||
unsigned long plane_mask,
|
||||
int format));
|
||||
|
||||
_FN(OpenDisplay, Display *, (_Xconst char *display_name));
|
||||
_FN(GetWindowAttributes, Status,
|
||||
(
|
||||
Display * display,
|
||||
Window w,
|
||||
XWindowAttributes *window_attributes_return));
|
||||
|
||||
_FN(CloseDisplay, int, (Display * display));
|
||||
_FN(Free, int, (void *data));
|
||||
_FN(InitThreads, Status, (void));
|
||||
|
||||
namespace rr {
|
||||
_FN(GetScreenResources, XRRScreenResources *, (Display * dpy, Window window));
|
||||
_FN(GetOutputInfo, XRROutputInfo *, (Display * dpy, XRRScreenResources *resources, RROutput output));
|
||||
_FN(GetCrtcInfo, XRRCrtcInfo *, (Display * dpy, XRRScreenResources *resources, RRCrtc crtc));
|
||||
_FN(FreeScreenResources, void, (XRRScreenResources * resources));
|
||||
_FN(FreeOutputInfo, void, (XRROutputInfo * outputInfo));
|
||||
_FN(FreeCrtcInfo, void, (XRRCrtcInfo * crtcInfo));
|
||||
|
||||
int init() {
|
||||
static void *handle { nullptr };
|
||||
static bool funcs_loaded = false;
|
||||
|
||||
if(funcs_loaded) return 0;
|
||||
|
||||
if(!handle) {
|
||||
handle = dyn::handle({ "libXrandr.so.2", "libXrandr.so" });
|
||||
if(!handle) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::tuple<dyn::apiproc *, const char *>> funcs {
|
||||
{ (dyn::apiproc *)&GetScreenResources, "XRRGetScreenResources" },
|
||||
{ (dyn::apiproc *)&GetOutputInfo, "XRRGetOutputInfo" },
|
||||
{ (dyn::apiproc *)&GetCrtcInfo, "XRRGetCrtcInfo" },
|
||||
{ (dyn::apiproc *)&FreeScreenResources, "XRRFreeScreenResources" },
|
||||
{ (dyn::apiproc *)&FreeOutputInfo, "XRRFreeOutputInfo" },
|
||||
{ (dyn::apiproc *)&FreeCrtcInfo, "XRRFreeCrtcInfo" },
|
||||
};
|
||||
|
||||
if(dyn::load(handle, funcs)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
funcs_loaded = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace rr
|
||||
namespace fix {
|
||||
_FN(GetCursorImage, XFixesCursorImage *, (Display * dpy));
|
||||
|
||||
int init() {
|
||||
static void *handle { nullptr };
|
||||
static bool funcs_loaded = false;
|
||||
|
||||
if(funcs_loaded) return 0;
|
||||
|
||||
if(!handle) {
|
||||
handle = dyn::handle({ "libXfixes.so.3", "libXfixes.so" });
|
||||
if(!handle) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::tuple<dyn::apiproc *, const char *>> funcs {
|
||||
{ (dyn::apiproc *)&GetCursorImage, "XFixesGetCursorImage" },
|
||||
};
|
||||
|
||||
if(dyn::load(handle, funcs)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
funcs_loaded = true;
|
||||
return 0;
|
||||
}
|
||||
} // namespace fix
|
||||
|
||||
int init() {
|
||||
static void *handle { nullptr };
|
||||
static bool funcs_loaded = false;
|
||||
|
||||
if(funcs_loaded) return 0;
|
||||
|
||||
if(!handle) {
|
||||
handle = dyn::handle({ "libX11.so.6", "libX11.so" });
|
||||
if(!handle) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::tuple<dyn::apiproc *, const char *>> funcs {
|
||||
{ (dyn::apiproc *)&GetImage, "XGetImage" },
|
||||
{ (dyn::apiproc *)&OpenDisplay, "XOpenDisplay" },
|
||||
{ (dyn::apiproc *)&GetWindowAttributes, "XGetWindowAttributes" },
|
||||
{ (dyn::apiproc *)&Free, "XFree" },
|
||||
{ (dyn::apiproc *)&CloseDisplay, "XCloseDisplay" },
|
||||
{ (dyn::apiproc *)&InitThreads, "XInitThreads" },
|
||||
};
|
||||
|
||||
if(dyn::load(handle, funcs)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
funcs_loaded = true;
|
||||
return 0;
|
||||
}
|
||||
} // namespace x11
|
||||
|
||||
namespace xcb {
|
||||
static xcb_extension_t *shm_id;
|
||||
|
||||
_FN(shm_get_image_reply, xcb_shm_get_image_reply_t *,
|
||||
(
|
||||
xcb_connection_t * c,
|
||||
xcb_shm_get_image_cookie_t cookie,
|
||||
xcb_generic_error_t **e));
|
||||
|
||||
_FN(shm_get_image_unchecked, xcb_shm_get_image_cookie_t,
|
||||
(
|
||||
xcb_connection_t * c,
|
||||
xcb_drawable_t drawable,
|
||||
int16_t x, int16_t y,
|
||||
uint16_t width, uint16_t height,
|
||||
uint32_t plane_mask,
|
||||
uint8_t format,
|
||||
xcb_shm_seg_t shmseg,
|
||||
uint32_t offset));
|
||||
|
||||
_FN(shm_attach, xcb_void_cookie_t,
|
||||
(xcb_connection_t * c,
|
||||
xcb_shm_seg_t shmseg,
|
||||
uint32_t shmid,
|
||||
uint8_t read_only));
|
||||
|
||||
_FN(get_extension_data, xcb_query_extension_reply_t *,
|
||||
(xcb_connection_t * c, xcb_extension_t *ext));
|
||||
|
||||
_FN(get_setup, xcb_setup_t *, (xcb_connection_t * c));
|
||||
_FN(disconnect, void, (xcb_connection_t * c));
|
||||
_FN(connection_has_error, int, (xcb_connection_t * c));
|
||||
_FN(connect, xcb_connection_t *, (const char *displayname, int *screenp));
|
||||
_FN(setup_roots_iterator, xcb_screen_iterator_t, (const xcb_setup_t *R));
|
||||
_FN(generate_id, std::uint32_t, (xcb_connection_t * c));
|
||||
|
||||
int init_shm() {
|
||||
static void *handle { nullptr };
|
||||
static bool funcs_loaded = false;
|
||||
|
||||
if(funcs_loaded) return 0;
|
||||
|
||||
if(!handle) {
|
||||
handle = dyn::handle({ "libxcb-shm.so.0", "libxcb-shm.so" });
|
||||
if(!handle) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::tuple<dyn::apiproc *, const char *>> funcs {
|
||||
{ (dyn::apiproc *)&shm_id, "xcb_shm_id" },
|
||||
{ (dyn::apiproc *)&shm_get_image_reply, "xcb_shm_get_image_reply" },
|
||||
{ (dyn::apiproc *)&shm_get_image_unchecked, "xcb_shm_get_image_unchecked" },
|
||||
{ (dyn::apiproc *)&shm_attach, "xcb_shm_attach" },
|
||||
};
|
||||
|
||||
if(dyn::load(handle, funcs)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
funcs_loaded = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int init() {
|
||||
static void *handle { nullptr };
|
||||
static bool funcs_loaded = false;
|
||||
|
||||
if(funcs_loaded) return 0;
|
||||
|
||||
if(!handle) {
|
||||
handle = dyn::handle({ "libxcb.so.1", "libxcb.so" });
|
||||
if(!handle) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::tuple<dyn::apiproc *, const char *>> funcs {
|
||||
{ (dyn::apiproc *)&get_extension_data, "xcb_get_extension_data" },
|
||||
{ (dyn::apiproc *)&get_setup, "xcb_get_setup" },
|
||||
{ (dyn::apiproc *)&disconnect, "xcb_disconnect" },
|
||||
{ (dyn::apiproc *)&connection_has_error, "xcb_connection_has_error" },
|
||||
{ (dyn::apiproc *)&connect, "xcb_connect" },
|
||||
{ (dyn::apiproc *)&setup_roots_iterator, "xcb_setup_roots_iterator" },
|
||||
{ (dyn::apiproc *)&generate_id, "xcb_generate_id" },
|
||||
};
|
||||
|
||||
if(dyn::load(handle, funcs)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
funcs_loaded = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
#undef _FN
|
||||
} // namespace xcb
|
||||
|
||||
void freeImage(XImage *);
|
||||
void freeX(XFixesCursorImage *);
|
||||
|
||||
using xcb_connect_t = util::dyn_safe_ptr<xcb_connection_t, &xcb::disconnect>;
|
||||
using xcb_img_t = util::c_ptr<xcb_shm_get_image_reply_t>;
|
||||
|
||||
using ximg_t = util::safe_ptr<XImage, freeImage>;
|
||||
using xcursor_t = util::safe_ptr<XFixesCursorImage, freeX>;
|
||||
|
||||
using crtc_info_t = util::dyn_safe_ptr<_XRRCrtcInfo, &x11::rr::FreeCrtcInfo>;
|
||||
using output_info_t = util::dyn_safe_ptr<_XRROutputInfo, &x11::rr::FreeOutputInfo>;
|
||||
using screen_res_t = util::dyn_safe_ptr<_XRRScreenResources, &x11::rr::FreeScreenResources>;
|
||||
|
||||
class shm_id_t {
|
||||
public:
|
||||
shm_id_t() : id { -1 } {}
|
||||
shm_id_t(int id) : id { id } {}
|
||||
shm_id_t(shm_id_t &&other) noexcept : id(other.id) {
|
||||
other.id = -1;
|
||||
}
|
||||
|
||||
~shm_id_t() {
|
||||
if(id != -1) {
|
||||
shmctl(id, IPC_RMID, nullptr);
|
||||
id = -1;
|
||||
}
|
||||
}
|
||||
int id;
|
||||
};
|
||||
|
||||
class shm_data_t {
|
||||
public:
|
||||
shm_data_t() : data { (void *)-1 } {}
|
||||
shm_data_t(void *data) : data { data } {}
|
||||
|
||||
shm_data_t(shm_data_t &&other) noexcept : data(other.data) {
|
||||
other.data = (void *)-1;
|
||||
}
|
||||
|
||||
~shm_data_t() {
|
||||
if((std::uintptr_t)data != -1) {
|
||||
shmdt(data);
|
||||
}
|
||||
}
|
||||
|
||||
void *data;
|
||||
};
|
||||
|
||||
struct x11_img_t : public img_t {
|
||||
ximg_t img;
|
||||
};
|
||||
|
||||
struct shm_img_t : public img_t {
|
||||
~shm_img_t() override {
|
||||
delete[] data;
|
||||
data = nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
static void blend_cursor(Display *display, img_t &img, int offsetX, int offsetY) {
|
||||
xcursor_t overlay { x11::fix::GetCursorImage(display) };
|
||||
|
||||
if(!overlay) {
|
||||
BOOST_LOG(error) << "Couldn't get cursor from XFixesGetCursorImage"sv;
|
||||
return;
|
||||
}
|
||||
|
||||
overlay->x -= overlay->xhot;
|
||||
overlay->y -= overlay->yhot;
|
||||
|
||||
overlay->x -= offsetX;
|
||||
overlay->y -= offsetY;
|
||||
|
||||
overlay->x = std::max((short)0, overlay->x);
|
||||
overlay->y = std::max((short)0, overlay->y);
|
||||
|
||||
auto pixels = (int *)img.data;
|
||||
|
||||
auto screen_height = img.height;
|
||||
auto screen_width = img.width;
|
||||
|
||||
auto delta_height = std::min<uint16_t>(overlay->height, std::max(0, screen_height - overlay->y));
|
||||
auto delta_width = std::min<uint16_t>(overlay->width, std::max(0, screen_width - overlay->x));
|
||||
for(auto y = 0; y < delta_height; ++y) {
|
||||
auto overlay_begin = &overlay->pixels[y * overlay->width];
|
||||
auto overlay_end = &overlay->pixels[y * overlay->width + delta_width];
|
||||
|
||||
auto pixels_begin = &pixels[(y + overlay->y) * (img.row_pitch / img.pixel_pitch) + overlay->x];
|
||||
|
||||
std::for_each(overlay_begin, overlay_end, [&](long pixel) {
|
||||
int *pixel_p = (int *)&pixel;
|
||||
|
||||
auto colors_in = (uint8_t *)pixels_begin;
|
||||
|
||||
auto alpha = (*(uint *)pixel_p) >> 24u;
|
||||
if(alpha == 255) {
|
||||
*pixels_begin = *pixel_p;
|
||||
}
|
||||
else {
|
||||
auto colors_out = (uint8_t *)pixel_p;
|
||||
colors_in[0] = colors_out[0] + (colors_in[0] * (255 - alpha) + 255 / 2) / 255;
|
||||
colors_in[1] = colors_out[1] + (colors_in[1] * (255 - alpha) + 255 / 2) / 255;
|
||||
colors_in[2] = colors_out[2] + (colors_in[2] * (255 - alpha) + 255 / 2) / 255;
|
||||
}
|
||||
++pixels_begin;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
struct x11_attr_t : public display_t {
|
||||
std::chrono::nanoseconds delay;
|
||||
|
||||
x11::xdisplay_t xdisplay;
|
||||
Window xwindow;
|
||||
XWindowAttributes xattr;
|
||||
|
||||
mem_type_e mem_type;
|
||||
|
||||
/*
|
||||
* 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) : xdisplay { x11::OpenDisplay(nullptr) }, xwindow {}, xattr {}, mem_type { mem_type } {
|
||||
x11::InitThreads();
|
||||
}
|
||||
|
||||
int init(const std::string &display_name, int framerate) {
|
||||
if(!xdisplay) {
|
||||
BOOST_LOG(error) << "Could not open X11 display"sv;
|
||||
return -1;
|
||||
}
|
||||
|
||||
delay = std::chrono::nanoseconds { 1s } / framerate;
|
||||
|
||||
xwindow = DefaultRootWindow(xdisplay.get());
|
||||
|
||||
refresh();
|
||||
|
||||
int streamedMonitor = -1;
|
||||
if(!display_name.empty()) {
|
||||
streamedMonitor = (int)util::from_view(display_name);
|
||||
}
|
||||
|
||||
if(streamedMonitor != -1) {
|
||||
BOOST_LOG(info) << "Configuring selected monitor ("sv << streamedMonitor << ") to stream"sv;
|
||||
screen_res_t screenr { x11::rr::GetScreenResources(xdisplay.get(), xwindow) };
|
||||
int output = screenr->noutput;
|
||||
|
||||
output_info_t result;
|
||||
int monitor = 0;
|
||||
for(int x = 0; x < output; ++x) {
|
||||
output_info_t out_info { x11::rr::GetOutputInfo(xdisplay.get(), screenr.get(), screenr->outputs[x]) };
|
||||
if(out_info && out_info->connection == RR_Connected) {
|
||||
if(monitor++ == streamedMonitor) {
|
||||
result = std::move(out_info);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!result) {
|
||||
BOOST_LOG(error) << "Could not stream display number ["sv << streamedMonitor << "], there are only ["sv << monitor << "] displays."sv;
|
||||
return -1;
|
||||
}
|
||||
|
||||
if(result->crtc) {
|
||||
crtc_info_t crt_info { x11::rr::GetCrtcInfo(xdisplay.get(), screenr.get(), result->crtc) };
|
||||
BOOST_LOG(info)
|
||||
<< "Streaming display: "sv << result->name << " with res "sv << crt_info->width << 'x' << crt_info->height << " offset by "sv << crt_info->x << 'x' << crt_info->y;
|
||||
|
||||
width = crt_info->width;
|
||||
height = crt_info->height;
|
||||
offset_x = crt_info->x;
|
||||
offset_y = crt_info->y;
|
||||
}
|
||||
else {
|
||||
BOOST_LOG(warning) << "Couldn't get requested display info, defaulting to recording entire virtual desktop"sv;
|
||||
width = xattr.width;
|
||||
height = xattr.height;
|
||||
}
|
||||
}
|
||||
else {
|
||||
width = xattr.width;
|
||||
height = xattr.height;
|
||||
}
|
||||
|
||||
env_width = xattr.width;
|
||||
env_height = xattr.height;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the display attributes should change.
|
||||
*/
|
||||
void refresh() {
|
||||
x11::GetWindowAttributes(xdisplay.get(), xwindow, &xattr); //Update xattr's
|
||||
}
|
||||
|
||||
capture_e capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr<img_t> img, bool *cursor) override {
|
||||
auto next_frame = std::chrono::steady_clock::now();
|
||||
|
||||
while(img) {
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
|
||||
if(next_frame > now) {
|
||||
std::this_thread::sleep_for((next_frame - now) / 3 * 2);
|
||||
}
|
||||
while(next_frame > now) {
|
||||
std::this_thread::sleep_for(1ns);
|
||||
now = std::chrono::steady_clock::now();
|
||||
}
|
||||
next_frame = now + delay;
|
||||
|
||||
auto status = snapshot(img.get(), 1000ms, *cursor);
|
||||
switch(status) {
|
||||
case platf::capture_e::reinit:
|
||||
case platf::capture_e::error:
|
||||
return status;
|
||||
case platf::capture_e::timeout:
|
||||
std::this_thread::sleep_for(1ms);
|
||||
continue;
|
||||
case platf::capture_e::ok:
|
||||
img = snapshot_cb(img);
|
||||
break;
|
||||
default:
|
||||
BOOST_LOG(error) << "Unrecognized capture status ["sv << (int)status << ']';
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
return capture_e::ok;
|
||||
}
|
||||
|
||||
capture_e snapshot(img_t *img_out_base, std::chrono::milliseconds timeout, bool cursor) {
|
||||
refresh();
|
||||
|
||||
//The whole X server changed, so we gotta 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(cursor) {
|
||||
blend_cursor(xdisplay.get(), *img_out_base, offset_x, offset_y);
|
||||
}
|
||||
|
||||
return capture_e::ok;
|
||||
}
|
||||
|
||||
std::shared_ptr<img_t> alloc_img() override {
|
||||
return std::make_shared<x11_img_t>();
|
||||
}
|
||||
|
||||
std::shared_ptr<hwdevice_t> make_hwdevice(pix_fmt_e pix_fmt) override {
|
||||
if(mem_type == mem_type_e::vaapi) {
|
||||
return va::make_hwdevice(width, height, false);
|
||||
}
|
||||
|
||||
#ifdef SUNSHINE_BUILD_CUDA
|
||||
if(mem_type == mem_type_e::cuda) {
|
||||
return cuda::make_hwdevice(width, height, false);
|
||||
}
|
||||
#endif
|
||||
|
||||
return std::make_shared<hwdevice_t>();
|
||||
}
|
||||
|
||||
int dummy_img(img_t *img) override {
|
||||
snapshot(img, 0s, true);
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
struct shm_attr_t : public x11_attr_t {
|
||||
x11::xdisplay_t shm_xdisplay; // Prevent race condition with x11_attr_t::xdisplay
|
||||
xcb_connect_t xcb;
|
||||
xcb_screen_t *display;
|
||||
std::uint32_t seg;
|
||||
|
||||
shm_id_t shm_id;
|
||||
|
||||
shm_data_t data;
|
||||
|
||||
util::TaskPool::task_id_t refresh_task_id;
|
||||
|
||||
void delayed_refresh() {
|
||||
refresh();
|
||||
|
||||
refresh_task_id = task_pool.pushDelayed(&shm_attr_t::delayed_refresh, 2s, this).task_id;
|
||||
}
|
||||
|
||||
shm_attr_t(mem_type_e mem_type) : x11_attr_t(mem_type), shm_xdisplay { x11::OpenDisplay(nullptr) } {
|
||||
refresh_task_id = task_pool.pushDelayed(&shm_attr_t::delayed_refresh, 2s, this).task_id;
|
||||
}
|
||||
|
||||
~shm_attr_t() override {
|
||||
while(!task_pool.cancel(refresh_task_id))
|
||||
;
|
||||
}
|
||||
|
||||
capture_e capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr<img_t> img, bool *cursor) override {
|
||||
auto next_frame = std::chrono::steady_clock::now();
|
||||
|
||||
while(img) {
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
|
||||
if(next_frame > now) {
|
||||
std::this_thread::sleep_for((next_frame - now) / 3 * 2);
|
||||
}
|
||||
while(next_frame > now) {
|
||||
std::this_thread::sleep_for(1ns);
|
||||
now = std::chrono::steady_clock::now();
|
||||
}
|
||||
next_frame = now + delay;
|
||||
|
||||
auto status = snapshot(img.get(), 1000ms, *cursor);
|
||||
switch(status) {
|
||||
case platf::capture_e::reinit:
|
||||
case platf::capture_e::error:
|
||||
return status;
|
||||
case platf::capture_e::timeout:
|
||||
std::this_thread::sleep_for(1ms);
|
||||
continue;
|
||||
case platf::capture_e::ok:
|
||||
img = snapshot_cb(img);
|
||||
break;
|
||||
default:
|
||||
BOOST_LOG(error) << "Unrecognized capture status ["sv << (int)status << ']';
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
return capture_e::ok;
|
||||
}
|
||||
|
||||
capture_e snapshot(img_t *img, std::chrono::milliseconds timeout, bool cursor) {
|
||||
//The whole X server changed, so we gotta reinit everything
|
||||
if(xattr.width != env_width || xattr.height != env_height) {
|
||||
BOOST_LOG(warning) << "X dimensions changed in SHM mode, request reinit"sv;
|
||||
return capture_e::reinit;
|
||||
}
|
||||
else {
|
||||
auto img_cookie = xcb::shm_get_image_unchecked(xcb.get(), display->root, offset_x, offset_y, width, height, ~0, XCB_IMAGE_FORMAT_Z_PIXMAP, seg, 0);
|
||||
|
||||
xcb_img_t img_reply { xcb::shm_get_image_reply(xcb.get(), img_cookie, nullptr) };
|
||||
if(!img_reply) {
|
||||
BOOST_LOG(error) << "Could not get image reply"sv;
|
||||
return capture_e::reinit;
|
||||
}
|
||||
|
||||
std::copy_n((std::uint8_t *)data.data, frame_size(), img->data);
|
||||
|
||||
if(cursor) {
|
||||
blend_cursor(shm_xdisplay.get(), *img, offset_x, offset_y);
|
||||
}
|
||||
|
||||
return capture_e::ok;
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<img_t> alloc_img() override {
|
||||
auto img = std::make_shared<shm_img_t>();
|
||||
img->width = width;
|
||||
img->height = height;
|
||||
img->pixel_pitch = 4;
|
||||
img->row_pitch = img->pixel_pitch * width;
|
||||
img->data = new std::uint8_t[height * img->row_pitch];
|
||||
|
||||
return img;
|
||||
}
|
||||
|
||||
int dummy_img(platf::img_t *img) override {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int init(const std::string &display_name, int framerate) {
|
||||
if(x11_attr_t::init(display_name, framerate)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
shm_xdisplay.reset(x11::OpenDisplay(nullptr));
|
||||
xcb.reset(xcb::connect(nullptr, nullptr));
|
||||
if(xcb::connection_has_error(xcb.get())) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if(!xcb::get_extension_data(xcb.get(), xcb::shm_id)->present) {
|
||||
BOOST_LOG(error) << "Missing SHM extension"sv;
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
auto iter = xcb::setup_roots_iterator(xcb::get_setup(xcb.get()));
|
||||
display = iter.data;
|
||||
seg = xcb::generate_id(xcb.get());
|
||||
|
||||
shm_id.id = shmget(IPC_PRIVATE, frame_size(), IPC_CREAT | 0777);
|
||||
if(shm_id.id == -1) {
|
||||
BOOST_LOG(error) << "shmget failed"sv;
|
||||
return -1;
|
||||
}
|
||||
|
||||
xcb::shm_attach(xcb.get(), seg, shm_id.id, false);
|
||||
data.data = shmat(shm_id.id, nullptr, 0);
|
||||
|
||||
if((uintptr_t)data.data == -1) {
|
||||
BOOST_LOG(error) << "shmat failed"sv;
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::uint32_t frame_size() {
|
||||
return width * height * 4;
|
||||
}
|
||||
};
|
||||
|
||||
std::shared_ptr<display_t> x11_display(platf::mem_type_e hwdevice_type, const std::string &display_name, int framerate) {
|
||||
if(hwdevice_type != platf::mem_type_e::system && hwdevice_type != platf::mem_type_e::vaapi && hwdevice_type != platf::mem_type_e::cuda) {
|
||||
BOOST_LOG(error) << "Could not initialize x11 display with the given hw device type"sv;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if(xcb::init_shm() || xcb::init() || x11::init() || x11::rr::init() || x11::fix::init()) {
|
||||
BOOST_LOG(error) << "Couldn't init x11 libraries"sv;
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Attempt to use shared memory X11 to avoid copying the frame
|
||||
auto shm_disp = std::make_shared<shm_attr_t>(hwdevice_type);
|
||||
|
||||
auto status = shm_disp->init(display_name, framerate);
|
||||
if(status > 0) {
|
||||
// x11_attr_t::init() failed, don't bother trying again.
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if(status == 0) {
|
||||
return shm_disp;
|
||||
}
|
||||
|
||||
// Fallback
|
||||
auto x11_disp = std::make_shared<x11_attr_t>(hwdevice_type);
|
||||
if(x11_disp->init(display_name, framerate)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return x11_disp;
|
||||
}
|
||||
|
||||
std::vector<std::string> x11_display_names() {
|
||||
if(load_x11() || load_xcb()) {
|
||||
BOOST_LOG(error) << "Couldn't init x11 libraries"sv;
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
BOOST_LOG(info) << "Detecting connected monitors"sv;
|
||||
|
||||
x11::xdisplay_t xdisplay { x11::OpenDisplay(nullptr) };
|
||||
if(!xdisplay) {
|
||||
return {};
|
||||
}
|
||||
|
||||
auto xwindow = DefaultRootWindow(xdisplay.get());
|
||||
screen_res_t screenr { x11::rr::GetScreenResources(xdisplay.get(), xwindow) };
|
||||
int output = screenr->noutput;
|
||||
|
||||
int monitor = 0;
|
||||
for(int x = 0; x < output; ++x) {
|
||||
output_info_t out_info { x11::rr::GetOutputInfo(xdisplay.get(), screenr.get(), screenr->outputs[x]) };
|
||||
if(out_info && out_info->connection == RR_Connected) {
|
||||
++monitor;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string> names;
|
||||
names.reserve(monitor);
|
||||
|
||||
for(auto x = 0; x < monitor; ++x) {
|
||||
names.emplace_back(std::to_string(x));
|
||||
}
|
||||
|
||||
return names;
|
||||
}
|
||||
|
||||
void freeImage(XImage *p) {
|
||||
XDestroyImage(p);
|
||||
}
|
||||
void freeX(XFixesCursorImage *p) {
|
||||
x11::Free(p);
|
||||
}
|
||||
|
||||
int load_xcb() {
|
||||
// This will be called once only
|
||||
static int xcb_status = xcb::init_shm() || xcb::init();
|
||||
|
||||
return xcb_status;
|
||||
}
|
||||
|
||||
int load_x11() {
|
||||
// This will be called once only
|
||||
static int x11_status =
|
||||
window_system == window_system_e::NONE ||
|
||||
x11::init() || x11::rr::init() || x11::fix::init();
|
||||
|
||||
return x11_status;
|
||||
}
|
||||
|
||||
namespace x11 {
|
||||
std::optional<cursor_t> cursor_t::make() {
|
||||
if(load_x11()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
cursor_t cursor;
|
||||
|
||||
cursor.ctx.reset((cursor_ctx_t::pointer)x11::OpenDisplay(nullptr));
|
||||
|
||||
return cursor;
|
||||
}
|
||||
|
||||
void cursor_t::capture(egl::cursor_t &img) {
|
||||
auto display = (xdisplay_t::pointer)ctx.get();
|
||||
|
||||
xcursor_t xcursor = fix::GetCursorImage(display);
|
||||
|
||||
if(img.serial != xcursor->cursor_serial) {
|
||||
auto buf_size = xcursor->width * xcursor->height * sizeof(int);
|
||||
|
||||
if(img.buffer.size() < buf_size) {
|
||||
img.buffer.resize(buf_size);
|
||||
}
|
||||
|
||||
std::transform(xcursor->pixels, xcursor->pixels + buf_size / 4, (int *)img.buffer.data(), [](long pixel) -> int {
|
||||
return pixel;
|
||||
});
|
||||
}
|
||||
|
||||
img.data = img.buffer.data();
|
||||
img.width = xcursor->width;
|
||||
img.height = xcursor->height;
|
||||
img.x = xcursor->x - xcursor->xhot;
|
||||
img.y = xcursor->y - xcursor->yhot;
|
||||
img.pixel_pitch = 4;
|
||||
img.row_pitch = img.pixel_pitch * img.width;
|
||||
img.serial = xcursor->cursor_serial;
|
||||
}
|
||||
|
||||
void cursor_t::blend(img_t &img, int offsetX, int offsetY) {
|
||||
blend_cursor((xdisplay_t::pointer)ctx.get(), img, offsetX, offsetY);
|
||||
}
|
||||
|
||||
xdisplay_t make_display() {
|
||||
return OpenDisplay(nullptr);
|
||||
}
|
||||
|
||||
void freeDisplay(_XDisplay *xdisplay) {
|
||||
CloseDisplay(xdisplay);
|
||||
}
|
||||
|
||||
void freeCursorCtx(cursor_ctx_t::pointer ctx) {
|
||||
CloseDisplay((xdisplay_t::pointer)ctx);
|
||||
}
|
||||
} // namespace x11
|
||||
} // namespace platf
|
||||
60
sunshine/platform/linux/x11grab.h
Normal file
60
sunshine/platform/linux/x11grab.h
Normal file
@@ -0,0 +1,60 @@
|
||||
#ifndef SUNSHINE_X11_GRAB
|
||||
#define SUNSHINE_X11_GRAB
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include "sunshine/platform/common.h"
|
||||
#include "sunshine/utility.h"
|
||||
|
||||
// X11 Display
|
||||
extern "C" struct _XDisplay;
|
||||
|
||||
namespace egl {
|
||||
class cursor_t;
|
||||
}
|
||||
|
||||
namespace platf::x11 {
|
||||
|
||||
#ifdef SUNSHINE_BUILD_X11
|
||||
struct cursor_ctx_raw_t;
|
||||
void freeCursorCtx(cursor_ctx_raw_t *ctx);
|
||||
void freeDisplay(_XDisplay *xdisplay);
|
||||
|
||||
using cursor_ctx_t = util::safe_ptr<cursor_ctx_raw_t, freeCursorCtx>;
|
||||
using xdisplay_t = util::safe_ptr<_XDisplay, freeDisplay>;
|
||||
|
||||
class cursor_t {
|
||||
public:
|
||||
static std::optional<cursor_t> make();
|
||||
|
||||
void 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
|
||||
*/
|
||||
void blend(img_t &img, int offsetX, int offsetY);
|
||||
|
||||
cursor_ctx_t ctx;
|
||||
};
|
||||
|
||||
xdisplay_t make_display();
|
||||
#else
|
||||
// It's never something different from nullptr
|
||||
util::safe_ptr<_XDisplay, std::default_delete<_XDisplay>>;
|
||||
|
||||
class cursor_t {
|
||||
public:
|
||||
static std::optional<cursor_t> make() { return std::nullopt; }
|
||||
|
||||
void capture(egl::cursor_t &) {}
|
||||
void blend(img_t &, int, int) {}
|
||||
};
|
||||
|
||||
xdisplay_t make_display() { return nullptr; }
|
||||
#endif
|
||||
} // namespace platf::x11
|
||||
|
||||
#endif
|
||||
@@ -22,19 +22,31 @@ void Release(T *dxgi) {
|
||||
dxgi->Release();
|
||||
}
|
||||
|
||||
using factory1_t = util::safe_ptr<IDXGIFactory1, Release<IDXGIFactory1>>;
|
||||
using dxgi_t = util::safe_ptr<IDXGIDevice, Release<IDXGIDevice>>;
|
||||
using dxgi1_t = util::safe_ptr<IDXGIDevice1, Release<IDXGIDevice1>>;
|
||||
using device_t = util::safe_ptr<ID3D11Device, Release<ID3D11Device>>;
|
||||
using device_ctx_t = util::safe_ptr<ID3D11DeviceContext, Release<ID3D11DeviceContext>>;
|
||||
using adapter_t = util::safe_ptr<IDXGIAdapter1, Release<IDXGIAdapter1>>;
|
||||
using output_t = util::safe_ptr<IDXGIOutput, Release<IDXGIOutput>>;
|
||||
using output1_t = util::safe_ptr<IDXGIOutput1, Release<IDXGIOutput1>>;
|
||||
using dup_t = util::safe_ptr<IDXGIOutputDuplication, Release<IDXGIOutputDuplication>>;
|
||||
using texture2d_t = util::safe_ptr<ID3D11Texture2D, Release<ID3D11Texture2D>>;
|
||||
using texture1d_t = util::safe_ptr<ID3D11Texture1D, Release<ID3D11Texture1D>>;
|
||||
using resource_t = util::safe_ptr<IDXGIResource, Release<IDXGIResource>>;
|
||||
using multithread_t = util::safe_ptr<ID3D11Multithread, Release<ID3D11Multithread>>;
|
||||
using factory1_t = util::safe_ptr<IDXGIFactory1, Release<IDXGIFactory1>>;
|
||||
using dxgi_t = util::safe_ptr<IDXGIDevice, Release<IDXGIDevice>>;
|
||||
using dxgi1_t = util::safe_ptr<IDXGIDevice1, Release<IDXGIDevice1>>;
|
||||
using device_t = util::safe_ptr<ID3D11Device, Release<ID3D11Device>>;
|
||||
using device_ctx_t = util::safe_ptr<ID3D11DeviceContext, Release<ID3D11DeviceContext>>;
|
||||
using adapter_t = util::safe_ptr<IDXGIAdapter1, Release<IDXGIAdapter1>>;
|
||||
using output_t = util::safe_ptr<IDXGIOutput, Release<IDXGIOutput>>;
|
||||
using output1_t = util::safe_ptr<IDXGIOutput1, Release<IDXGIOutput1>>;
|
||||
using dup_t = util::safe_ptr<IDXGIOutputDuplication, Release<IDXGIOutputDuplication>>;
|
||||
using texture2d_t = util::safe_ptr<ID3D11Texture2D, Release<ID3D11Texture2D>>;
|
||||
using texture1d_t = util::safe_ptr<ID3D11Texture1D, Release<ID3D11Texture1D>>;
|
||||
using resource_t = util::safe_ptr<IDXGIResource, Release<IDXGIResource>>;
|
||||
using multithread_t = util::safe_ptr<ID3D11Multithread, Release<ID3D11Multithread>>;
|
||||
using vs_t = util::safe_ptr<ID3D11VertexShader, Release<ID3D11VertexShader>>;
|
||||
using ps_t = util::safe_ptr<ID3D11PixelShader, Release<ID3D11PixelShader>>;
|
||||
using blend_t = util::safe_ptr<ID3D11BlendState, Release<ID3D11BlendState>>;
|
||||
using input_layout_t = util::safe_ptr<ID3D11InputLayout, Release<ID3D11InputLayout>>;
|
||||
using render_target_t = util::safe_ptr<ID3D11RenderTargetView, Release<ID3D11RenderTargetView>>;
|
||||
using shader_res_t = util::safe_ptr<ID3D11ShaderResourceView, Release<ID3D11ShaderResourceView>>;
|
||||
using buf_t = util::safe_ptr<ID3D11Buffer, Release<ID3D11Buffer>>;
|
||||
using raster_state_t = util::safe_ptr<ID3D11RasterizerState, Release<ID3D11RasterizerState>>;
|
||||
using sampler_state_t = util::safe_ptr<ID3D11SamplerState, Release<ID3D11SamplerState>>;
|
||||
using blob_t = util::safe_ptr<ID3DBlob, Release<ID3DBlob>>;
|
||||
using depth_stencil_state_t = util::safe_ptr<ID3D11DepthStencilState, Release<ID3D11DepthStencilState>>;
|
||||
using depth_stencil_view_t = util::safe_ptr<ID3D11DepthStencilView, Release<ID3D11DepthStencilView>>;
|
||||
|
||||
namespace video {
|
||||
using device_t = util::safe_ptr<ID3D11VideoDevice, Release<ID3D11VideoDevice>>;
|
||||
@@ -54,10 +66,29 @@ struct cursor_t {
|
||||
bool visible;
|
||||
};
|
||||
|
||||
struct gpu_cursor_t {
|
||||
texture2d_t texture;
|
||||
class gpu_cursor_t {
|
||||
public:
|
||||
gpu_cursor_t() : cursor_view { 0, 0, 0, 0, 0.0f, 1.0f } {};
|
||||
void set_pos(LONG rel_x, LONG rel_y, bool visible) {
|
||||
cursor_view.TopLeftX = rel_x;
|
||||
cursor_view.TopLeftY = rel_y;
|
||||
|
||||
LONG width, height;
|
||||
this->visible = visible;
|
||||
}
|
||||
|
||||
void set_texture(LONG width, LONG height, texture2d_t &&texture) {
|
||||
cursor_view.Width = width;
|
||||
cursor_view.Height = height;
|
||||
|
||||
this->texture = std::move(texture);
|
||||
}
|
||||
|
||||
texture2d_t texture;
|
||||
shader_res_t input_res;
|
||||
|
||||
D3D11_VIEWPORT cursor_view;
|
||||
|
||||
bool visible;
|
||||
};
|
||||
|
||||
class duplication_t {
|
||||
@@ -74,7 +105,9 @@ public:
|
||||
|
||||
class display_base_t : public display_t {
|
||||
public:
|
||||
int init();
|
||||
int init(int framerate, const std::string &display_name);
|
||||
|
||||
std::chrono::nanoseconds delay;
|
||||
|
||||
factory1_t factory;
|
||||
adapter_t adapter;
|
||||
@@ -100,11 +133,14 @@ public:
|
||||
|
||||
class display_ram_t : public display_base_t {
|
||||
public:
|
||||
capture_e snapshot(img_t *img, std::chrono::milliseconds timeout, bool cursor_visible) override;
|
||||
capture_e capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr<img_t> img, bool *cursor) override;
|
||||
capture_e snapshot(img_t *img, std::chrono::milliseconds timeout, bool cursor_visible);
|
||||
|
||||
|
||||
std::shared_ptr<img_t> alloc_img() override;
|
||||
int dummy_img(img_t *img) override;
|
||||
|
||||
int init();
|
||||
int init(int framerate, const std::string &display_name);
|
||||
|
||||
cursor_t cursor;
|
||||
D3D11_MAPPED_SUBRESOURCE img_info;
|
||||
@@ -113,15 +149,26 @@ public:
|
||||
|
||||
class display_vram_t : public display_base_t, public std::enable_shared_from_this<display_vram_t> {
|
||||
public:
|
||||
capture_e snapshot(img_t *img, std::chrono::milliseconds timeout, bool cursor_visible) override;
|
||||
capture_e capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr<img_t> img, bool *cursor) override;
|
||||
capture_e snapshot(img_t *img, std::chrono::milliseconds timeout, bool cursor_visible);
|
||||
|
||||
std::shared_ptr<img_t> alloc_img() override;
|
||||
int dummy_img(img_t *img_base) override;
|
||||
|
||||
std::shared_ptr<platf::hwdevice_t> make_hwdevice(int width, int height, pix_fmt_e pix_fmt) override;
|
||||
int init(int framerate, const std::string &display_name);
|
||||
|
||||
std::shared_ptr<platf::hwdevice_t> make_hwdevice(pix_fmt_e pix_fmt) override;
|
||||
|
||||
sampler_state_t sampler_linear;
|
||||
|
||||
blend_t blend_enable;
|
||||
blend_t blend_disable;
|
||||
|
||||
ps_t scene_ps;
|
||||
vs_t scene_vs;
|
||||
|
||||
texture2d_t src;
|
||||
gpu_cursor_t cursor;
|
||||
std::vector<hwdevice_t *> hwdevices;
|
||||
};
|
||||
} // namespace platf::dxgi
|
||||
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
|
||||
#include <codecvt>
|
||||
|
||||
#include "display.h"
|
||||
#include "misc.h"
|
||||
#include "sunshine/config.h"
|
||||
#include "sunshine/main.h"
|
||||
#include "sunshine/platform/common.h"
|
||||
|
||||
#include "display.h"
|
||||
|
||||
namespace platf {
|
||||
using namespace std::literals;
|
||||
}
|
||||
@@ -73,7 +73,7 @@ duplication_t::~duplication_t() {
|
||||
release_frame();
|
||||
}
|
||||
|
||||
int display_base_t::init() {
|
||||
int display_base_t::init(int framerate, const std::string &display_name) {
|
||||
/* Uncomment when use of IDXGIOutput5 is implemented
|
||||
std::call_once(windows_cpp_once_flag, []() {
|
||||
DECLARE_HANDLE(DPI_AWARENESS_CONTEXT);
|
||||
@@ -91,6 +91,15 @@ int display_base_t::init() {
|
||||
});
|
||||
*/
|
||||
|
||||
// Ensure we can duplicate the current display
|
||||
syncThreadDesktop();
|
||||
|
||||
delay = std::chrono::nanoseconds { 1s } / framerate;
|
||||
|
||||
// Get rectangle of full desktop for absolute mouse coordinates
|
||||
env_width = GetSystemMetrics(SM_CXVIRTUALSCREEN);
|
||||
env_height = GetSystemMetrics(SM_CYVIRTUALSCREEN);
|
||||
|
||||
HRESULT status;
|
||||
|
||||
status = CreateDXGIFactory1(IID_IDXGIFactory1, (void **)&factory);
|
||||
@@ -102,7 +111,7 @@ int display_base_t::init() {
|
||||
std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>, wchar_t> converter;
|
||||
|
||||
auto adapter_name = converter.from_bytes(config::video.adapter_name);
|
||||
auto output_name = converter.from_bytes(config::video.output_name);
|
||||
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) {
|
||||
@@ -133,6 +142,11 @@ int display_base_t::init() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,7 +209,9 @@ int display_base_t::init() {
|
||||
<< "Device Sys Mem : "sv << adapter_desc.DedicatedSystemMemory / 1048576 << " MiB"sv << std::endl
|
||||
<< "Share Sys Mem : "sv << adapter_desc.SharedSystemMemory / 1048576 << " MiB"sv << std::endl
|
||||
<< "Feature Level : 0x"sv << util::hex(feature_level).to_string_view() << std::endl
|
||||
<< "Capture size : "sv << width << 'x' << height;
|
||||
<< "Capture size : "sv << width << 'x' << height << std::endl
|
||||
<< "Offset : "sv << offset_x << 'x' << offset_y << std::endl
|
||||
<< "Virtual Desktop : "sv << env_width << 'x' << env_height;
|
||||
|
||||
// Bump up thread priority
|
||||
{
|
||||
@@ -417,22 +433,81 @@ const char *format_str[] = {
|
||||
} // namespace platf::dxgi
|
||||
|
||||
namespace platf {
|
||||
std::shared_ptr<display_t> display(dev_type_e hwdevice_type) {
|
||||
if(hwdevice_type == dev_type_e::dxgi) {
|
||||
std::shared_ptr<display_t> display(mem_type_e hwdevice_type, const std::string &display_name, int framerate) {
|
||||
if(hwdevice_type == mem_type_e::dxgi) {
|
||||
auto disp = std::make_shared<dxgi::display_vram_t>();
|
||||
|
||||
if(!disp->init()) {
|
||||
if(!disp->init(framerate, display_name)) {
|
||||
return disp;
|
||||
}
|
||||
}
|
||||
else if(hwdevice_type == dev_type_e::none) {
|
||||
else if(hwdevice_type == mem_type_e::system) {
|
||||
auto disp = std::make_shared<dxgi::display_ram_t>();
|
||||
|
||||
if(!disp->init()) {
|
||||
if(!disp->init(framerate, display_name)) {
|
||||
return disp;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::vector<std::string> display_names(mem_type_e) {
|
||||
std::vector<std::string> display_names;
|
||||
|
||||
HRESULT status;
|
||||
|
||||
BOOST_LOG(debug) << "Detecting monitors..."sv;
|
||||
|
||||
std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>, wchar_t> converter;
|
||||
|
||||
dxgi::factory1_t factory;
|
||||
status = CreateDXGIFactory1(IID_IDXGIFactory1, (void **)&factory);
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Failed to create DXGIFactory1 [0x"sv << util::hex(status).to_string_view() << ']' << std::endl;
|
||||
return {};
|
||||
}
|
||||
|
||||
dxgi::adapter_t adapter;
|
||||
for(int x = 0; factory->EnumAdapters1(x, &adapter) != DXGI_ERROR_NOT_FOUND; ++x) {
|
||||
DXGI_ADAPTER_DESC1 adapter_desc;
|
||||
adapter->GetDesc1(&adapter_desc);
|
||||
|
||||
BOOST_LOG(debug)
|
||||
<< std::endl
|
||||
<< "====== ADAPTER ====="sv << std::endl
|
||||
<< "Device Name : "sv << converter.to_bytes(adapter_desc.Description) << std::endl
|
||||
<< "Device Vendor ID : 0x"sv << util::hex(adapter_desc.VendorId).to_string_view() << std::endl
|
||||
<< "Device Device ID : 0x"sv << util::hex(adapter_desc.DeviceId).to_string_view() << std::endl
|
||||
<< "Device Video Mem : "sv << adapter_desc.DedicatedVideoMemory / 1048576 << " MiB"sv << std::endl
|
||||
<< "Device Sys Mem : "sv << adapter_desc.DedicatedSystemMemory / 1048576 << " MiB"sv << std::endl
|
||||
<< "Share Sys Mem : "sv << adapter_desc.SharedSystemMemory / 1048576 << " MiB"sv << std::endl
|
||||
<< std::endl
|
||||
<< " ====== OUTPUT ======"sv << std::endl;
|
||||
|
||||
dxgi::output_t::pointer output_p {};
|
||||
for(int y = 0; adapter->EnumOutputs(y, &output_p) != DXGI_ERROR_NOT_FOUND; ++y) {
|
||||
dxgi::output_t output { output_p };
|
||||
|
||||
DXGI_OUTPUT_DESC desc;
|
||||
output->GetDesc(&desc);
|
||||
|
||||
auto device_name = converter.to_bytes(desc.DeviceName);
|
||||
|
||||
auto width = desc.DesktopCoordinates.right - desc.DesktopCoordinates.left;
|
||||
auto height = desc.DesktopCoordinates.bottom - desc.DesktopCoordinates.top;
|
||||
|
||||
BOOST_LOG(debug)
|
||||
<< " Output Name : "sv << device_name << std::endl
|
||||
<< " AttachedToDesktop : "sv << (desc.AttachedToDesktop ? "yes"sv : "no"sv) << std::endl
|
||||
<< " Resolution : "sv << width << 'x' << height << std::endl
|
||||
<< std::endl;
|
||||
|
||||
display_names.emplace_back(std::move(device_name));
|
||||
}
|
||||
}
|
||||
|
||||
return display_names;
|
||||
}
|
||||
|
||||
} // namespace platf
|
||||
|
||||
@@ -165,6 +165,36 @@ void blend_cursor(const cursor_t &cursor, img_t &img) {
|
||||
}
|
||||
}
|
||||
|
||||
capture_e display_ram_t::capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr<::platf::img_t> img, bool *cursor) {
|
||||
auto next_frame = std::chrono::steady_clock::now();
|
||||
|
||||
while(img) {
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
while(next_frame > now) {
|
||||
now = std::chrono::steady_clock::now();
|
||||
}
|
||||
next_frame = now + delay;
|
||||
|
||||
auto status = snapshot(img.get(), 1000ms, *cursor);
|
||||
switch(status) {
|
||||
case platf::capture_e::reinit:
|
||||
case platf::capture_e::error:
|
||||
return status;
|
||||
case platf::capture_e::timeout:
|
||||
std::this_thread::sleep_for(1ms);
|
||||
continue;
|
||||
case platf::capture_e::ok:
|
||||
img = snapshot_cb(img);
|
||||
break;
|
||||
default:
|
||||
BOOST_LOG(error) << "Unrecognized capture status ["sv << (int)status << ']';
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
return capture_e::ok;
|
||||
}
|
||||
|
||||
capture_e display_ram_t::snapshot(::platf::img_t *img_base, std::chrono::milliseconds timeout, bool cursor_visible) {
|
||||
auto img = (img_t *)img_base;
|
||||
|
||||
@@ -263,8 +293,8 @@ int display_ram_t::dummy_img(platf::img_t *img) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int display_ram_t::init() {
|
||||
if(display_base_t::init()) {
|
||||
int display_ram_t::init(int framerate, const std::string &display_name) {
|
||||
if(display_base_t::init(framerate, display_name)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,65 +5,28 @@
|
||||
#include <d3dcompiler.h>
|
||||
#include <directxmath.h>
|
||||
|
||||
extern "C" {
|
||||
#include <libavcodec/avcodec.h>
|
||||
#include <libavutil/hwcontext_d3d11va.h>
|
||||
}
|
||||
|
||||
#include "display.h"
|
||||
#include "sunshine/main.h"
|
||||
#include "sunshine/video.h"
|
||||
|
||||
#define SUNSHINE_SHADERS_DIR SUNSHINE_ASSETS_DIR "/shaders"
|
||||
|
||||
#define SUNSHINE_SHADERS_DIR SUNSHINE_ASSETS_DIR "/shaders/directx"
|
||||
namespace platf {
|
||||
using namespace std::literals;
|
||||
}
|
||||
|
||||
namespace platf::dxgi {
|
||||
using input_layout_t = util::safe_ptr<ID3D11InputLayout, Release<ID3D11InputLayout>>;
|
||||
using render_target_t = util::safe_ptr<ID3D11RenderTargetView, Release<ID3D11RenderTargetView>>;
|
||||
using shader_res_t = util::safe_ptr<ID3D11ShaderResourceView, Release<ID3D11ShaderResourceView>>;
|
||||
using buf_t = util::safe_ptr<ID3D11Buffer, Release<ID3D11Buffer>>;
|
||||
using blend_t = util::safe_ptr<ID3D11BlendState, Release<ID3D11BlendState>>;
|
||||
using raster_state_t = util::safe_ptr<ID3D11RasterizerState, Release<ID3D11RasterizerState>>;
|
||||
using sampler_state_t = util::safe_ptr<ID3D11SamplerState, Release<ID3D11SamplerState>>;
|
||||
using vs_t = util::safe_ptr<ID3D11VertexShader, Release<ID3D11VertexShader>>;
|
||||
using ps_t = util::safe_ptr<ID3D11PixelShader, Release<ID3D11PixelShader>>;
|
||||
using blob_t = util::safe_ptr<ID3DBlob, Release<ID3DBlob>>;
|
||||
using depth_stencil_state_t = util::safe_ptr<ID3D11DepthStencilState, Release<ID3D11DepthStencilState>>;
|
||||
using depth_stencil_view_t = util::safe_ptr<ID3D11DepthStencilView, Release<ID3D11DepthStencilView>>;
|
||||
|
||||
using float4 = DirectX::XMFLOAT4;
|
||||
using float3 = DirectX::XMFLOAT3;
|
||||
using float2 = DirectX::XMFLOAT2;
|
||||
struct __attribute__((__aligned__(16))) color_t {
|
||||
float4 color_vec_y;
|
||||
float4 color_vec_u;
|
||||
float4 color_vec_v;
|
||||
float2 range_y;
|
||||
float2 range_uv;
|
||||
};
|
||||
|
||||
color_t make_color_matrix(float Cr, float Cb, float U_max, float V_max, float add_Y, float add_UV, float2 range_Y, float2 range_UV) {
|
||||
float Cg = 1.0f - Cr - Cb;
|
||||
|
||||
float Cr_i = 1.0f - Cr;
|
||||
float Cb_i = 1.0f - Cb;
|
||||
|
||||
float shift_y = range_Y.x / 256.0f;
|
||||
float shift_uv = range_UV.x / 256.0f;
|
||||
|
||||
float scale_y = (range_Y.y - range_Y.x) / 256.0f;
|
||||
float scale_uv = (range_UV.y - range_UV.x) / 256.0f;
|
||||
return {
|
||||
{ Cr, Cg, Cb, add_Y },
|
||||
{ -(Cr * U_max / Cb_i), -(Cg * U_max / Cb_i), U_max, add_UV },
|
||||
{ V_max, -(Cg * V_max / Cr_i), -(Cb * V_max / Cr_i), add_UV },
|
||||
{ scale_y, shift_y },
|
||||
{ scale_uv, shift_uv },
|
||||
};
|
||||
static void free_frame(AVFrame *frame) {
|
||||
av_frame_free(&frame);
|
||||
}
|
||||
|
||||
color_t colors[] {
|
||||
make_color_matrix(0.299f, 0.114f, 0.436f, 0.615f, 0.0625, 0.5f, { 16.0f, 235.0f }, { 16.0f, 240.0f }), // BT601 MPEG
|
||||
make_color_matrix(0.299f, 0.114f, 0.5f, 0.5f, 0.0f, 0.5f, { 0.0f, 255.0f }, { 0.0f, 255.0f }), // BT601 JPEG
|
||||
make_color_matrix(0.2126f, 0.0722f, 0.436f, 0.615f, 0.0625, 0.5f, { 16.0f, 235.0f }, { 16.0f, 240.0f }), //BT701 MPEG
|
||||
make_color_matrix(0.2126f, 0.0722f, 0.5f, 0.5f, 0.0f, 0.5f, { 0.0f, 255.0f }, { 0.0f, 255.0f }), //BT701 JPEG
|
||||
};
|
||||
using frame_t = util::safe_ptr<AVFrame, free_frame>;
|
||||
|
||||
namespace platf::dxgi {
|
||||
|
||||
template<class T>
|
||||
buf_t make_buffer(device_t::pointer device, const T &t) {
|
||||
@@ -123,10 +86,13 @@ blob_t convert_Y_ps_hlsl;
|
||||
blob_t scene_ps_hlsl;
|
||||
|
||||
struct img_d3d_t : public platf::img_t {
|
||||
shader_res_t input_res;
|
||||
texture2d_t texture;
|
||||
std::shared_ptr<platf::display_t> display;
|
||||
|
||||
shader_res_t input_res;
|
||||
render_target_t scene_rt;
|
||||
|
||||
texture2d_t texture;
|
||||
|
||||
~img_d3d_t() override = default;
|
||||
};
|
||||
|
||||
@@ -249,85 +215,61 @@ blob_t compile_vertex_shader(LPCSTR file) {
|
||||
return compile_shader(file, "main_vs", "vs_5_0");
|
||||
}
|
||||
|
||||
int init_rt(device_t::pointer device, shader_res_t &shader_res, render_target_t &render_target, int width, int height, DXGI_FORMAT format, texture2d_t::pointer tex) {
|
||||
D3D11_SHADER_RESOURCE_VIEW_DESC shader_resource_desc {
|
||||
format,
|
||||
D3D11_SRV_DIMENSION_TEXTURE2D
|
||||
};
|
||||
shader_resource_desc.Texture2D.MipLevels = 1;
|
||||
|
||||
auto status = device->CreateShaderResourceView(tex, &shader_resource_desc, &shader_res);
|
||||
if(status) {
|
||||
BOOST_LOG(error) << "Failed to create render target texture for luma [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return -1;
|
||||
}
|
||||
|
||||
D3D11_RENDER_TARGET_VIEW_DESC render_target_desc {
|
||||
format,
|
||||
D3D11_RTV_DIMENSION_TEXTURE2D
|
||||
};
|
||||
|
||||
status = device->CreateRenderTargetView(tex, &render_target_desc, &render_target);
|
||||
if(status) {
|
||||
BOOST_LOG(error) << "Failed to create render target view [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int init_rt(device_t::pointer device, shader_res_t &shader_res, render_target_t &render_target, int width, int height, DXGI_FORMAT format) {
|
||||
D3D11_TEXTURE2D_DESC desc {};
|
||||
|
||||
desc.Width = width;
|
||||
desc.Height = height;
|
||||
desc.Format = format;
|
||||
desc.Usage = D3D11_USAGE_DEFAULT;
|
||||
desc.BindFlags = D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE;
|
||||
desc.MipLevels = 1;
|
||||
desc.ArraySize = 1;
|
||||
desc.SampleDesc.Count = 1;
|
||||
|
||||
texture2d_t tex;
|
||||
auto status = device->CreateTexture2D(&desc, nullptr, &tex);
|
||||
if(status) {
|
||||
BOOST_LOG(error) << "Failed to create render target texture for luma [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return -1;
|
||||
}
|
||||
|
||||
return init_rt(device, shader_res, render_target, width, height, format, tex.get());
|
||||
}
|
||||
|
||||
class hwdevice_t : public platf::hwdevice_t {
|
||||
public:
|
||||
hwdevice_t(std::vector<hwdevice_t *> *hwdevices_p) : hwdevices_p { hwdevices_p } {}
|
||||
hwdevice_t() = delete;
|
||||
|
||||
void set_cursor_pos(LONG rel_x, LONG rel_y, bool visible) {
|
||||
cursor_visible = visible;
|
||||
|
||||
if(!visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto x = ((float)rel_x);
|
||||
auto y = ((float)rel_y);
|
||||
|
||||
cursor_view.TopLeftX = x;
|
||||
cursor_view.TopLeftY = y;
|
||||
}
|
||||
|
||||
int set_cursor_texture(texture2d_t::pointer texture, LONG width, LONG height) {
|
||||
auto device = (device_t::pointer)data;
|
||||
|
||||
cursor_view.Width = width;
|
||||
cursor_view.Height = height;
|
||||
|
||||
D3D11_SHADER_RESOURCE_VIEW_DESC desc {
|
||||
DXGI_FORMAT_B8G8R8A8_UNORM,
|
||||
D3D11_SRV_DIMENSION_TEXTURE2D
|
||||
};
|
||||
desc.Texture2D.MipLevels = 1;
|
||||
|
||||
auto status = device->CreateShaderResourceView(texture, &desc, &img.input_res);
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Failed to create cursor shader resource view [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int convert(platf::img_t &img_base) override {
|
||||
auto &img = (img_d3d_t &)img_base;
|
||||
|
||||
if(!img.input_res) {
|
||||
auto device = (device_t::pointer)data;
|
||||
|
||||
D3D11_SHADER_RESOURCE_VIEW_DESC desc {
|
||||
DXGI_FORMAT_B8G8R8A8_UNORM,
|
||||
D3D11_SRV_DIMENSION_TEXTURE2D
|
||||
};
|
||||
desc.Texture2D.MipLevels = 1;
|
||||
|
||||
auto status = device->CreateShaderResourceView(img.texture.get(), &desc, &img.input_res);
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Failed to create input shader resource view [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
auto input_res_p = img.input_res.get();
|
||||
|
||||
if(cursor_visible) {
|
||||
_init_view_port(img.width, img.height);
|
||||
|
||||
device_ctx_p->OMSetRenderTargets(1, &scene_rt, nullptr);
|
||||
device_ctx_p->VSSetShader(scene_vs.get(), nullptr, 0);
|
||||
device_ctx_p->PSSetShader(scene_ps.get(), nullptr, 0);
|
||||
device_ctx_p->PSSetShaderResources(0, 1, &input_res_p);
|
||||
|
||||
device_ctx_p->Draw(3, 0);
|
||||
|
||||
device_ctx_p->OMSetBlendState(blend_enable.get(), nullptr, 0xFFFFFFFFu);
|
||||
device_ctx_p->RSSetViewports(1, &cursor_view);
|
||||
device_ctx_p->PSSetShaderResources(0, 1, &this->img.input_res);
|
||||
device_ctx_p->Draw(3, 0);
|
||||
device_ctx_p->OMSetBlendState(blend_disable.get(), nullptr, 0xFFFFFFFFu);
|
||||
|
||||
input_res_p = scene_sr.get();
|
||||
}
|
||||
device_ctx_p->IASetInputLayout(input_layout.get());
|
||||
|
||||
_init_view_port(this->img.width, this->img.height);
|
||||
device_ctx_p->OMSetRenderTargets(1, &nv12_Y_rt, nullptr);
|
||||
@@ -337,7 +279,7 @@ public:
|
||||
device_ctx_p->Draw(3, 0);
|
||||
|
||||
device_ctx_p->RSSetViewports(1, &outY_view);
|
||||
device_ctx_p->PSSetShaderResources(0, 1, &input_res_p);
|
||||
device_ctx_p->PSSetShaderResources(0, 1, &img.input_res);
|
||||
device_ctx_p->Draw(3, 0);
|
||||
|
||||
// Artifacts start appearing on the rendered image if Sunshine doesn't flush
|
||||
@@ -352,7 +294,7 @@ public:
|
||||
device_ctx_p->Draw(3, 0);
|
||||
|
||||
device_ctx_p->RSSetViewports(1, &outUV_view);
|
||||
device_ctx_p->PSSetShaderResources(0, 1, &input_res_p);
|
||||
device_ctx_p->PSSetShaderResources(0, 1, &img.input_res);
|
||||
device_ctx_p->Draw(3, 0);
|
||||
device_ctx_p->Flush();
|
||||
|
||||
@@ -362,15 +304,15 @@ public:
|
||||
void set_colorspace(std::uint32_t colorspace, std::uint32_t color_range) override {
|
||||
switch(colorspace) {
|
||||
case 5: // SWS_CS_SMPTE170M
|
||||
color_p = &colors[0];
|
||||
color_p = &::video::colors[0];
|
||||
break;
|
||||
case 1: // SWS_CS_ITU709
|
||||
color_p = &colors[2];
|
||||
color_p = &::video::colors[2];
|
||||
break;
|
||||
case 9: // SWS_CS_BT2020
|
||||
default:
|
||||
BOOST_LOG(warning) << "Colorspace: ["sv << colorspace << "] not yet supported: switching to default"sv;
|
||||
color_p = &colors[0];
|
||||
color_p = &::video::colors[0];
|
||||
};
|
||||
|
||||
if(color_range > 1) {
|
||||
@@ -388,25 +330,17 @@ public:
|
||||
this->color_matrix = std::move(color_matrix);
|
||||
}
|
||||
|
||||
int init(
|
||||
std::shared_ptr<platf::display_t> display, device_t::pointer device_p, device_ctx_t::pointer device_ctx_p,
|
||||
int out_width, int out_height,
|
||||
pix_fmt_e pix_fmt) {
|
||||
HRESULT status;
|
||||
int set_frame(AVFrame *frame) {
|
||||
this->hwframe.reset(frame);
|
||||
this->frame = frame;
|
||||
|
||||
device_p->AddRef();
|
||||
data = device_p;
|
||||
auto device_p = (device_t::pointer)data;
|
||||
|
||||
this->device_ctx_p = device_ctx_p;
|
||||
auto out_width = frame->width;
|
||||
auto out_height = frame->height;
|
||||
|
||||
cursor_visible = false;
|
||||
cursor_view.MinDepth = 0.0f;
|
||||
cursor_view.MaxDepth = 1.0f;
|
||||
|
||||
platf::hwdevice_t::img = &img;
|
||||
|
||||
float in_width = display->width;
|
||||
float in_height = display->height;
|
||||
float in_width = img.display->width;
|
||||
float in_height = img.display->height;
|
||||
|
||||
// // Ensure aspect ratio is maintained
|
||||
auto scalar = std::fminf(out_width / in_width, out_height / in_height);
|
||||
@@ -420,6 +354,87 @@ public:
|
||||
outY_view = D3D11_VIEWPORT { offsetX, offsetY, out_width_f, out_height_f, 0.0f, 1.0f };
|
||||
outUV_view = D3D11_VIEWPORT { offsetX / 2, offsetY / 2, out_width_f / 2, out_height_f / 2, 0.0f, 1.0f };
|
||||
|
||||
D3D11_TEXTURE2D_DESC t {};
|
||||
t.Width = out_width;
|
||||
t.Height = out_height;
|
||||
t.MipLevels = 1;
|
||||
t.ArraySize = 1;
|
||||
t.SampleDesc.Count = 1;
|
||||
t.Usage = D3D11_USAGE_DEFAULT;
|
||||
t.Format = format;
|
||||
t.BindFlags = D3D11_BIND_RENDER_TARGET;
|
||||
|
||||
auto status = device_p->CreateTexture2D(&t, nullptr, &img.texture);
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Failed to create render target texture [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return -1;
|
||||
}
|
||||
|
||||
img.width = out_width;
|
||||
img.height = out_height;
|
||||
img.data = (std::uint8_t *)img.texture.get();
|
||||
img.row_pitch = out_width * 4;
|
||||
img.pixel_pitch = 4;
|
||||
|
||||
float info_in[16 / sizeof(float)] { 1.0f / (float)out_width }; //aligned to 16-byte
|
||||
info_scene = make_buffer(device_p, info_in);
|
||||
|
||||
if(!info_in) {
|
||||
BOOST_LOG(error) << "Failed to create info scene buffer"sv;
|
||||
return -1;
|
||||
}
|
||||
|
||||
D3D11_RENDER_TARGET_VIEW_DESC nv12_rt_desc {
|
||||
DXGI_FORMAT_R8_UNORM,
|
||||
D3D11_RTV_DIMENSION_TEXTURE2D
|
||||
};
|
||||
|
||||
status = device_p->CreateRenderTargetView(img.texture.get(), &nv12_rt_desc, &nv12_Y_rt);
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Failed to create render target view [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return -1;
|
||||
}
|
||||
|
||||
nv12_rt_desc.Format = DXGI_FORMAT_R8G8_UNORM;
|
||||
|
||||
status = device_p->CreateRenderTargetView(img.texture.get(), &nv12_rt_desc, &nv12_UV_rt);
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Failed to create render target view [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Need to have something refcounted
|
||||
if(!frame->buf[0]) {
|
||||
frame->buf[0] = av_buffer_allocz(sizeof(AVD3D11FrameDescriptor));
|
||||
}
|
||||
|
||||
auto desc = (AVD3D11FrameDescriptor *)frame->buf[0]->data;
|
||||
desc->texture = (ID3D11Texture2D *)img.data;
|
||||
desc->index = 0;
|
||||
|
||||
frame->data[0] = img.data;
|
||||
frame->data[1] = 0;
|
||||
|
||||
frame->linesize[0] = img.row_pitch;
|
||||
|
||||
frame->height = img.height;
|
||||
frame->width = img.width;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int init(
|
||||
std::shared_ptr<platf::display_t> display, device_t::pointer device_p, device_ctx_t::pointer device_ctx_p,
|
||||
pix_fmt_e pix_fmt) {
|
||||
|
||||
HRESULT status;
|
||||
|
||||
device_p->AddRef();
|
||||
data = device_p;
|
||||
|
||||
this->device_ctx_p = device_ctx_p;
|
||||
|
||||
format = (pix_fmt == pix_fmt_e::nv12 ? DXGI_FORMAT_NV12 : DXGI_FORMAT_P010);
|
||||
status = device_p->CreateVertexShader(scene_vs_hlsl->GetBufferPointer(), scene_vs_hlsl->GetBufferSize(), nullptr, &scene_vs);
|
||||
if(status) {
|
||||
BOOST_LOG(error) << "Failed to create scene vertex shader [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
@@ -450,30 +465,12 @@ public:
|
||||
return -1;
|
||||
}
|
||||
|
||||
blend_disable = make_blend(device_p, false);
|
||||
blend_enable = make_blend(device_p, true);
|
||||
|
||||
if(!blend_disable || !blend_enable) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if(_init_rt(scene_sr, scene_rt, in_width, in_height, DXGI_FORMAT_B8G8R8A8_UNORM)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
color_matrix = make_buffer(device_p, colors[0]);
|
||||
color_matrix = make_buffer(device_p, ::video::colors[0]);
|
||||
if(!color_matrix) {
|
||||
BOOST_LOG(error) << "Failed to create color matrix buffer"sv;
|
||||
return -1;
|
||||
}
|
||||
|
||||
float info_in[16 / sizeof(float)] { 1.0f / (float)out_width }; //aligned to 16-byte
|
||||
info_scene = make_buffer(device_p, info_in);
|
||||
if(!info_in) {
|
||||
BOOST_LOG(error) << "Failed to create info scene buffer"sv;
|
||||
return -1;
|
||||
}
|
||||
|
||||
D3D11_INPUT_ELEMENT_DESC layout_desc {
|
||||
"SV_Position", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0
|
||||
};
|
||||
@@ -483,61 +480,7 @@ public:
|
||||
convert_UV_vs_hlsl->GetBufferPointer(), convert_UV_vs_hlsl->GetBufferSize(),
|
||||
&input_layout);
|
||||
|
||||
D3D11_TEXTURE2D_DESC t {};
|
||||
t.Width = out_width;
|
||||
t.Height = out_height;
|
||||
t.MipLevels = 1;
|
||||
t.ArraySize = 1;
|
||||
t.SampleDesc.Count = 1;
|
||||
t.Usage = D3D11_USAGE_DEFAULT;
|
||||
t.Format = pix_fmt == pix_fmt_e::nv12 ? DXGI_FORMAT_NV12 : DXGI_FORMAT_P010;
|
||||
t.BindFlags = D3D11_BIND_RENDER_TARGET;
|
||||
|
||||
status = device_p->CreateTexture2D(&t, nullptr, &img.texture);
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Failed to create render target texture [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return -1;
|
||||
}
|
||||
|
||||
img.display = std::move(display);
|
||||
img.width = out_width;
|
||||
img.height = out_height;
|
||||
img.data = (std::uint8_t *)img.texture.get();
|
||||
img.row_pitch = out_width;
|
||||
img.pixel_pitch = 1;
|
||||
|
||||
D3D11_RENDER_TARGET_VIEW_DESC nv12_rt_desc {
|
||||
DXGI_FORMAT_R8_UNORM,
|
||||
D3D11_RTV_DIMENSION_TEXTURE2D
|
||||
};
|
||||
|
||||
status = device_p->CreateRenderTargetView(img.texture.get(), &nv12_rt_desc, &nv12_Y_rt);
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Failed to create render target view [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return -1;
|
||||
}
|
||||
|
||||
nv12_rt_desc.Format = DXGI_FORMAT_R8G8_UNORM;
|
||||
status = device_p->CreateRenderTargetView(img.texture.get(), &nv12_rt_desc, &nv12_UV_rt);
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Failed to create render target view [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return -1;
|
||||
}
|
||||
|
||||
D3D11_SAMPLER_DESC sampler_desc {};
|
||||
sampler_desc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR;
|
||||
sampler_desc.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP;
|
||||
sampler_desc.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP;
|
||||
sampler_desc.AddressW = D3D11_TEXTURE_ADDRESS_WRAP;
|
||||
sampler_desc.ComparisonFunc = D3D11_COMPARISON_NEVER;
|
||||
sampler_desc.MinLOD = 0;
|
||||
sampler_desc.MaxLOD = D3D11_FLOAT32_MAX;
|
||||
|
||||
status = device_p->CreateSamplerState(&sampler_desc, &sampler_linear);
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Failed to create point sampler state [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return -1;
|
||||
}
|
||||
img.display = std::move(display);
|
||||
|
||||
// Color the background black, so that the padding for keeping the aspect ratio
|
||||
// is black
|
||||
@@ -558,12 +501,9 @@ public:
|
||||
return -1;
|
||||
}
|
||||
|
||||
device_ctx_p->OMSetBlendState(blend_disable.get(), nullptr, 0xFFFFFFFFu);
|
||||
device_ctx_p->PSSetSamplers(0, 1, &sampler_linear);
|
||||
device_ctx_p->IASetInputLayout(input_layout.get());
|
||||
device_ctx_p->PSSetConstantBuffers(0, 1, &color_matrix);
|
||||
device_ctx_p->VSSetConstantBuffers(0, 1, &info_scene);
|
||||
device_ctx_p->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP);
|
||||
device_ctx_p->IASetInputLayout(input_layout.get());
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -572,11 +512,6 @@ public:
|
||||
if(data) {
|
||||
((ID3D11Device *)data)->Release();
|
||||
}
|
||||
|
||||
auto it = std::find(std::begin(*hwdevices_p), std::end(*hwdevices_p), this);
|
||||
if(it != std::end(*hwdevices_p)) {
|
||||
hwdevices_p->erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
@@ -594,73 +529,21 @@ private:
|
||||
_init_view_port(0.0f, 0.0f, width, height);
|
||||
}
|
||||
|
||||
int _init_rt(shader_res_t &shader_res, render_target_t &render_target, int width, int height, DXGI_FORMAT format) {
|
||||
D3D11_TEXTURE2D_DESC desc {};
|
||||
|
||||
desc.Width = width;
|
||||
desc.Height = height;
|
||||
desc.Format = format;
|
||||
desc.Usage = D3D11_USAGE_DEFAULT;
|
||||
desc.BindFlags = D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE;
|
||||
desc.MipLevels = 1;
|
||||
desc.ArraySize = 1;
|
||||
desc.SampleDesc.Count = 1;
|
||||
|
||||
auto device = (device_t::pointer)data;
|
||||
|
||||
texture2d_t tex;
|
||||
auto status = device->CreateTexture2D(&desc, nullptr, &tex);
|
||||
if(status) {
|
||||
BOOST_LOG(error) << "Failed to create render target texture for luma [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
D3D11_SHADER_RESOURCE_VIEW_DESC shader_resource_desc {
|
||||
format,
|
||||
D3D11_SRV_DIMENSION_TEXTURE2D
|
||||
};
|
||||
shader_resource_desc.Texture2D.MipLevels = 1;
|
||||
|
||||
device->CreateShaderResourceView(tex.get(), &shader_resource_desc, &shader_res);
|
||||
if(status) {
|
||||
BOOST_LOG(error) << "Failed to create render target texture for luma [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return -1;
|
||||
}
|
||||
|
||||
D3D11_RENDER_TARGET_VIEW_DESC render_target_desc {
|
||||
format,
|
||||
D3D11_RTV_DIMENSION_TEXTURE2D
|
||||
};
|
||||
|
||||
device->CreateRenderTargetView(tex.get(), &render_target_desc, &render_target);
|
||||
if(status) {
|
||||
BOOST_LOG(error) << "Failed to create render target view [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public:
|
||||
color_t *color_p;
|
||||
frame_t hwframe;
|
||||
|
||||
blend_t blend_enable;
|
||||
blend_t blend_disable;
|
||||
::video::color_t *color_p;
|
||||
|
||||
buf_t info_scene;
|
||||
buf_t color_matrix;
|
||||
|
||||
sampler_state_t sampler_linear;
|
||||
|
||||
input_layout_t input_layout;
|
||||
|
||||
render_target_t nv12_Y_rt;
|
||||
render_target_t nv12_UV_rt;
|
||||
|
||||
render_target_t scene_rt;
|
||||
shader_res_t scene_sr;
|
||||
|
||||
// The image referenced by hwframe
|
||||
// The resulting image is stored here.
|
||||
img_d3d_t img;
|
||||
|
||||
// Clear nv12 render target to black
|
||||
@@ -675,15 +558,41 @@ public:
|
||||
D3D11_VIEWPORT outY_view;
|
||||
D3D11_VIEWPORT outUV_view;
|
||||
|
||||
D3D11_VIEWPORT cursor_view;
|
||||
bool cursor_visible;
|
||||
DXGI_FORMAT format;
|
||||
|
||||
device_ctx_t::pointer device_ctx_p;
|
||||
|
||||
// The destructor will remove itself from the list of hardware devices, this is done synchronously
|
||||
std::vector<hwdevice_t *> *hwdevices_p;
|
||||
};
|
||||
|
||||
capture_e display_vram_t::capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr<::platf::img_t> img, bool *cursor) {
|
||||
auto next_frame = std::chrono::steady_clock::now();
|
||||
|
||||
while(img) {
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
while(next_frame > now) {
|
||||
now = std::chrono::steady_clock::now();
|
||||
}
|
||||
next_frame = now + delay;
|
||||
|
||||
auto status = snapshot(img.get(), 1000ms, *cursor);
|
||||
switch(status) {
|
||||
case platf::capture_e::reinit:
|
||||
case platf::capture_e::error:
|
||||
return status;
|
||||
case platf::capture_e::timeout:
|
||||
std::this_thread::sleep_for(1ms);
|
||||
continue;
|
||||
case platf::capture_e::ok:
|
||||
img = snapshot_cb(img);
|
||||
break;
|
||||
default:
|
||||
BOOST_LOG(error) << "Unrecognized capture status ["sv << (int)status << ']';
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
return capture_e::ok;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -746,41 +655,121 @@ capture_e display_vram_t::snapshot(platf::img_t *img_base, std::chrono::millisec
|
||||
return capture_e::error;
|
||||
}
|
||||
|
||||
for(auto *hwdevice : hwdevices) {
|
||||
if(hwdevice->set_cursor_texture(texture.get(), t.Width, t.Height)) {
|
||||
return capture_e::error;
|
||||
}
|
||||
D3D11_SHADER_RESOURCE_VIEW_DESC desc {
|
||||
DXGI_FORMAT_B8G8R8A8_UNORM,
|
||||
D3D11_SRV_DIMENSION_TEXTURE2D
|
||||
};
|
||||
desc.Texture2D.MipLevels = 1;
|
||||
|
||||
// Free resources before allocating on the next line.
|
||||
cursor.input_res.reset();
|
||||
status = device->CreateShaderResourceView(texture.get(), &desc, &cursor.input_res);
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Failed to create cursor shader resource view [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return capture_e::error;
|
||||
}
|
||||
|
||||
cursor.texture = std::move(texture);
|
||||
cursor.width = t.Width;
|
||||
cursor.height = t.Height;
|
||||
cursor.set_texture(t.Width, t.Height, std::move(texture));
|
||||
}
|
||||
|
||||
if(frame_info.LastMouseUpdateTime.QuadPart) {
|
||||
for(auto *hwdevice : hwdevices) {
|
||||
hwdevice->set_cursor_pos(frame_info.PointerPosition.Position.x, frame_info.PointerPosition.Position.y, frame_info.PointerPosition.Visible && cursor_visible);
|
||||
}
|
||||
cursor.set_pos(frame_info.PointerPosition.Position.x, frame_info.PointerPosition.Position.y, frame_info.PointerPosition.Visible && cursor_visible);
|
||||
}
|
||||
|
||||
if(frame_update_flag) {
|
||||
texture2d_t src;
|
||||
src.reset();
|
||||
status = res->QueryInterface(IID_ID3D11Texture2D, (void **)&src);
|
||||
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Couldn't query interface [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return capture_e::error;
|
||||
}
|
||||
}
|
||||
|
||||
device_ctx->CopyResource(img->texture.get(), src.get());
|
||||
device_ctx->CopyResource(img->texture.get(), src.get());
|
||||
if(cursor.visible) {
|
||||
D3D11_VIEWPORT view {
|
||||
0.0f, 0.0f,
|
||||
(float)width, (float)height,
|
||||
0.0f, 1.0f
|
||||
};
|
||||
|
||||
device_ctx->VSSetShader(scene_vs.get(), nullptr, 0);
|
||||
device_ctx->PSSetShader(scene_ps.get(), nullptr, 0);
|
||||
device_ctx->RSSetViewports(1, &view);
|
||||
device_ctx->OMSetRenderTargets(1, &img->scene_rt, nullptr);
|
||||
device_ctx->PSSetShaderResources(0, 1, &cursor.input_res);
|
||||
device_ctx->OMSetBlendState(blend_enable.get(), nullptr, 0xFFFFFFFFu);
|
||||
device_ctx->RSSetViewports(1, &cursor.cursor_view);
|
||||
device_ctx->Draw(3, 0);
|
||||
device_ctx->OMSetBlendState(blend_disable.get(), nullptr, 0xFFFFFFFFu);
|
||||
}
|
||||
|
||||
return capture_e::ok;
|
||||
}
|
||||
|
||||
int display_vram_t::init(int framerate, const std::string &display_name) {
|
||||
if(display_base_t::init(framerate, display_name)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
D3D11_SAMPLER_DESC sampler_desc {};
|
||||
sampler_desc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR;
|
||||
sampler_desc.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP;
|
||||
sampler_desc.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP;
|
||||
sampler_desc.AddressW = D3D11_TEXTURE_ADDRESS_WRAP;
|
||||
sampler_desc.ComparisonFunc = D3D11_COMPARISON_NEVER;
|
||||
sampler_desc.MinLOD = 0;
|
||||
sampler_desc.MaxLOD = D3D11_FLOAT32_MAX;
|
||||
|
||||
auto status = device->CreateSamplerState(&sampler_desc, &sampler_linear);
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Failed to create point sampler state [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return -1;
|
||||
}
|
||||
|
||||
status = device->CreateVertexShader(scene_vs_hlsl->GetBufferPointer(), scene_vs_hlsl->GetBufferSize(), nullptr, &scene_vs);
|
||||
if(status) {
|
||||
BOOST_LOG(error) << "Failed to create scene vertex shader [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return -1;
|
||||
}
|
||||
|
||||
status = device->CreatePixelShader(scene_ps_hlsl->GetBufferPointer(), scene_ps_hlsl->GetBufferSize(), nullptr, &scene_ps);
|
||||
if(status) {
|
||||
BOOST_LOG(error) << "Failed to create scene pixel shader [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return -1;
|
||||
}
|
||||
|
||||
blend_enable = make_blend(device.get(), true);
|
||||
blend_disable = make_blend(device.get(), false);
|
||||
|
||||
if(!blend_disable || !blend_enable) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
device_ctx->OMSetBlendState(blend_disable.get(), nullptr, 0xFFFFFFFFu);
|
||||
device_ctx->PSSetSamplers(0, 1, &sampler_linear);
|
||||
device_ctx->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::shared_ptr<platf::img_t> display_vram_t::alloc_img() {
|
||||
auto img = std::make_shared<img_d3d_t>();
|
||||
|
||||
img->pixel_pitch = 4;
|
||||
img->row_pitch = img->pixel_pitch * width;
|
||||
img->width = width;
|
||||
img->height = height;
|
||||
img->display = shared_from_this();
|
||||
|
||||
auto dummy_data = std::make_unique<std::uint8_t[]>(img->row_pitch * height);
|
||||
D3D11_SUBRESOURCE_DATA data {
|
||||
dummy_data.get(),
|
||||
(UINT)img->row_pitch
|
||||
};
|
||||
std::fill_n(dummy_data.get(), img->row_pitch * height, 0);
|
||||
|
||||
D3D11_TEXTURE2D_DESC t {};
|
||||
t.Width = width;
|
||||
t.Height = height;
|
||||
@@ -789,20 +778,19 @@ std::shared_ptr<platf::img_t> display_vram_t::alloc_img() {
|
||||
t.SampleDesc.Count = 1;
|
||||
t.Usage = D3D11_USAGE_DEFAULT;
|
||||
t.Format = format;
|
||||
t.BindFlags = D3D11_BIND_SHADER_RESOURCE;
|
||||
t.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET;
|
||||
|
||||
auto status = device->CreateTexture2D(&t, nullptr, &img->texture);
|
||||
auto status = device->CreateTexture2D(&t, &data, &img->texture);
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Failed to create img buf texture [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
img->data = (std::uint8_t *)img->texture.get();
|
||||
img->row_pitch = 0;
|
||||
img->pixel_pitch = 4;
|
||||
img->width = 0;
|
||||
img->height = 0;
|
||||
img->display = shared_from_this();
|
||||
if(init_rt(device.get(), img->input_res, img->scene_rt, width, height, format, img->texture.get())) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
img->data = (std::uint8_t *)img->texture.get();
|
||||
|
||||
return img;
|
||||
}
|
||||
@@ -810,6 +798,10 @@ std::shared_ptr<platf::img_t> display_vram_t::alloc_img() {
|
||||
int display_vram_t::dummy_img(platf::img_t *img_base) {
|
||||
auto img = (img_d3d_t *)img_base;
|
||||
|
||||
if(img->texture) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
img->row_pitch = width * 4;
|
||||
auto dummy_data = std::make_unique<int[]>(width * height);
|
||||
D3D11_SUBRESOURCE_DATA data {
|
||||
@@ -835,54 +827,35 @@ int display_vram_t::dummy_img(platf::img_t *img_base) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
img->texture = std::move(tex);
|
||||
img->data = (std::uint8_t *)img->texture.get();
|
||||
img->height = height;
|
||||
img->width = width;
|
||||
img->pixel_pitch = 4;
|
||||
img->texture = std::move(tex);
|
||||
img->data = (std::uint8_t *)img->texture.get();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::shared_ptr<platf::hwdevice_t> display_vram_t::make_hwdevice(int width, int height, pix_fmt_e pix_fmt) {
|
||||
std::shared_ptr<platf::hwdevice_t> display_vram_t::make_hwdevice(pix_fmt_e pix_fmt) {
|
||||
if(pix_fmt != platf::pix_fmt_e::nv12) {
|
||||
BOOST_LOG(error) << "display_vram_t doesn't support pixel format ["sv << from_pix_fmt(pix_fmt) << ']';
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto hwdevice = std::make_shared<hwdevice_t>(&hwdevices);
|
||||
auto hwdevice = std::make_shared<hwdevice_t>();
|
||||
|
||||
auto ret = hwdevice->init(
|
||||
shared_from_this(),
|
||||
device.get(),
|
||||
device_ctx.get(),
|
||||
width, height,
|
||||
pix_fmt);
|
||||
|
||||
if(ret) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if(cursor.texture && hwdevice->set_cursor_texture(cursor.texture.get(), cursor.width, cursor.height)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
hwdevices.emplace_back(hwdevice.get());
|
||||
|
||||
return hwdevice;
|
||||
}
|
||||
|
||||
int init() {
|
||||
for(auto &color : colors) {
|
||||
BOOST_LOG(debug) << "Color Matrix"sv;
|
||||
BOOST_LOG(debug) << "Y ["sv << color.color_vec_y.x << ", "sv << color.color_vec_y.y << ", "sv << color.color_vec_y.z << ", "sv << color.color_vec_y.w << ']';
|
||||
BOOST_LOG(debug) << "U ["sv << color.color_vec_u.x << ", "sv << color.color_vec_u.y << ", "sv << color.color_vec_u.z << ", "sv << color.color_vec_u.w << ']';
|
||||
BOOST_LOG(debug) << "V ["sv << color.color_vec_v.x << ", "sv << color.color_vec_v.y << ", "sv << color.color_vec_v.z << ", "sv << color.color_vec_v.w << ']';
|
||||
BOOST_LOG(debug) << "range Y ["sv << color.range_y.x << ", "sv << color.range_y.y << ']';
|
||||
BOOST_LOG(debug) << "range UV ["sv << color.range_uv.x << ", "sv << color.range_uv.y << ']';
|
||||
}
|
||||
|
||||
BOOST_LOG(info) << "Compiling shaders..."sv;
|
||||
scene_vs_hlsl = compile_vertex_shader(SUNSHINE_SHADERS_DIR "/SceneVS.hlsl");
|
||||
if(!scene_vs_hlsl) {
|
||||
|
||||
@@ -1,39 +1,51 @@
|
||||
#include <cmath>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
|
||||
// prevent clang format from "optimizing" the header include order
|
||||
// clang-format off
|
||||
#include <winsock2.h>
|
||||
#include <iphlpapi.h>
|
||||
#include <windows.h>
|
||||
#include <winuser.h>
|
||||
#include <ws2tcpip.h>
|
||||
// clang-format on
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include <ViGEm/Client.h>
|
||||
|
||||
#include "misc.h"
|
||||
#include "sunshine/config.h"
|
||||
#include "sunshine/main.h"
|
||||
#include "sunshine/platform/common.h"
|
||||
|
||||
namespace platf {
|
||||
using namespace std::literals;
|
||||
|
||||
using adapteraddrs_t = util::c_ptr<IP_ADAPTER_ADDRESSES>;
|
||||
thread_local HDESK _lastKnownInputDesktop = nullptr;
|
||||
|
||||
volatile HDESK _lastKnownInputDesktop = NULL;
|
||||
constexpr touch_port_t target_touch_port {
|
||||
0, 0,
|
||||
65535, 65535
|
||||
};
|
||||
|
||||
HDESK pairInputDesktop();
|
||||
using client_t = util::safe_ptr<_VIGEM_CLIENT_T, vigem_free>;
|
||||
using target_t = util::safe_ptr<_VIGEM_TARGET_T, vigem_target_free>;
|
||||
|
||||
static VIGEM_TARGET_TYPE map(const std::string_view &gp) {
|
||||
if(gp == "x360"sv) {
|
||||
return Xbox360Wired;
|
||||
}
|
||||
|
||||
return DualShock4Wired;
|
||||
}
|
||||
|
||||
void CALLBACK x360_notify(
|
||||
client_t::pointer client,
|
||||
target_t::pointer target,
|
||||
std::uint8_t largeMotor, std::uint8_t smallMotor,
|
||||
std::uint8_t /* led_number */,
|
||||
void *userdata);
|
||||
|
||||
void CALLBACK ds4_notify(
|
||||
client_t::pointer client,
|
||||
target_t::pointer target,
|
||||
std::uint8_t largeMotor, std::uint8_t smallMotor,
|
||||
DS4_LIGHTBAR_COLOR /* led_color */,
|
||||
void *userdata);
|
||||
|
||||
class vigem_t {
|
||||
public:
|
||||
using client_t = util::safe_ptr<_VIGEM_CLIENT_T, vigem_free>;
|
||||
using target_t = util::safe_ptr<_VIGEM_TARGET_T, vigem_target_free>;
|
||||
|
||||
int init() {
|
||||
VIGEM_ERROR status;
|
||||
|
||||
@@ -46,44 +58,75 @@ public:
|
||||
return -1;
|
||||
}
|
||||
|
||||
x360s.resize(MAX_GAMEPADS);
|
||||
gamepads.resize(MAX_GAMEPADS);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int alloc_x360(int nr) {
|
||||
auto &x360 = x360s[nr];
|
||||
assert(!x360);
|
||||
int alloc_gamepad_interal(int nr, rumble_queue_t &rumble_queue, VIGEM_TARGET_TYPE gp_type) {
|
||||
auto &[rumble, gp] = gamepads[nr];
|
||||
assert(!gp);
|
||||
|
||||
x360.reset(vigem_target_x360_alloc());
|
||||
auto status = vigem_target_add(client.get(), x360.get());
|
||||
if(gp_type == Xbox360Wired) {
|
||||
gp.reset(vigem_target_x360_alloc());
|
||||
}
|
||||
else {
|
||||
gp.reset(vigem_target_ds4_alloc());
|
||||
}
|
||||
|
||||
auto status = vigem_target_add(client.get(), gp.get());
|
||||
if(!VIGEM_SUCCESS(status)) {
|
||||
BOOST_LOG(error) << "Couldn't add Gamepad to ViGEm connection ["sv << util::hex(status).to_string_view() << ']';
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
rumble = std::move(rumble_queue);
|
||||
|
||||
if(gp_type == Xbox360Wired) {
|
||||
status = vigem_target_x360_register_notification(client.get(), gp.get(), x360_notify, this);
|
||||
}
|
||||
else {
|
||||
status = vigem_target_ds4_register_notification(client.get(), gp.get(), ds4_notify, this);
|
||||
}
|
||||
|
||||
if(!VIGEM_SUCCESS(status)) {
|
||||
BOOST_LOG(warning) << "Couldn't register notifications for rumble support ["sv << util::hex(status).to_string_view() << ']';
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void free_target(int nr) {
|
||||
auto &x360 = x360s[nr];
|
||||
auto &[_, gp] = gamepads[nr];
|
||||
|
||||
if(x360 && vigem_target_is_attached(x360.get())) {
|
||||
auto status = vigem_target_remove(client.get(), x360.get());
|
||||
if(gp && vigem_target_is_attached(gp.get())) {
|
||||
auto status = vigem_target_remove(client.get(), gp.get());
|
||||
if(!VIGEM_SUCCESS(status)) {
|
||||
BOOST_LOG(warning) << "Couldn't detach gamepad from ViGEm ["sv << util::hex(status).to_string_view() << ']';
|
||||
}
|
||||
}
|
||||
|
||||
x360.reset();
|
||||
gp.reset();
|
||||
}
|
||||
|
||||
void rumble(target_t::pointer target, std::uint8_t smallMotor, std::uint8_t largeMotor) {
|
||||
for(int x = 0; x < gamepads.size(); ++x) {
|
||||
auto &[rumble_queue, gp] = gamepads[x];
|
||||
|
||||
if(gp.get() == target) {
|
||||
rumble_queue->raise(x, ((std::uint16_t)smallMotor) << 8, ((std::uint16_t)largeMotor) << 8);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
~vigem_t() {
|
||||
if(client) {
|
||||
for(auto &x360 : x360s) {
|
||||
if(x360 && vigem_target_is_attached(x360.get())) {
|
||||
auto status = vigem_target_remove(client.get(), x360.get());
|
||||
for(auto &[_, gp] : gamepads) {
|
||||
if(gp && vigem_target_is_attached(gp.get())) {
|
||||
auto status = vigem_target_remove(client.get(), gp.get());
|
||||
if(!VIGEM_SUCCESS(status)) {
|
||||
BOOST_LOG(warning) << "Couldn't detach gamepad from ViGEm ["sv << util::hex(status).to_string_view() << ']';
|
||||
}
|
||||
@@ -94,73 +137,37 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<target_t> x360s;
|
||||
std::vector<std::pair<rumble_queue_t, target_t>> gamepads;
|
||||
|
||||
client_t client;
|
||||
};
|
||||
|
||||
std::string from_sockaddr(const sockaddr *const socket_address) {
|
||||
char data[INET6_ADDRSTRLEN];
|
||||
void CALLBACK x360_notify(
|
||||
client_t::pointer client,
|
||||
target_t::pointer target,
|
||||
std::uint8_t largeMotor, std::uint8_t smallMotor,
|
||||
std::uint8_t /* led_number */,
|
||||
void *userdata) {
|
||||
|
||||
auto family = socket_address->sa_family;
|
||||
if(family == AF_INET6) {
|
||||
inet_ntop(AF_INET6, &((sockaddr_in6 *)socket_address)->sin6_addr, data, INET6_ADDRSTRLEN);
|
||||
}
|
||||
BOOST_LOG(debug)
|
||||
<< "largeMotor: "sv << (int)largeMotor << std::endl
|
||||
<< "smallMotor: "sv << (int)smallMotor;
|
||||
|
||||
if(family == AF_INET) {
|
||||
inet_ntop(AF_INET, &((sockaddr_in *)socket_address)->sin_addr, data, INET_ADDRSTRLEN);
|
||||
}
|
||||
|
||||
return std::string { data };
|
||||
task_pool.push(&vigem_t::rumble, (vigem_t *)userdata, target, smallMotor, largeMotor);
|
||||
}
|
||||
|
||||
std::pair<std::uint16_t, std::string> from_sockaddr_ex(const sockaddr *const ip_addr) {
|
||||
char data[INET6_ADDRSTRLEN];
|
||||
void CALLBACK ds4_notify(
|
||||
client_t::pointer client,
|
||||
target_t::pointer target,
|
||||
std::uint8_t largeMotor, std::uint8_t smallMotor,
|
||||
DS4_LIGHTBAR_COLOR /* led_color */,
|
||||
void *userdata) {
|
||||
|
||||
auto family = ip_addr->sa_family;
|
||||
std::uint16_t port;
|
||||
if(family == AF_INET6) {
|
||||
inet_ntop(AF_INET6, &((sockaddr_in6 *)ip_addr)->sin6_addr, data, INET6_ADDRSTRLEN);
|
||||
port = ((sockaddr_in6 *)ip_addr)->sin6_port;
|
||||
}
|
||||
BOOST_LOG(debug)
|
||||
<< "largeMotor: "sv << (int)largeMotor << std::endl
|
||||
<< "smallMotor: "sv << (int)smallMotor;
|
||||
|
||||
if(family == AF_INET) {
|
||||
inet_ntop(AF_INET, &((sockaddr_in *)ip_addr)->sin_addr, data, INET_ADDRSTRLEN);
|
||||
port = ((sockaddr_in *)ip_addr)->sin_port;
|
||||
}
|
||||
|
||||
return { port, std::string { data } };
|
||||
}
|
||||
|
||||
adapteraddrs_t get_adapteraddrs() {
|
||||
adapteraddrs_t info { nullptr };
|
||||
ULONG size = 0;
|
||||
|
||||
while(GetAdaptersAddresses(AF_UNSPEC, 0, nullptr, info.get(), &size) == ERROR_BUFFER_OVERFLOW) {
|
||||
info.reset((PIP_ADAPTER_ADDRESSES)malloc(size));
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
std::string get_mac_address(const std::string_view &address) {
|
||||
adapteraddrs_t info = get_adapteraddrs();
|
||||
for(auto adapter_pos = info.get(); adapter_pos != nullptr; adapter_pos = adapter_pos->Next) {
|
||||
for(auto addr_pos = adapter_pos->FirstUnicastAddress; addr_pos != nullptr; addr_pos = addr_pos->Next) {
|
||||
if(adapter_pos->PhysicalAddressLength != 0 && address == from_sockaddr(addr_pos->Address.lpSockaddr)) {
|
||||
std::stringstream mac_addr;
|
||||
mac_addr << std::hex;
|
||||
for(int i = 0; i < adapter_pos->PhysicalAddressLength; i++) {
|
||||
if(i > 0) {
|
||||
mac_addr << ':';
|
||||
}
|
||||
mac_addr << std::setw(2) << std::setfill('0') << (int)adapter_pos->PhysicalAddress[i];
|
||||
}
|
||||
return mac_addr.str();
|
||||
}
|
||||
}
|
||||
}
|
||||
BOOST_LOG(warning) << "Unable to find MAC address for "sv << address;
|
||||
return "00:00:00:00:00:00"s;
|
||||
task_pool.push(&vigem_t::rumble, (vigem_t *)userdata, target, smallMotor, largeMotor);
|
||||
}
|
||||
|
||||
input_t input() {
|
||||
@@ -178,14 +185,15 @@ void send_input(INPUT &i) {
|
||||
retry:
|
||||
auto send = SendInput(1, &i, sizeof(INPUT));
|
||||
if(send != 1) {
|
||||
auto hDesk = pairInputDesktop();
|
||||
auto hDesk = syncThreadDesktop();
|
||||
if(_lastKnownInputDesktop != hDesk) {
|
||||
_lastKnownInputDesktop = hDesk;
|
||||
goto retry;
|
||||
}
|
||||
BOOST_LOG(warning) << "Couldn't send input"sv;
|
||||
BOOST_LOG(error) << "Couldn't send input"sv;
|
||||
}
|
||||
}
|
||||
|
||||
void abs_mouse(input_t &input, const touch_port_t &touch_port, float x, float y) {
|
||||
INPUT i {};
|
||||
|
||||
@@ -277,10 +285,6 @@ void scroll(input_t &input, int distance) {
|
||||
}
|
||||
|
||||
void keyboard(input_t &input, uint16_t modcode, bool release) {
|
||||
if(modcode == VK_RMENU) {
|
||||
modcode = VK_LWIN;
|
||||
}
|
||||
|
||||
INPUT i {};
|
||||
i.type = INPUT_KEYBOARD;
|
||||
auto &ki = i.ki;
|
||||
@@ -322,12 +326,12 @@ void keyboard(input_t &input, uint16_t modcode, bool release) {
|
||||
send_input(i);
|
||||
}
|
||||
|
||||
int alloc_gamepad(input_t &input, int nr) {
|
||||
int alloc_gamepad(input_t &input, int nr, rumble_queue_t rumble_queue) {
|
||||
if(!input) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return ((vigem_t *)input.get())->alloc_x360(nr);
|
||||
return ((vigem_t *)input.get())->alloc_gamepad_interal(nr, rumble_queue, map(config::input.gamepad));
|
||||
}
|
||||
|
||||
void free_gamepad(input_t &input, int nr) {
|
||||
@@ -337,6 +341,112 @@ void free_gamepad(input_t &input, int nr) {
|
||||
|
||||
((vigem_t *)input.get())->free_target(nr);
|
||||
}
|
||||
|
||||
static VIGEM_ERROR x360_update(client_t::pointer client, target_t::pointer gp, const gamepad_state_t &gamepad_state) {
|
||||
auto &xusb = *(PXUSB_REPORT)&gamepad_state;
|
||||
|
||||
return vigem_target_x360_update(client, gp, xusb);
|
||||
}
|
||||
|
||||
static DS4_DPAD_DIRECTIONS ds4_dpad(const gamepad_state_t &gamepad_state) {
|
||||
auto flags = gamepad_state.buttonFlags;
|
||||
if(flags & DPAD_UP) {
|
||||
if(flags & DPAD_RIGHT) {
|
||||
return DS4_BUTTON_DPAD_NORTHEAST;
|
||||
}
|
||||
else if(flags & DPAD_LEFT) {
|
||||
return DS4_BUTTON_DPAD_NORTHWEST;
|
||||
}
|
||||
else {
|
||||
return DS4_BUTTON_DPAD_NORTH;
|
||||
}
|
||||
}
|
||||
|
||||
else if(flags & DPAD_DOWN) {
|
||||
if(flags & DPAD_RIGHT) {
|
||||
return DS4_BUTTON_DPAD_SOUTHEAST;
|
||||
}
|
||||
else if(flags & DPAD_LEFT) {
|
||||
return DS4_BUTTON_DPAD_SOUTHWEST;
|
||||
}
|
||||
else {
|
||||
return DS4_BUTTON_DPAD_SOUTH;
|
||||
}
|
||||
}
|
||||
|
||||
else if(flags & DPAD_RIGHT) {
|
||||
return DS4_BUTTON_DPAD_EAST;
|
||||
}
|
||||
|
||||
else if(flags & DPAD_LEFT) {
|
||||
return DS4_BUTTON_DPAD_WEST;
|
||||
}
|
||||
|
||||
return DS4_BUTTON_DPAD_NONE;
|
||||
}
|
||||
|
||||
static DS4_BUTTONS ds4_buttons(const gamepad_state_t &gamepad_state) {
|
||||
int buttons {};
|
||||
|
||||
auto flags = gamepad_state.buttonFlags;
|
||||
// clang-format off
|
||||
if(flags & LEFT_STICK) buttons |= DS4_BUTTON_THUMB_LEFT;
|
||||
if(flags & RIGHT_STICK) buttons |= DS4_BUTTON_THUMB_RIGHT;
|
||||
if(flags & LEFT_BUTTON) buttons |= DS4_BUTTON_SHOULDER_LEFT;
|
||||
if(flags & RIGHT_BUTTON) buttons |= DS4_BUTTON_SHOULDER_RIGHT;
|
||||
if(flags & START) buttons |= DS4_BUTTON_OPTIONS;
|
||||
if(flags & A) buttons |= DS4_BUTTON_CROSS;
|
||||
if(flags & B) buttons |= DS4_BUTTON_CIRCLE;
|
||||
if(flags & X) buttons |= DS4_BUTTON_SQUARE;
|
||||
if(flags & Y) buttons |= DS4_BUTTON_TRIANGLE;
|
||||
|
||||
if(gamepad_state.lt > 0) buttons |= DS4_BUTTON_TRIGGER_LEFT;
|
||||
if(gamepad_state.rt > 0) buttons |= DS4_BUTTON_TRIGGER_RIGHT;
|
||||
// clang-format on
|
||||
|
||||
return (DS4_BUTTONS)buttons;
|
||||
}
|
||||
|
||||
static DS4_SPECIAL_BUTTONS 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;
|
||||
}
|
||||
|
||||
static std::uint8_t to_ds4_triggerX(std::int16_t v) {
|
||||
return (v + std::numeric_limits<std::uint16_t>::max() / 2 + 1) / 257;
|
||||
}
|
||||
|
||||
static std::uint8_t to_ds4_triggerY(std::int16_t v) {
|
||||
auto new_v = -((std::numeric_limits<std::uint16_t>::max() / 2 + v - 1)) / 257;
|
||||
|
||||
return new_v == 0 ? 0xFF : (std::uint8_t)new_v;
|
||||
}
|
||||
|
||||
static VIGEM_ERROR ds4_update(client_t::pointer client, target_t::pointer gp, const gamepad_state_t &gamepad_state) {
|
||||
DS4_REPORT report;
|
||||
|
||||
DS4_REPORT_INIT(&report);
|
||||
DS4_SET_DPAD(&report, ds4_dpad(gamepad_state));
|
||||
report.wButtons |= ds4_buttons(gamepad_state);
|
||||
report.bSpecial = ds4_special_buttons(gamepad_state);
|
||||
|
||||
report.bTriggerL = gamepad_state.lt;
|
||||
report.bTriggerR = gamepad_state.rt;
|
||||
|
||||
report.bThumbLX = to_ds4_triggerX(gamepad_state.lsX);
|
||||
report.bThumbLY = to_ds4_triggerY(gamepad_state.lsY);
|
||||
|
||||
report.bThumbRX = to_ds4_triggerX(gamepad_state.rsX);
|
||||
report.bThumbRY = to_ds4_triggerY(gamepad_state.rsY);
|
||||
|
||||
return vigem_target_ds4_update(client, gp, report);
|
||||
}
|
||||
|
||||
|
||||
void gamepad(input_t &input, int nr, const gamepad_state_t &gamepad_state) {
|
||||
// If there is no gamepad support
|
||||
if(!input) {
|
||||
@@ -345,10 +455,17 @@ void gamepad(input_t &input, int nr, const gamepad_state_t &gamepad_state) {
|
||||
|
||||
auto vigem = (vigem_t *)input.get();
|
||||
|
||||
auto &xusb = *(PXUSB_REPORT)&gamepad_state;
|
||||
auto &x360 = vigem->x360s[nr];
|
||||
auto &[_, gp] = vigem->gamepads[nr];
|
||||
|
||||
VIGEM_ERROR status;
|
||||
|
||||
if(vigem_target_get_type(gp.get()) == Xbox360Wired) {
|
||||
status = x360_update(vigem->client.get(), gp.get(), gamepad_state);
|
||||
}
|
||||
else {
|
||||
status = ds4_update(vigem->client.get(), gp.get(), gamepad_state);
|
||||
}
|
||||
|
||||
auto status = vigem_target_x360_update(vigem->client.get(), x360.get(), xusb);
|
||||
if(!VIGEM_SUCCESS(status)) {
|
||||
BOOST_LOG(fatal) << "Couldn't send gamepad input to ViGEm ["sv << util::hex(status).to_string_view() << ']';
|
||||
|
||||
@@ -357,32 +474,18 @@ void gamepad(input_t &input, int nr, const gamepad_state_t &gamepad_state) {
|
||||
}
|
||||
}
|
||||
|
||||
int thread_priority() {
|
||||
return SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_HIGHEST) ? 0 : 1;
|
||||
}
|
||||
|
||||
HDESK pairInputDesktop() {
|
||||
auto hDesk = OpenInputDesktop(DF_ALLOWOTHERACCOUNTHOOK, FALSE, GENERIC_ALL);
|
||||
if(NULL == hDesk) {
|
||||
auto err = GetLastError();
|
||||
BOOST_LOG(error) << "Failed to OpenInputDesktop [0x"sv << util::hex(err).to_string_view() << ']';
|
||||
}
|
||||
else {
|
||||
BOOST_LOG(info) << std::endl
|
||||
<< "Opened desktop [0x"sv << util::hex(hDesk).to_string_view() << ']';
|
||||
if(!SetThreadDesktop(hDesk)) {
|
||||
auto err = GetLastError();
|
||||
BOOST_LOG(error) << "Failed to SetThreadDesktop [0x"sv << util::hex(err).to_string_view() << ']';
|
||||
}
|
||||
CloseDesktop(hDesk);
|
||||
}
|
||||
|
||||
return hDesk;
|
||||
}
|
||||
|
||||
void freeInput(void *p) {
|
||||
auto vigem = (vigem_t *)p;
|
||||
|
||||
delete vigem;
|
||||
}
|
||||
|
||||
std::vector<std::string_view> &supported_gamepads() {
|
||||
// ds4 == ps4
|
||||
static std::vector<std::string_view> gps {
|
||||
"x360"sv, "ds4"sv, "ps4"sv
|
||||
};
|
||||
|
||||
return gps;
|
||||
}
|
||||
} // namespace platf
|
||||
|
||||
123
sunshine/platform/windows/misc.cpp
Normal file
123
sunshine/platform/windows/misc.cpp
Normal file
@@ -0,0 +1,123 @@
|
||||
#include <filesystem>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
|
||||
|
||||
// prevent clang format from "optimizing" the header include order
|
||||
// clang-format off
|
||||
#include <winsock2.h>
|
||||
#include <iphlpapi.h>
|
||||
#include <windows.h>
|
||||
#include <winuser.h>
|
||||
#include <ws2tcpip.h>
|
||||
// clang-format on
|
||||
|
||||
#include "sunshine/main.h"
|
||||
#include "sunshine/utility.h"
|
||||
|
||||
using namespace std::literals;
|
||||
namespace platf {
|
||||
using adapteraddrs_t = util::c_ptr<IP_ADAPTER_ADDRESSES>;
|
||||
|
||||
std::filesystem::path appdata() {
|
||||
return L"."sv;
|
||||
}
|
||||
|
||||
std::string from_sockaddr(const sockaddr *const socket_address) {
|
||||
char data[INET6_ADDRSTRLEN];
|
||||
|
||||
auto family = socket_address->sa_family;
|
||||
if(family == AF_INET6) {
|
||||
inet_ntop(AF_INET6, &((sockaddr_in6 *)socket_address)->sin6_addr, data, INET6_ADDRSTRLEN);
|
||||
}
|
||||
|
||||
if(family == AF_INET) {
|
||||
inet_ntop(AF_INET, &((sockaddr_in *)socket_address)->sin_addr, data, INET_ADDRSTRLEN);
|
||||
}
|
||||
|
||||
return std::string { data };
|
||||
}
|
||||
|
||||
std::pair<std::uint16_t, std::string> from_sockaddr_ex(const sockaddr *const ip_addr) {
|
||||
char data[INET6_ADDRSTRLEN];
|
||||
|
||||
auto family = ip_addr->sa_family;
|
||||
std::uint16_t port;
|
||||
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) {
|
||||
inet_ntop(AF_INET, &((sockaddr_in *)ip_addr)->sin_addr, data, INET_ADDRSTRLEN);
|
||||
port = ((sockaddr_in *)ip_addr)->sin_port;
|
||||
}
|
||||
|
||||
return { port, std::string { data } };
|
||||
}
|
||||
|
||||
adapteraddrs_t get_adapteraddrs() {
|
||||
adapteraddrs_t info { nullptr };
|
||||
ULONG size = 0;
|
||||
|
||||
while(GetAdaptersAddresses(AF_UNSPEC, 0, nullptr, info.get(), &size) == ERROR_BUFFER_OVERFLOW) {
|
||||
info.reset((PIP_ADAPTER_ADDRESSES)malloc(size));
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
std::string get_mac_address(const std::string_view &address) {
|
||||
adapteraddrs_t info = get_adapteraddrs();
|
||||
for(auto adapter_pos = info.get(); adapter_pos != nullptr; adapter_pos = adapter_pos->Next) {
|
||||
for(auto addr_pos = adapter_pos->FirstUnicastAddress; addr_pos != nullptr; addr_pos = addr_pos->Next) {
|
||||
if(adapter_pos->PhysicalAddressLength != 0 && address == from_sockaddr(addr_pos->Address.lpSockaddr)) {
|
||||
std::stringstream mac_addr;
|
||||
mac_addr << std::hex;
|
||||
for(int i = 0; i < adapter_pos->PhysicalAddressLength; i++) {
|
||||
if(i > 0) {
|
||||
mac_addr << ':';
|
||||
}
|
||||
mac_addr << std::setw(2) << std::setfill('0') << (int)adapter_pos->PhysicalAddress[i];
|
||||
}
|
||||
return mac_addr.str();
|
||||
}
|
||||
}
|
||||
}
|
||||
BOOST_LOG(warning) << "Unable to find MAC address for "sv << address;
|
||||
return "00:00:00:00:00:00"s;
|
||||
}
|
||||
|
||||
HDESK syncThreadDesktop() {
|
||||
auto hDesk = OpenInputDesktop(DF_ALLOWOTHERACCOUNTHOOK, FALSE, GENERIC_ALL);
|
||||
if(!hDesk) {
|
||||
auto err = GetLastError();
|
||||
BOOST_LOG(error) << "Failed to Open Input Desktop [0x"sv << util::hex(err).to_string_view() << ']';
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if(!SetThreadDesktop(hDesk)) {
|
||||
auto err = GetLastError();
|
||||
BOOST_LOG(error) << "Failed to sync desktop to thread [0x"sv << util::hex(err).to_string_view() << ']';
|
||||
}
|
||||
|
||||
CloseDesktop(hDesk);
|
||||
|
||||
return hDesk;
|
||||
}
|
||||
|
||||
void print_status(const std::string_view &prefix, HRESULT status) {
|
||||
char err_string[1024];
|
||||
|
||||
DWORD bytes = FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
|
||||
nullptr,
|
||||
status,
|
||||
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
|
||||
err_string,
|
||||
sizeof(err_string),
|
||||
nullptr);
|
||||
|
||||
BOOST_LOG(error) << prefix << ": "sv << std::string_view { err_string, bytes };
|
||||
}
|
||||
} // namespace platf
|
||||
13
sunshine/platform/windows/misc.h
Normal file
13
sunshine/platform/windows/misc.h
Normal file
@@ -0,0 +1,13 @@
|
||||
#ifndef SUNSHINE_WINDOWS_MISC_H
|
||||
#define SUNSHINE_WINDOWS_MISC_H
|
||||
|
||||
#include <windows.h>
|
||||
#include <winnt.h>
|
||||
#include <string_view>
|
||||
|
||||
namespace platf {
|
||||
void print_status(const std::string_view &prefix, HRESULT status);
|
||||
HDESK syncThreadDesktop();
|
||||
}
|
||||
|
||||
#endif
|
||||
189
sunshine/platform/windows/publish.cpp
Normal file
189
sunshine/platform/windows/publish.cpp
Normal file
@@ -0,0 +1,189 @@
|
||||
#include <winsock2.h>
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <windns.h>
|
||||
#include <winerror.h>
|
||||
|
||||
#include <boost/asio/ip/host_name.hpp>
|
||||
|
||||
#include "misc.h"
|
||||
#include "sunshine/config.h"
|
||||
#include "sunshine/main.h"
|
||||
#include "sunshine/network.h"
|
||||
#include "sunshine/nvhttp.h"
|
||||
#include "sunshine/platform/common.h"
|
||||
#include "sunshine/thread_safe.h"
|
||||
|
||||
#define _FN(x, ret, args) \
|
||||
typedef ret(*x##_fn) args; \
|
||||
static x##_fn x
|
||||
|
||||
using namespace std::literals;
|
||||
|
||||
#define __SV(quote) L##quote##sv
|
||||
#define SV(quote) __SV(quote)
|
||||
|
||||
extern "C" {
|
||||
constexpr auto DNS_REQUEST_PENDING = 9506L;
|
||||
constexpr auto DNS_QUERY_REQUEST_VERSION1 = 0x1;
|
||||
constexpr auto DNS_QUERY_RESULTS_VERSION1 = 0x1;
|
||||
|
||||
#define SERVICE_DOMAIN "local"
|
||||
|
||||
constexpr auto SERVICE_INSTANCE_NAME = SV(SERVICE_NAME "." SERVICE_TYPE "." SERVICE_DOMAIN);
|
||||
constexpr auto SERVICE_TYPE_DOMAIN = SV(SERVICE_TYPE "." SERVICE_DOMAIN);
|
||||
|
||||
typedef struct _DNS_SERVICE_INSTANCE {
|
||||
LPWSTR pszInstanceName;
|
||||
LPWSTR pszHostName;
|
||||
|
||||
IP4_ADDRESS *ip4Address;
|
||||
IP6_ADDRESS *ip6Address;
|
||||
|
||||
WORD wPort;
|
||||
WORD wPriority;
|
||||
WORD wWeight;
|
||||
|
||||
// Property list
|
||||
DWORD dwPropertyCount;
|
||||
|
||||
PWSTR *keys;
|
||||
PWSTR *values;
|
||||
|
||||
DWORD dwInterfaceIndex;
|
||||
} DNS_SERVICE_INSTANCE, *PDNS_SERVICE_INSTANCE;
|
||||
|
||||
typedef VOID WINAPI DNS_SERVICE_REGISTER_COMPLETE(
|
||||
_In_ DWORD Status,
|
||||
_In_ PVOID pQueryContext,
|
||||
_In_ PDNS_SERVICE_INSTANCE pInstance);
|
||||
|
||||
typedef DNS_SERVICE_REGISTER_COMPLETE *PDNS_SERVICE_REGISTER_COMPLETE;
|
||||
|
||||
typedef struct _DNS_SERVICE_CANCEL {
|
||||
PVOID reserved;
|
||||
} DNS_SERVICE_CANCEL, *PDNS_SERVICE_CANCEL;
|
||||
|
||||
typedef struct _DNS_SERVICE_REGISTER_REQUEST {
|
||||
ULONG Version;
|
||||
ULONG InterfaceIndex;
|
||||
PDNS_SERVICE_INSTANCE pServiceInstance;
|
||||
PDNS_SERVICE_REGISTER_COMPLETE pRegisterCompletionCallback;
|
||||
PVOID pQueryContext;
|
||||
HANDLE hCredentials;
|
||||
BOOL unicastEnabled;
|
||||
} DNS_SERVICE_REGISTER_REQUEST, *PDNS_SERVICE_REGISTER_REQUEST;
|
||||
|
||||
_FN(_DnsServiceFreeInstance, VOID, (_In_ PDNS_SERVICE_INSTANCE pInstance));
|
||||
_FN(_DnsServiceDeRegister, DWORD, (_In_ PDNS_SERVICE_REGISTER_REQUEST pRequest, _Inout_opt_ PDNS_SERVICE_CANCEL pCancel));
|
||||
_FN(_DnsServiceRegister, DWORD, (_In_ PDNS_SERVICE_REGISTER_REQUEST pRequest, _Inout_opt_ PDNS_SERVICE_CANCEL pCancel));
|
||||
} /* extern "C" */
|
||||
|
||||
namespace platf::publish {
|
||||
VOID WINAPI register_cb(DWORD status, PVOID pQueryContext, PDNS_SERVICE_INSTANCE pInstance) {
|
||||
auto alarm = (safe::alarm_t<DNS_STATUS>::element_type *)pQueryContext;
|
||||
|
||||
auto fg = util::fail_guard([&]() {
|
||||
if(pInstance) {
|
||||
_DnsServiceFreeInstance(pInstance);
|
||||
}
|
||||
});
|
||||
|
||||
if(status) {
|
||||
print_status("register_cb()"sv, status);
|
||||
alarm->ring(-1);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
alarm->ring(0);
|
||||
}
|
||||
|
||||
static int service(bool enable) {
|
||||
auto alarm = safe::make_alarm<DNS_STATUS>();
|
||||
|
||||
std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>, wchar_t> converter;
|
||||
|
||||
std::wstring name { SERVICE_INSTANCE_NAME.data(), SERVICE_INSTANCE_NAME.size() };
|
||||
std::wstring domain { SERVICE_TYPE_DOMAIN.data(), SERVICE_TYPE_DOMAIN.size() };
|
||||
|
||||
auto host = converter.from_bytes(boost::asio::ip::host_name() + ".local");
|
||||
|
||||
DNS_SERVICE_INSTANCE instance {};
|
||||
instance.pszInstanceName = name.data();
|
||||
instance.wPort = map_port(nvhttp::PORT_HTTP);
|
||||
instance.pszHostName = host.data();
|
||||
|
||||
DNS_SERVICE_REGISTER_REQUEST req {};
|
||||
req.Version = DNS_QUERY_REQUEST_VERSION1;
|
||||
req.pQueryContext = alarm.get();
|
||||
req.pServiceInstance = &instance;
|
||||
req.pRegisterCompletionCallback = register_cb;
|
||||
|
||||
DNS_STATUS status {};
|
||||
|
||||
if(enable) {
|
||||
status = _DnsServiceRegister(&req, nullptr);
|
||||
}
|
||||
else {
|
||||
status = _DnsServiceDeRegister(&req, nullptr);
|
||||
}
|
||||
|
||||
alarm->wait();
|
||||
|
||||
status = *alarm->status();
|
||||
if(status) {
|
||||
BOOST_LOG(error) << "No mDNS service"sv;
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
class deinit_t : public ::platf::deinit_t {
|
||||
public:
|
||||
~deinit_t() override {
|
||||
if(service(false)) {
|
||||
std::abort();
|
||||
}
|
||||
|
||||
BOOST_LOG(info) << "Unregistered Sunshine Gamestream service"sv;
|
||||
}
|
||||
};
|
||||
|
||||
int load_funcs(HMODULE handle) {
|
||||
auto fg = util::fail_guard([handle]() {
|
||||
FreeLibrary(handle);
|
||||
});
|
||||
|
||||
_DnsServiceFreeInstance = (_DnsServiceFreeInstance_fn)GetProcAddress(handle, "DnsServiceFreeInstance");
|
||||
_DnsServiceDeRegister = (_DnsServiceDeRegister_fn)GetProcAddress(handle, "DnsServiceDeRegister");
|
||||
_DnsServiceRegister = (_DnsServiceRegister_fn)GetProcAddress(handle, "DnsServiceRegister");
|
||||
|
||||
if(!(_DnsServiceFreeInstance && _DnsServiceDeRegister && _DnsServiceRegister)) {
|
||||
BOOST_LOG(error) << "mDNS service not available in dnsapi.dll"sv;
|
||||
return -1;
|
||||
}
|
||||
|
||||
fg.disable();
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::unique_ptr<::platf::deinit_t> start() {
|
||||
HMODULE handle = LoadLibrary("dnsapi.dll");
|
||||
|
||||
if(!handle || load_funcs(handle)) {
|
||||
BOOST_LOG(error) << "Couldn't load dnsapi.dll, You'll need to add PC manually from Moonlight"sv;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if(service(true)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
BOOST_LOG(info) << "Registered Sunshine Gamestream service"sv;
|
||||
|
||||
return std::make_unique<deinit_t>();
|
||||
}
|
||||
} // namespace platf::publish
|
||||
1
sunshine/platform/windows/windows.rs.in
Normal file
1
sunshine/platform/windows/windows.rs.in
Normal file
@@ -0,0 +1 @@
|
||||
SuperDuperAmazing ICON DISCARDABLE "@SUNSHINE_ICON_PATH@"
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
#include <boost/property_tree/json_parser.hpp>
|
||||
#include <boost/property_tree/ptree.hpp>
|
||||
#include <boost/filesystem.hpp>
|
||||
|
||||
#include "main.h"
|
||||
#include "utility.h"
|
||||
@@ -94,7 +95,7 @@ int proc_t::execute(int app_id) {
|
||||
|
||||
for(auto &cmd : proc.detached) {
|
||||
BOOST_LOG(info) << "Spawning ["sv << cmd << ']';
|
||||
if(proc.output.empty()) {
|
||||
if(proc.output.empty() || proc.output == "null"sv) {
|
||||
bp::spawn(cmd, _env, bp::std_out > bp::null, bp::std_err > bp::null, ec);
|
||||
}
|
||||
else {
|
||||
@@ -106,15 +107,20 @@ int proc_t::execute(int app_id) {
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_LOG(info) << "Executing: ["sv << proc.cmd << ']';
|
||||
if(proc.cmd.empty()) {
|
||||
BOOST_LOG(debug) << "Executing [Desktop]"sv;
|
||||
placebo = true;
|
||||
}
|
||||
else if(proc.output.empty() || proc.output == "null"sv) {
|
||||
_process = bp::child(_process_handle, proc.cmd, _env, bp::std_out > bp::null, bp::std_err > bp::null, ec);
|
||||
}
|
||||
else {
|
||||
_process = bp::child(_process_handle, proc.cmd, _env, bp::std_out > _pipe.get(), bp::std_err > _pipe.get(), ec);
|
||||
} else {
|
||||
boost::filesystem::path working_dir = proc.working_dir.empty() ?
|
||||
boost::filesystem::path(proc.cmd).parent_path() : boost::filesystem::path(proc.working_dir);
|
||||
if(proc.output.empty() || proc.output == "null"sv) {
|
||||
BOOST_LOG(info) << "Executing: ["sv << proc.cmd << ']';
|
||||
_process = bp::child(_process_handle, proc.cmd, _env, bp::start_dir(working_dir), bp::std_out > bp::null, bp::std_err > bp::null, ec);
|
||||
}
|
||||
else {
|
||||
BOOST_LOG(info) << "Executing: ["sv << proc.cmd << ']';
|
||||
_process = bp::child(_process_handle, proc.cmd, _env, bp::start_dir(working_dir), bp::std_out > _pipe.get(), bp::std_err > _pipe.get(), ec);
|
||||
}
|
||||
}
|
||||
|
||||
if(ec) {
|
||||
@@ -273,6 +279,7 @@ std::optional<proc::proc_t> parse(const std::string &file_name) {
|
||||
auto output = app_node.get_optional<std::string>("output"s);
|
||||
auto name = parse_env_val(this_env, app_node.get<std::string>("name"s));
|
||||
auto cmd = app_node.get_optional<std::string>("cmd"s);
|
||||
auto working_dir = app_node.get_optional<std::string>("working-dir"s);
|
||||
|
||||
std::vector<proc::cmd_t> prep_cmds;
|
||||
if(prep_nodes_opt) {
|
||||
@@ -310,6 +317,10 @@ std::optional<proc::proc_t> parse(const std::string &file_name) {
|
||||
ctx.cmd = parse_env_val(this_env, *cmd);
|
||||
}
|
||||
|
||||
if(working_dir) {
|
||||
ctx.working_dir = parse_env_val(this_env, *working_dir);
|
||||
}
|
||||
|
||||
ctx.name = std::move(name);
|
||||
ctx.prep_cmds = std::move(prep_cmds);
|
||||
ctx.detached = std::move(detached);
|
||||
@@ -332,6 +343,11 @@ void refresh(const std::string &file_name) {
|
||||
auto proc_opt = proc::parse(file_name);
|
||||
|
||||
if(proc_opt) {
|
||||
{
|
||||
proc::ctx_t ctx;
|
||||
ctx.name = "Desktop"s;
|
||||
proc_opt->get_apps().emplace(std::begin(proc_opt->get_apps()), std::move(ctx));
|
||||
}
|
||||
proc = std::move(*proc_opt);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ struct cmd_t {
|
||||
* 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
|
||||
@@ -52,12 +53,13 @@ struct ctx_t {
|
||||
|
||||
std::string name;
|
||||
std::string cmd;
|
||||
std::string working_dir;
|
||||
std::string output;
|
||||
};
|
||||
|
||||
class proc_t {
|
||||
public:
|
||||
KITTY_DEFAULT_CONSTR_THROW(proc_t)
|
||||
KITTY_DEFAULT_CONSTR_MOVE_THROW(proc_t)
|
||||
|
||||
proc_t(
|
||||
boost::process::environment &&env,
|
||||
|
||||
@@ -2,10 +2,18 @@
|
||||
// Created by loki on 2/2/20.
|
||||
//
|
||||
|
||||
#define BOOST_BIND_GLOBAL_PLACEHOLDERS
|
||||
|
||||
extern "C" {
|
||||
#include <moonlight-common-c/src/Rtsp.h>
|
||||
}
|
||||
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
|
||||
#include <boost/asio.hpp>
|
||||
#include <boost/bind.hpp>
|
||||
|
||||
#include "config.h"
|
||||
#include "input.h"
|
||||
#include "main.h"
|
||||
@@ -22,17 +30,6 @@ using asio::ip::udp;
|
||||
using namespace std::literals;
|
||||
|
||||
namespace stream {
|
||||
|
||||
//FIXME: Quick and dirty workaround for bug in MinGW 9.3 causing a linker error when using std::to_string
|
||||
template<class T>
|
||||
std::string to_string(T &&t) {
|
||||
std::stringstream ss;
|
||||
ss << std::forward<T>(t);
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
constexpr auto RTSP_SETUP_PORT = 48010;
|
||||
|
||||
void free_msg(PRTSP_MESSAGE msg) {
|
||||
freeMessage(msg);
|
||||
|
||||
@@ -42,20 +39,180 @@ void free_msg(PRTSP_MESSAGE msg) {
|
||||
class rtsp_server_t;
|
||||
|
||||
using msg_t = util::safe_ptr<RTSP_MESSAGE, free_msg>;
|
||||
using cmd_func_t = std::function<void(rtsp_server_t *, net::peer_t, msg_t &&)>;
|
||||
using cmd_func_t = std::function<void(rtsp_server_t *server, tcp::socket &, msg_t &&)>;
|
||||
|
||||
void print_msg(PRTSP_MESSAGE msg);
|
||||
void cmd_not_found(net::host_t::pointer host, net::peer_t peer, msg_t &&req);
|
||||
void cmd_not_found(tcp::socket &sock, msg_t &&req);
|
||||
void respond(tcp::socket &sock, POPTION_ITEM options, int statuscode, const char *status_msg, int seqn, const std::string_view &payload);
|
||||
|
||||
class socket_t : public std::enable_shared_from_this<socket_t> {
|
||||
public:
|
||||
socket_t(boost::asio::io_service &ios, std::function<void(tcp::socket &sock, msg_t &&)> &&handle_data_fn)
|
||||
: handle_data_fn { std::move(handle_data_fn) }, sock { ios } {}
|
||||
|
||||
void read() {
|
||||
if(begin == std::end(msg_buf)) {
|
||||
BOOST_LOG(error) << "RTSP: read(): Exceeded maximum rtsp packet size: "sv << msg_buf.size();
|
||||
|
||||
respond(sock, nullptr, 400, "BAD REQUEST", 0, {});
|
||||
|
||||
sock.close();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
sock.async_read_some(
|
||||
boost::asio::buffer(begin, (std::size_t)(std::end(msg_buf) - begin)),
|
||||
boost::bind(
|
||||
&socket_t::handle_read, shared_from_this(),
|
||||
boost::asio::placeholders::error,
|
||||
boost::asio::placeholders::bytes_transferred));
|
||||
}
|
||||
|
||||
void read_payload() {
|
||||
if(begin == std::end(msg_buf)) {
|
||||
BOOST_LOG(error) << "RTSP: read_payload(): Exceeded maximum rtsp packet size: "sv << msg_buf.size();
|
||||
|
||||
respond(sock, nullptr, 400, "BAD REQUEST", 0, {});
|
||||
|
||||
sock.close();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
sock.async_read_some(
|
||||
boost::asio::buffer(begin, (std::size_t)(std::end(msg_buf) - begin)),
|
||||
boost::bind(
|
||||
&socket_t::handle_payload, shared_from_this(),
|
||||
boost::asio::placeholders::error,
|
||||
boost::asio::placeholders::bytes_transferred));
|
||||
}
|
||||
|
||||
static void handle_payload(std::shared_ptr<socket_t> &socket, const boost::system::error_code &ec, std::size_t bytes) {
|
||||
BOOST_LOG(debug) << "handle_payload(): Handle read of size: "sv << bytes << " bytes"sv;
|
||||
|
||||
auto sock_close = util::fail_guard([&socket]() {
|
||||
boost::system::error_code ec;
|
||||
socket->sock.close(ec);
|
||||
|
||||
if(ec) {
|
||||
BOOST_LOG(error) << "RTSP: handle_payload(): Couldn't close tcp socket: "sv << ec.message();
|
||||
}
|
||||
});
|
||||
|
||||
if(ec) {
|
||||
BOOST_LOG(error) << "RTSP: handle_payload(): Couldn't read from tcp socket: "sv << ec.message();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
auto end = socket->begin + bytes;
|
||||
msg_t req { new msg_t::element_type {} };
|
||||
if(auto status = parseRtspMessage(req.get(), socket->msg_buf.data(), (std::size_t)(end - socket->msg_buf.data()))) {
|
||||
BOOST_LOG(error) << "Malformed RTSP message: ["sv << status << ']';
|
||||
|
||||
respond(socket->sock, nullptr, 400, "BAD REQUEST", req->sequenceNumber, {});
|
||||
return;
|
||||
}
|
||||
|
||||
sock_close.disable();
|
||||
|
||||
auto fg = util::fail_guard([&socket]() {
|
||||
socket->read_payload();
|
||||
});
|
||||
|
||||
auto content_lenght = 0;
|
||||
for(auto option = req->options; option != nullptr; option = option->next) {
|
||||
if("Content-length"sv == option->option) {
|
||||
BOOST_LOG(debug) << "Found Content-Length: "sv << option->content << " bytes"sv;
|
||||
|
||||
// If content_length > bytes read, then we need to store current data read,
|
||||
// to be appended by the next read.
|
||||
std::string_view content { option->content };
|
||||
auto begin = std::find_if(std::begin(content), std::end(content), [](auto ch) { return (bool)std::isdigit(ch); });
|
||||
|
||||
content_lenght = util::from_chars(begin, std::end(content));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(end - socket->crlf >= content_lenght) {
|
||||
if(end - socket->crlf > content_lenght) {
|
||||
BOOST_LOG(warning) << "(end - socket->crlf) > content_lenght -- "sv << (std::size_t)(end - socket->crlf) << " > "sv << content_lenght;
|
||||
}
|
||||
|
||||
fg.disable();
|
||||
print_msg(req.get());
|
||||
|
||||
socket->handle_data(std::move(req));
|
||||
}
|
||||
|
||||
socket->begin = end;
|
||||
}
|
||||
|
||||
static void handle_read(std::shared_ptr<socket_t> &socket, const boost::system::error_code &ec, std::size_t bytes) {
|
||||
BOOST_LOG(debug) << "handle_read(): Handle read of size: "sv << bytes << " bytes"sv;
|
||||
|
||||
if(ec) {
|
||||
BOOST_LOG(error) << "RTSP: handle_read(): Couldn't read from tcp socket: "sv << ec.message();
|
||||
|
||||
boost::system::error_code ec;
|
||||
socket->sock.close(ec);
|
||||
|
||||
if(ec) {
|
||||
BOOST_LOG(error) << "RTSP: handle_read(): Couldn't close tcp socket: "sv << ec.message();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
auto fg = util::fail_guard([&socket]() {
|
||||
socket->read();
|
||||
});
|
||||
|
||||
auto begin = std::max(socket->begin - 4, socket->begin);
|
||||
auto buf_size = bytes + (begin - socket->begin);
|
||||
auto end = begin + buf_size;
|
||||
|
||||
constexpr auto needle = "\r\n\r\n"sv;
|
||||
|
||||
auto it = std::search(begin, begin + buf_size, std::begin(needle), std::end(needle));
|
||||
if(it == end) {
|
||||
socket->begin = end;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Emulate read completion for payload data
|
||||
socket->begin = it + needle.size();
|
||||
socket->crlf = socket->begin;
|
||||
buf_size = end - socket->begin;
|
||||
|
||||
fg.disable();
|
||||
handle_payload(socket, ec, buf_size);
|
||||
}
|
||||
|
||||
void handle_data(msg_t &&req) {
|
||||
handle_data_fn(sock, std::move(req));
|
||||
}
|
||||
|
||||
std::function<void(tcp::socket &sock, msg_t &&)> handle_data_fn;
|
||||
|
||||
tcp::socket sock;
|
||||
|
||||
std::array<char, 2048> msg_buf;
|
||||
|
||||
char *crlf;
|
||||
char *begin = msg_buf.data();
|
||||
};
|
||||
|
||||
class rtsp_server_t {
|
||||
public:
|
||||
~rtsp_server_t() {
|
||||
if(_host) {
|
||||
clear();
|
||||
}
|
||||
clear();
|
||||
}
|
||||
|
||||
int bind(std::uint16_t port) {
|
||||
int bind(std::uint16_t port, boost::system::error_code &ec) {
|
||||
{
|
||||
auto lg = _session_slots.lock();
|
||||
|
||||
@@ -63,13 +220,85 @@ public:
|
||||
_slot_count = config::stream.channels;
|
||||
}
|
||||
|
||||
_host = net::host_create(_addr, 1, port);
|
||||
acceptor.open(tcp::v4(), ec);
|
||||
if(ec) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return !(bool)_host;
|
||||
acceptor.set_option(boost::asio::socket_base::reuse_address { true });
|
||||
|
||||
acceptor.bind(tcp::endpoint(tcp::v4(), port), ec);
|
||||
if(ec) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
acceptor.listen(4096, ec);
|
||||
if(ec) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
next_socket = std::make_shared<socket_t>(ios, [this](tcp::socket &sock, msg_t &&msg) {
|
||||
handle_msg(sock, std::move(msg));
|
||||
});
|
||||
|
||||
acceptor.async_accept(next_socket->sock, [this](const auto &ec) {
|
||||
handle_accept(ec);
|
||||
});
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
template<class T, class X>
|
||||
void iterate(std::chrono::duration<T, X> timeout) {
|
||||
ios.run_one_for(timeout);
|
||||
}
|
||||
|
||||
void handle_msg(tcp::socket &sock, msg_t &&req) {
|
||||
auto func = _map_cmd_cb.find(req->message.request.command);
|
||||
if(func != std::end(_map_cmd_cb)) {
|
||||
func->second(this, sock, std::move(req));
|
||||
}
|
||||
else {
|
||||
cmd_not_found(sock, std::move(req));
|
||||
}
|
||||
|
||||
sock.shutdown(boost::asio::socket_base::shutdown_type::shutdown_both);
|
||||
}
|
||||
|
||||
void handle_accept(const boost::system::error_code &ec) {
|
||||
if(ec) {
|
||||
BOOST_LOG(error) << "Couldn't accept incoming connections: "sv << ec.message();
|
||||
|
||||
//Stop server
|
||||
clear();
|
||||
return;
|
||||
}
|
||||
|
||||
auto socket = std::move(next_socket);
|
||||
socket->read();
|
||||
|
||||
next_socket = std::make_shared<socket_t>(ios, [this](tcp::socket &sock, msg_t &&msg) {
|
||||
handle_msg(sock, std::move(msg));
|
||||
});
|
||||
|
||||
acceptor.async_accept(next_socket->sock, [this](const auto &ec) {
|
||||
handle_accept(ec);
|
||||
});
|
||||
}
|
||||
|
||||
void map(const std::string_view &type, cmd_func_t cb) {
|
||||
_map_cmd_cb.emplace(type, std::move(cb));
|
||||
}
|
||||
|
||||
void session_raise(launch_session_t launch_session) {
|
||||
//FIXME: If client abandons us at this stage, _slot_count won't be raised again.
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
|
||||
// If a launch event is still pending, don't overwrite it.
|
||||
if(raised_timeout > now && launch_event.peek()) {
|
||||
return;
|
||||
}
|
||||
raised_timeout = now + 10s;
|
||||
|
||||
--_slot_count;
|
||||
launch_event.raise(launch_session);
|
||||
}
|
||||
@@ -78,75 +307,17 @@ public:
|
||||
return config::stream.channels - _slot_count;
|
||||
}
|
||||
|
||||
template<class T, class X>
|
||||
void iterate(std::chrono::duration<T, X> timeout) {
|
||||
ENetEvent event;
|
||||
auto res = enet_host_service(_host.get(), &event, std::chrono::floor<std::chrono::milliseconds>(timeout).count());
|
||||
|
||||
if(res > 0) {
|
||||
switch(event.type) {
|
||||
case ENET_EVENT_TYPE_RECEIVE: {
|
||||
net::packet_t packet { event.packet };
|
||||
net::peer_t peer { event.peer };
|
||||
|
||||
msg_t req { new msg_t::element_type };
|
||||
|
||||
//TODO: compare addresses of the peers
|
||||
if(_queue_packet.second == nullptr) {
|
||||
parseRtspMessage(req.get(), (char *)packet->data, packet->dataLength);
|
||||
for(auto option = req->options; option != nullptr; option = option->next) {
|
||||
if("Content-length"sv == option->option) {
|
||||
_queue_packet = std::make_pair(peer, std::move(packet));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
std::vector<char> full_payload;
|
||||
|
||||
auto old_msg = std::move(_queue_packet);
|
||||
auto &old_packet = old_msg.second;
|
||||
|
||||
std::string_view new_payload { (char *)packet->data, packet->dataLength };
|
||||
std::string_view old_payload { (char *)old_packet->data, old_packet->dataLength };
|
||||
full_payload.resize(new_payload.size() + old_payload.size());
|
||||
|
||||
std::copy(std::begin(old_payload), std::end(old_payload), std::begin(full_payload));
|
||||
std::copy(std::begin(new_payload), std::end(new_payload), std::begin(full_payload) + old_payload.size());
|
||||
|
||||
parseRtspMessage(req.get(), full_payload.data(), full_payload.size());
|
||||
}
|
||||
|
||||
print_msg(req.get());
|
||||
|
||||
msg_t resp;
|
||||
auto func = _map_cmd_cb.find(req->message.request.command);
|
||||
if(func != std::end(_map_cmd_cb)) {
|
||||
func->second(this, peer, std::move(req));
|
||||
}
|
||||
else {
|
||||
cmd_not_found(host(), peer, std::move(req));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
case ENET_EVENT_TYPE_CONNECT:
|
||||
BOOST_LOG(info) << "CLIENT CONNECTED TO RTSP"sv;
|
||||
break;
|
||||
case ENET_EVENT_TYPE_DISCONNECT:
|
||||
BOOST_LOG(info) << "CLIENT DISCONNECTED FROM RTSP"sv;
|
||||
break;
|
||||
case ENET_EVENT_TYPE_NONE:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void map(const std::string_view &type, cmd_func_t cb) {
|
||||
_map_cmd_cb.emplace(type, std::move(cb));
|
||||
}
|
||||
safe::event_t<launch_session_t> launch_event;
|
||||
|
||||
void clear(bool all = true) {
|
||||
// if a launch event timed out --> Remove it.
|
||||
if(raised_timeout < std::chrono::steady_clock::now()) {
|
||||
auto discarded = launch_event.pop(0s);
|
||||
if(discarded) {
|
||||
++_slot_count;
|
||||
}
|
||||
}
|
||||
|
||||
auto lg = _session_slots.lock();
|
||||
|
||||
for(auto &slot : *_session_slots) {
|
||||
@@ -160,10 +331,8 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
if(all) {
|
||||
std::for_each(_host->peers, _host->peers + _host->peerCount, [](auto &peer) {
|
||||
enet_peer_disconnect_now(&peer, 0);
|
||||
});
|
||||
if(all && !ios.stopped()) {
|
||||
ios.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,27 +357,21 @@ public:
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
net::host_t::pointer host() const {
|
||||
return _host.get();
|
||||
}
|
||||
|
||||
safe::event_t<launch_session_t> launch_event;
|
||||
|
||||
private:
|
||||
// named _queue_packet because I want to make it an actual queue
|
||||
// It's like this for convenience sake
|
||||
std::pair<net::peer_t, net::packet_t> _queue_packet;
|
||||
|
||||
std::unordered_map<std::string_view, cmd_func_t> _map_cmd_cb;
|
||||
|
||||
util::sync_t<std::vector<std::shared_ptr<session_t>>> _session_slots;
|
||||
|
||||
std::chrono::steady_clock::time_point raised_timeout;
|
||||
int _slot_count;
|
||||
ENetAddress _addr;
|
||||
net::host_t _host;
|
||||
|
||||
boost::asio::io_service ios;
|
||||
tcp::acceptor acceptor { ios };
|
||||
|
||||
std::shared_ptr<socket_t> next_socket;
|
||||
};
|
||||
|
||||
rtsp_server_t server;
|
||||
rtsp_server_t server {};
|
||||
|
||||
void launch_session_raise(launch_session_t launch_session) {
|
||||
server.session_raise(launch_session);
|
||||
@@ -221,9 +384,26 @@ int session_count() {
|
||||
return server.session_count();
|
||||
}
|
||||
|
||||
void respond(net::host_t::pointer host, net::peer_t peer, msg_t &resp) {
|
||||
int send(tcp::socket &sock, const std::string_view &sv) {
|
||||
std::size_t bytes_send = 0;
|
||||
|
||||
while(bytes_send != sv.size()) {
|
||||
boost::system::error_code ec;
|
||||
bytes_send += sock.send(boost::asio::buffer(sv.substr(bytes_send)), 0, ec);
|
||||
|
||||
if(ec) {
|
||||
BOOST_LOG(error) << "RTSP: Couldn't send data over tcp socket: "sv << ec.message();
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void respond(tcp::socket &sock, msg_t &resp) {
|
||||
auto payload = std::make_pair(resp->payload, resp->payloadLength);
|
||||
|
||||
// Restore response message for proper destruction
|
||||
auto lg = util::fail_guard([&]() {
|
||||
resp->payload = payload.first;
|
||||
resp->payloadLength = payload.second;
|
||||
@@ -241,57 +421,44 @@ void respond(net::host_t::pointer host, net::peer_t peer, msg_t &resp) {
|
||||
<< "---End Response---"sv << std::endl;
|
||||
|
||||
std::string_view tmp_resp { raw_resp.get(), (size_t)serialized_len };
|
||||
{
|
||||
auto packet = enet_packet_create(tmp_resp.data(), tmp_resp.size(), ENET_PACKET_FLAG_RELIABLE);
|
||||
if(enet_peer_send(peer, 0, packet)) {
|
||||
enet_packet_destroy(packet);
|
||||
return;
|
||||
}
|
||||
|
||||
enet_host_flush(host);
|
||||
if(send(sock, tmp_resp)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(payload.second > 0) {
|
||||
auto packet = enet_packet_create(payload.first, payload.second, ENET_PACKET_FLAG_RELIABLE);
|
||||
if(enet_peer_send(peer, 0, packet)) {
|
||||
enet_packet_destroy(packet);
|
||||
return;
|
||||
}
|
||||
|
||||
enet_host_flush(host);
|
||||
}
|
||||
send(sock, std::string_view { payload.first, (std::size_t)payload.second });
|
||||
}
|
||||
|
||||
void respond(net::host_t::pointer host, net::peer_t peer, POPTION_ITEM options, int statuscode, const char *status_msg, int seqn, const std::string_view &payload) {
|
||||
void respond(tcp::socket &sock, POPTION_ITEM options, int statuscode, const char *status_msg, int seqn, const std::string_view &payload) {
|
||||
msg_t resp { new msg_t::element_type };
|
||||
createRtspResponse(resp.get(), nullptr, 0, const_cast<char *>("RTSP/1.0"), statuscode, const_cast<char *>(status_msg), seqn, options, const_cast<char *>(payload.data()), (int)payload.size());
|
||||
|
||||
respond(host, peer, resp);
|
||||
respond(sock, resp);
|
||||
}
|
||||
|
||||
void cmd_not_found(net::host_t::pointer host, net::peer_t peer, msg_t &&req) {
|
||||
respond(host, peer, nullptr, 404, "NOT FOUND", req->sequenceNumber, {});
|
||||
void cmd_not_found(tcp::socket &sock, msg_t &&req) {
|
||||
respond(sock, nullptr, 404, "NOT FOUND", req->sequenceNumber, {});
|
||||
}
|
||||
|
||||
void cmd_option(rtsp_server_t *server, net::peer_t peer, msg_t &&req) {
|
||||
void cmd_option(rtsp_server_t *server, tcp::socket &sock, msg_t &&req) {
|
||||
OPTION_ITEM option {};
|
||||
|
||||
// I know these string literals will not be modified
|
||||
option.option = const_cast<char *>("CSeq");
|
||||
|
||||
auto seqn_str = to_string(req->sequenceNumber);
|
||||
auto seqn_str = std::to_string(req->sequenceNumber);
|
||||
option.content = const_cast<char *>(seqn_str.c_str());
|
||||
|
||||
respond(server->host(), peer, &option, 200, "OK", req->sequenceNumber, {});
|
||||
respond(sock, &option, 200, "OK", req->sequenceNumber, {});
|
||||
}
|
||||
|
||||
void cmd_describe(rtsp_server_t *server, net::peer_t peer, msg_t &&req) {
|
||||
void cmd_describe(rtsp_server_t *server, tcp::socket &sock, msg_t &&req) {
|
||||
OPTION_ITEM option {};
|
||||
|
||||
// I know these string literals will not be modified
|
||||
option.option = const_cast<char *>("CSeq");
|
||||
|
||||
auto seqn_str = to_string(req->sequenceNumber);
|
||||
auto seqn_str = std::to_string(req->sequenceNumber);
|
||||
option.content = const_cast<char *>(seqn_str.c_str());
|
||||
|
||||
std::stringstream ss;
|
||||
@@ -326,18 +493,19 @@ void cmd_describe(rtsp_server_t *server, net::peer_t peer, msg_t &&req) {
|
||||
ss << std::endl;
|
||||
}
|
||||
|
||||
respond(server->host(), peer, &option, 200, "OK", req->sequenceNumber, ss.str());
|
||||
respond(sock, &option, 200, "OK", req->sequenceNumber, ss.str());
|
||||
}
|
||||
|
||||
void cmd_setup(rtsp_server_t *server, net::peer_t peer, msg_t &&req) {
|
||||
OPTION_ITEM options[2] {};
|
||||
void cmd_setup(rtsp_server_t *server, tcp::socket &sock, msg_t &&req) {
|
||||
OPTION_ITEM options[3] {};
|
||||
|
||||
auto &seqn = options[0];
|
||||
auto &session_option = options[1];
|
||||
auto &port_option = options[2];
|
||||
|
||||
seqn.option = const_cast<char *>("CSeq");
|
||||
|
||||
auto seqn_str = to_string(req->sequenceNumber);
|
||||
auto seqn_str = std::to_string(req->sequenceNumber);
|
||||
seqn.content = const_cast<char *>(seqn_str.c_str());
|
||||
|
||||
std::string_view target { req->message.request.target };
|
||||
@@ -345,34 +513,52 @@ void cmd_setup(rtsp_server_t *server, net::peer_t peer, msg_t &&req) {
|
||||
auto end = std::find(begin, std::end(target), '/');
|
||||
std::string_view type { begin, (size_t)std::distance(begin, end) };
|
||||
|
||||
std::uint16_t port;
|
||||
if(type == "audio"sv) {
|
||||
seqn.next = &session_option;
|
||||
|
||||
session_option.option = const_cast<char *>("Session");
|
||||
session_option.content = const_cast<char *>("DEADBEEFCAFE;timeout = 90");
|
||||
port = map_port(stream::AUDIO_STREAM_PORT);
|
||||
}
|
||||
else if(type != "video"sv && type != "control"sv) {
|
||||
cmd_not_found(server->host(), peer, std::move(req));
|
||||
else if(type == "video"sv) {
|
||||
port = map_port(stream::VIDEO_STREAM_PORT);
|
||||
}
|
||||
else if(type == "control"sv) {
|
||||
port = map_port(stream::CONTROL_PORT);
|
||||
}
|
||||
else {
|
||||
cmd_not_found(sock, std::move(req));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
respond(server->host(), peer, &seqn, 200, "OK", req->sequenceNumber, {});
|
||||
seqn.next = &session_option;
|
||||
|
||||
session_option.option = const_cast<char *>("Session");
|
||||
session_option.content = const_cast<char *>("DEADBEEFCAFE;timeout = 90");
|
||||
|
||||
session_option.next = &port_option;
|
||||
|
||||
// Moonlight merely requires 'server_port=<port>'
|
||||
auto port_value = "server_port=" + std::to_string(port);
|
||||
|
||||
port_option.option = const_cast<char *>("Transport");
|
||||
port_option.content = port_value.data();
|
||||
|
||||
|
||||
respond(sock, &seqn, 200, "OK", req->sequenceNumber, {});
|
||||
}
|
||||
|
||||
void cmd_announce(rtsp_server_t *server, net::peer_t peer, msg_t &&req) {
|
||||
void cmd_announce(rtsp_server_t *server, tcp::socket &sock, msg_t &&req) {
|
||||
OPTION_ITEM option {};
|
||||
|
||||
// I know these string literals will not be modified
|
||||
option.option = const_cast<char *>("CSeq");
|
||||
|
||||
auto seqn_str = to_string(req->sequenceNumber);
|
||||
auto seqn_str = std::to_string(req->sequenceNumber);
|
||||
option.content = const_cast<char *>(seqn_str.c_str());
|
||||
|
||||
if(!server->launch_event.peek()) {
|
||||
// /launch has not been used
|
||||
|
||||
respond(server->host(), peer, &option, 503, "Service Unavailable", req->sequenceNumber, {});
|
||||
respond(sock, &option, 503, "Service Unavailable", req->sequenceNumber, {});
|
||||
return;
|
||||
}
|
||||
auto launch_session { server->launch_event.pop() };
|
||||
@@ -424,6 +610,9 @@ void cmd_announce(rtsp_server_t *server, net::peer_t peer, msg_t &&req) {
|
||||
args.try_emplace("x-nv-vqos[0].bitStreamFormat"sv, "0"sv);
|
||||
args.try_emplace("x-nv-video[0].dynamicRangeMode"sv, "0"sv);
|
||||
args.try_emplace("x-nv-aqos.packetDuration"sv, "5"sv);
|
||||
args.try_emplace("x-nv-general.useReliableUdp"sv, "1"sv);
|
||||
args.try_emplace("x-nv-vqos[0].fec.minRequiredFecPackets"sv, "0"sv);
|
||||
args.try_emplace("x-nv-general.featureFlags"sv, "135"sv);
|
||||
|
||||
config_t config;
|
||||
|
||||
@@ -436,7 +625,10 @@ void cmd_announce(rtsp_server_t *server, net::peer_t peer, msg_t &&req) {
|
||||
config.audio.flags[audio::config_t::HIGH_QUALITY] =
|
||||
util::from_view(args.at("x-nv-audio.surround.AudioQuality"sv));
|
||||
|
||||
config.packetsize = util::from_view(args.at("x-nv-video[0].packetSize"sv));
|
||||
config.controlProtocolType = util::from_view(args.at("x-nv-general.useReliableUdp"sv));
|
||||
config.packetsize = util::from_view(args.at("x-nv-video[0].packetSize"sv));
|
||||
config.minRequiredFecPackets = util::from_view(args.at("x-nv-vqos[0].fec.minRequiredFecPackets"sv));
|
||||
config.featureFlags = util::from_view(args.at("x-nv-general.featureFlags"sv));
|
||||
|
||||
config.monitor.height = util::from_view(args.at("x-nv-video[0].clientViewportHt"sv));
|
||||
config.monitor.width = util::from_view(args.at("x-nv-video[0].clientViewportWd"sv));
|
||||
@@ -450,14 +642,14 @@ void cmd_announce(rtsp_server_t *server, net::peer_t peer, msg_t &&req) {
|
||||
}
|
||||
catch(std::out_of_range &) {
|
||||
|
||||
respond(server->host(), peer, &option, 400, "BAD REQUEST", req->sequenceNumber, {});
|
||||
respond(sock, &option, 400, "BAD REQUEST", req->sequenceNumber, {});
|
||||
return;
|
||||
}
|
||||
|
||||
if(config.monitor.videoFormat != 0 && config::video.hevc_mode == 1) {
|
||||
BOOST_LOG(warning) << "HEVC is disabled, yet the client requested HEVC"sv;
|
||||
|
||||
respond(server->host(), peer, &option, 400, "BAD REQUEST", req->sequenceNumber, {});
|
||||
respond(sock, &option, 400, "BAD REQUEST", req->sequenceNumber, {});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -467,34 +659,37 @@ void cmd_announce(rtsp_server_t *server, net::peer_t peer, msg_t &&req) {
|
||||
if(!slot) {
|
||||
BOOST_LOG(info) << "Ran out of slots for client from ["sv << ']';
|
||||
|
||||
respond(server->host(), peer, &option, 503, "Service Unavailable", req->sequenceNumber, {});
|
||||
respond(sock, &option, 503, "Service Unavailable", req->sequenceNumber, {});
|
||||
return;
|
||||
}
|
||||
|
||||
if(session::start(*session, platf::from_sockaddr((sockaddr *)&peer->address.address))) {
|
||||
if(session::start(*session, sock.remote_endpoint().address().to_string())) {
|
||||
BOOST_LOG(error) << "Failed to start a streaming session"sv;
|
||||
|
||||
server->clear(slot);
|
||||
respond(server->host(), peer, &option, 500, "Internal Server Error", req->sequenceNumber, {});
|
||||
respond(sock, &option, 500, "Internal Server Error", req->sequenceNumber, {});
|
||||
return;
|
||||
}
|
||||
|
||||
respond(server->host(), peer, &option, 200, "OK", req->sequenceNumber, {});
|
||||
respond(sock, &option, 200, "OK", req->sequenceNumber, {});
|
||||
}
|
||||
|
||||
void cmd_play(rtsp_server_t *server, net::peer_t peer, msg_t &&req) {
|
||||
void cmd_play(rtsp_server_t *server, tcp::socket &sock, msg_t &&req) {
|
||||
OPTION_ITEM option {};
|
||||
|
||||
// I know these string literals will not be modified
|
||||
option.option = const_cast<char *>("CSeq");
|
||||
|
||||
auto seqn_str = to_string(req->sequenceNumber);
|
||||
auto seqn_str = std::to_string(req->sequenceNumber);
|
||||
option.content = const_cast<char *>(seqn_str.c_str());
|
||||
|
||||
respond(server->host(), peer, &option, 200, "OK", req->sequenceNumber, {});
|
||||
respond(sock, &option, 200, "OK", req->sequenceNumber, {});
|
||||
}
|
||||
|
||||
void rtpThread(std::shared_ptr<safe::signal_t> shutdown_event) {
|
||||
void rtpThread() {
|
||||
auto shutdown_event = mail::man->event<bool>(mail::shutdown);
|
||||
auto broadcast_shutdown_event = mail::man->event<bool>(mail::broadcast_shutdown);
|
||||
|
||||
server.map("OPTIONS"sv, &cmd_option);
|
||||
server.map("DESCRIBE"sv, &cmd_describe);
|
||||
server.map("SETUP"sv, &cmd_setup);
|
||||
@@ -502,8 +697,9 @@ void rtpThread(std::shared_ptr<safe::signal_t> shutdown_event) {
|
||||
|
||||
server.map("PLAY"sv, &cmd_play);
|
||||
|
||||
if(server.bind(RTSP_SETUP_PORT)) {
|
||||
BOOST_LOG(fatal) << "Couldn't bind RTSP server to port ["sv << RTSP_SETUP_PORT << "], likely another process already bound to the port"sv;
|
||||
boost::system::error_code ec;
|
||||
if(server.bind(map_port(RTSP_SETUP_PORT), ec)) {
|
||||
BOOST_LOG(fatal) << "Couldn't bind RTSP server to port ["sv << map_port(RTSP_SETUP_PORT) << "], " << ec.message();
|
||||
shutdown_event->raise(true);
|
||||
|
||||
return;
|
||||
@@ -512,7 +708,7 @@ void rtpThread(std::shared_ptr<safe::signal_t> shutdown_event) {
|
||||
while(!shutdown_event->peek()) {
|
||||
server.iterate(std::min(500ms, config::stream.ping_timeout));
|
||||
|
||||
if(broadcast_shutdown_event.peek()) {
|
||||
if(broadcast_shutdown_event->peek()) {
|
||||
server.clear();
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
#include "thread_safe.h"
|
||||
|
||||
namespace stream {
|
||||
constexpr auto RTSP_SETUP_PORT = 21;
|
||||
|
||||
struct launch_session_t {
|
||||
crypto::aes_t gcm_key;
|
||||
crypto::aes_t iv;
|
||||
@@ -21,7 +23,7 @@ struct launch_session_t {
|
||||
void launch_session_raise(launch_session_t launch_session);
|
||||
int session_count();
|
||||
|
||||
void rtpThread(std::shared_ptr<safe::signal_t> shutdown_event);
|
||||
void rtpThread();
|
||||
|
||||
} // namespace stream
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -12,11 +12,19 @@
|
||||
#include "video.h"
|
||||
|
||||
namespace stream {
|
||||
constexpr auto VIDEO_STREAM_PORT = 9;
|
||||
constexpr auto CONTROL_PORT = 10;
|
||||
constexpr auto AUDIO_STREAM_PORT = 11;
|
||||
|
||||
struct session_t;
|
||||
struct config_t {
|
||||
audio::config_t audio;
|
||||
video::config_t monitor;
|
||||
|
||||
int packetsize;
|
||||
int minRequiredFecPackets;
|
||||
int featureFlags;
|
||||
int controlProtocolType;
|
||||
|
||||
std::optional<int> gcmap;
|
||||
};
|
||||
@@ -35,8 +43,6 @@ void stop(session_t &session);
|
||||
void join(session_t &session);
|
||||
state_e state(session_t &session);
|
||||
} // namespace session
|
||||
|
||||
extern safe::signal_t broadcast_shutdown_event;
|
||||
} // namespace stream
|
||||
|
||||
#endif //SUNSHINE_STREAM_H
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user