Merge branch 'master' into web-ui-troubleshooting

This commit is contained in:
Elia Zammuto 2021-08-29 18:25:03 +02:00
commit c4b371ccc9
32 changed files with 14819 additions and 969 deletions

View File

@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.0)
project(Sunshine)
set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR})
set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake)
add_subdirectory(third-party/Simple-Web-Server)
@ -102,18 +102,45 @@ else()
add_compile_definitions(SUNSHINE_PLATFORM="linux")
list(APPEND SUNSHINE_DEFINITIONS APPS_JSON="apps_linux.json")
find_package(X11 REQUIRED)
find_package(FFmpeg REQUIRED)
option(SUNSHINE_ENABLE_DRM "Enable KMS grab if available" ON)
option(SUNSHINE_ENABLE_X11 "Enable X11 grab if available" ON)
set(PLATFORM_TARGET_FILES
if(${SUNSHINE_ENABLE_X11})
find_package(X11)
endif()
if(${SUNSHINE_ENABLE_DRM})
find_package(LIBDRM)
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(LIBDRM_FOUND)
add_compile_definitions(SUNSHINE_BUILD_DRM)
include_directories(${LIBDRM_INCLUDE_DIRS})
list(APPEND PLATFORM_LIBRARIES ${LIBDRM_LIBRARIES})
list(APPEND PLATFORM_TARGET_FILES sunshine/platform/linux/kmsgrab.cpp)
list(APPEND SUNSHINE_DEFINITIONS EGL_NO_X11=1)
endif()
if(NOT X11_FOUND AND NOT LIBDRM_FOUND)
message(FATAL "Couldn't find either x11 or libdrm")
endif()
list(APPEND PLATFORM_TARGET_FILES
sunshine/platform/linux/publish.cpp
sunshine/platform/linux/vaapi.h
sunshine/platform/linux/vaapi.cpp
sunshine/platform/linux/graphics.h
sunshine/platform/linux/graphics.cpp
sunshine/platform/linux/misc.h
sunshine/platform/linux/misc.cpp
sunshine/platform/linux/display.cpp
sunshine/platform/linux/audio.cpp
sunshine/platform/linux/input.cpp
sunshine/platform/linux/x11grab.h
third-party/glad/src/egl.c
third-party/glad/src/gl.c
third-party/glad/include/EGL/eglplatform.h
@ -121,22 +148,14 @@ else()
third-party/glad/include/glad/gl.h
third-party/glad/include/glad/egl.h)
set(PLATFORM_LIBRARIES
Xfixes
Xtst
xcb
xcb-shm
xcb-xfixes
Xrandr
${X11_LIBRARIES}
list(APPEND PLATFORM_LIBRARIES
dl
evdev
pulse
pulse-simple
)
set(PLATFORM_INCLUDE_DIRS
${X11_INCLUDE_DIR}
include_directories(
/usr/include/libevdev-1.0
third-party/glad/include)

View File

@ -14,21 +14,38 @@ Sunshine is a Gamestream host for Moonlight
## Linux
### Requirements:
Ubuntu 20.04:
Install the following
Install the following:
#### X11 Only
```
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 libxtst-dev libx11-dev libxrandr-dev libxfixes-dev libevdev-dev libxcb1-dev libxcb-shm0-dev libxcb-xfixes0-dev
```
#### X11 + KMS (Requires additional setup)
KMS allows Sunshine to grab the monitor with lower latency then through X11
```
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 libxtst-dev libx11-dev libxrandr-dev libxfixes-dev libevdev-dev libxcb1-dev libxcb-shm0-dev libxcb-xfixes0-dev libdrm-dev
```
### Compilation:
#### X11 Only
- `git clone https://github.com/loki-47-6F-64/sunshine.git --recurse-submodules`
- `cd sunshine && mkdir build && cd build`
- `cmake -DCMAKE_C_COMPILER=gcc-10 -DCMAKE_CXX_COMPILER=g++-10 -DSUNSHINE_ENABLE_DRM=OFF ..`
- `make -j ${nproc}`
#### X11 + KMS
- `git clone https://github.com/loki-47-6F-64/sunshine.git --recurse-submodules`
- `cd sunshine && mkdir build && cd build`
- `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:
@ -52,6 +69,11 @@ 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+ep sunshine`
### Trouleshooting:
- If you get "Could not create Sunshine Gamepad: Permission Denied", ensure you are part of the group "input":
- `groups $USER`

View File

@ -9,7 +9,7 @@ environment:
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
- 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 libdrm-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"
before_build:

View File

@ -150,6 +150,22 @@
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">

View File

@ -5,18 +5,9 @@
<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="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-wEmeIV1mKuiNpC+IOBjI7aAzPcEZeedi5yW5f2yOq55WWLwNGmvvx4Um1vskeMj0"
crossorigin="anonymous"
/>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0/dist/js/bootstrap.bundle.min.js"
integrity="sha384-p34f1UUtsS3wqzfto5wAAmdvj+osOnFyQFpp4Ua3gs/ZVWx6oOypYoCJhGGScy+8"
crossorigin="anonymous"
></script>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script>
<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>

View File

@ -5,18 +5,9 @@
<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="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-wEmeIV1mKuiNpC+IOBjI7aAzPcEZeedi5yW5f2yOq55WWLwNGmvvx4Um1vskeMj0"
crossorigin="anonymous"
/>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0/dist/js/bootstrap.bundle.min.js"
integrity="sha384-p34f1UUtsS3wqzfto5wAAmdvj+osOnFyQFpp4Ua3gs/ZVWx6oOypYoCJhGGScy+8"
crossorigin="anonymous"
></script>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script>
<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>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

11965
assets/web/third_party/vue.js vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,45 +1,57 @@
<main role="main" id="app" style="max-width: 600px; margin: 0 auto">
<div class="container-parent">
<div class="container py-3">
<h1 class="mb-0">Welcome to Sunshine!</h1>
<p class="mb-0 align-self-start">
Before Getting Started, write down below these credentials
</p>
</div>
</div>
<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" class="card p-4" style="width: 100%">
<form @submit.prevent="save">
<div class="mb-2">
<label for="" class="form-label">Username: </label>
<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="" class="form-label">Password: </label>
<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="" class="form-label">Password: </label>
<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 class="mb-2 btn btn-primary" style="margin: 1em auto">Login</button>
<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
@ -55,6 +67,7 @@
return {
error: null,
success: false,
loading: false,
passwordData: {
newUsername: "sunshine",
newPassword: "",
@ -65,10 +78,12 @@
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") {

21
cmake/FindLIBDRM.cmake Normal file
View 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)

View File

@ -37,8 +37,8 @@ Package: sunshine
Architecture: amd64
Maintainer: @loki
Priority: optional
Version: 0.9.1
Depends: libssl1.1, libavdevice58, libboost-thread1.67.0 | libboost-thread1.71.0, libboost-filesystem1.67.0 | libboost-filesystem1.71.0, libboost-log1.67.0 | libboost-log1.71.0, libpulse0, libopus0, libxcb-shm0, libxcb-xfixes0, libxtst6, libevdev2
Version: 0.10.2
Depends: libssl1.1, libavdevice58, libboost-thread1.67.0 | libboost-thread1.71.0, libboost-filesystem1.67.0 | libboost-filesystem1.71.0, libboost-log1.67.0 | libboost-log1.71.0, libpulse0, libopus0, libxcb-shm0, libxcb-xfixes0, libxtst6, libevdev2, libdrm2
Description: Gamestream host for Moonlight
EOF
@ -88,6 +88,13 @@ 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+ep /usr/bin/sunshine"
$path_to_setcap cap_sys_admin+ep /usr/bin/sunshine
fi
EOF
cat << 'EOF' > $RULES/85-sunshine-rules.rules

View File

@ -130,7 +130,7 @@ void capture(safe::mail_t mail, config_t config, void *channel_data) {
auto &control = ref->control;
if(!control) {
BOOST_LOG(error) << "Couldn't create audio control"sv;
shutdown_event->view();
return;
}
@ -223,21 +223,29 @@ 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;
}

View File

@ -221,6 +221,27 @@ void getTroubleshootingPage(resp_https_t response, req_https_t request) {
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;
@ -550,27 +571,30 @@ void start() {
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.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/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.config.reuse_address = true;
server.config.address = "0.0.0.0"s;
server.config.port = port_https;
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();

View File

@ -147,10 +147,6 @@ public:
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;
};

View File

@ -0,0 +1,707 @@
#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 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 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;
}
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;
bool fail() {
return eglGetError() != EGL_SUCCESS;
}
display_t make_display(gbm::gbm_t::pointer gbm) {
constexpr auto EGL_PLATFORM_GBM_MESA = 0x31D7;
display_t display = eglGetPlatformDisplay(EGL_PLATFORM_GBM_MESA, gbm, 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",
"EGL_KHR_image_pixmap"
};
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;
}
std::optional<rgb_t> import_source(display_t::pointer egl_display, const surface_descriptor_t &xrgb) {
EGLAttrib img_attr_planes[13] {
EGL_LINUX_DRM_FOURCC_EXT, DRM_FORMAT_XRGB8888,
EGL_WIDTH, xrgb.width,
EGL_HEIGHT, xrgb.height,
EGL_DMA_BUF_PLANE0_FD_EXT, xrgb.fd,
EGL_DMA_BUF_PLANE0_OFFSET_EXT, xrgb.offset,
EGL_DMA_BUF_PLANE0_PITCH_EXT, xrgb.pitch,
EGL_NONE
};
rgb_t rgb {
egl_display,
eglCreateImage(egl_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, img_attr_planes),
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.fd,
EGL_DMA_BUF_PLANE0_OFFSET_EXT, r8.offset,
EGL_DMA_BUF_PLANE0_PITCH_EXT, r8.pitch,
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.fd,
EGL_DMA_BUF_PLANE0_OFFSET_EXT, gr88.offset,
EGL_DMA_BUF_PLANE0_PITCH_EXT, gr88.pitch,
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);
}
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) {
gl::ctx.BindTexture(GL_TEXTURE_2D, tex[0]);
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(cursor_t &img, int offset_x, int offset_y, int framebuffer) {
gl::ctx.BindFramebuffer(GL_FRAMEBUFFER, framebuffer);
gl::ctx.ReadBuffer(GL_COLOR_ATTACHMENT0);
gl::ctx.BindTexture(GL_TEXTURE_2D, tex[0]);
gl::ctx.CopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, offset_x, offset_y, in_width, in_height);
if(img.data) {
gl::ctx.BindTexture(GL_TEXTURE_2D, tex[1]);
if(serial != img.serial) {
serial = img.serial;
gl::ctx.TexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, img.width, img.height);
gl::ctx.TexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, img.width, img.height, GL_BGRA, GL_UNSIGNED_BYTE, img.data);
}
gl::ctx.Enable(GL_BLEND);
GLenum attachment = GL_COLOR_ATTACHMENT0;
gl::ctx.BindFramebuffer(GL_FRAMEBUFFER, cursor_framebuffer[0]);
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.UseProgram(program[2].handle());
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);
}
int sws_t::convert(nv12_t &nv12) {
gl::ctx.BindTexture(GL_TEXTURE_2D, tex[0]);
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, nv12->buf[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);
}

View File

@ -0,0 +1,267 @@
#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(x) #x
#define gl_drain_errors_helper(x) gl::drain_errors("line " SUNSHINE_STRINGIFY(x))
#define gl_drain_errors gl_drain_errors_helper(__LINE__)
extern "C" int close(int __fd);
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);
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;
});
}
};
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 fd;
int width;
int height;
int offset;
int pitch;
};
display_t make_display(gbm::gbm_t::pointer gbm);
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;
};
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);
int convert(nv12_t &nv12);
void load_ram(platf::img_t &img);
void load_vram(cursor_t &img, int offset_x, int offset_y, int framebuffer);
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;
// 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;
// Store latest cursor for load_vram
std::uint64_t serial;
};
bool fail();
} // namespace egl
#endif

View File

@ -0,0 +1,745 @@
#include <drm_fourcc.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <xf86drm.h>
#include <xf86drmMode.h>
#include <filesystem>
#include "sunshine/main.h"
#include "sunshine/platform/common.h"
#include "sunshine/round_robin.h"
#include "sunshine/utility.h"
// Cursor rendering support through x11
#include "graphics.h"
#include "vaapi.h"
#include "x11grab.h"
using namespace std::literals;
namespace fs = std::filesystem;
namespace platf {
namespace kms {
using plane_res_t = util::safe_ptr<drmModePlaneRes, drmModeFreePlaneResources>;
using plane_t = util::safe_ptr<drmModePlane, drmModeFreePlane>;
using fb_t = util::safe_ptr<drmModeFB, drmModeFreeFB>;
using crtc_t = util::safe_ptr<drmModeCrtc, drmModeFreeCrtc>;
using obj_prop_t = util::safe_ptr<drmModeObjectProperties, drmModeFreeObjectProperties>;
using prop_t = util::safe_ptr<drmModePropertyRes, drmModeFreeProperty>;
static int env_width;
static int env_height;
std::string_view plane_type(std::uint64_t val) {
switch(val) {
case DRM_PLANE_TYPE_OVERLAY:
return "DRM_PLANE_TYPE_OVERLAY"sv;
case DRM_PLANE_TYPE_PRIMARY:
return "DRM_PLANE_TYPE_PRIMARY"sv;
case DRM_PLANE_TYPE_CURSOR:
return "DRM_PLANE_TYPE_CURSOR"sv;
}
return "UNKNOWN"sv;
}
class plane_it_t : public util::it_wrap_t<plane_t::element_type, plane_it_t> {
public:
plane_it_t(int fd, std::uint32_t *plane_p, std::uint32_t *end)
: fd { fd }, plane_p { plane_p }, end { end } {
inc();
}
plane_it_t(int fd, std::uint32_t *end)
: fd { fd }, plane_p { end }, end { end } {}
void inc() {
this->plane.reset();
for(; plane_p != end; ++plane_p) {
plane_t plane = drmModeGetPlane(fd, *plane_p);
if(!plane) {
BOOST_LOG(error) << "Couldn't get drm plane ["sv << (end - plane_p) << "]: "sv << strerror(errno);
continue;
}
// If this plane is unused
if(plane->fb_id) {
this->plane = util::make_shared<plane_t>(plane.release());
// One last increment
++plane_p;
break;
}
}
}
bool eq(const plane_it_t &other) const {
return plane_p == other.plane_p;
}
plane_t::pointer get() {
return plane.get();
}
int fd;
std::uint32_t *plane_p;
std::uint32_t *end;
util::shared_t<plane_t> plane;
};
class card_t {
public:
int init(const char *path) {
fd.el = open(path, O_RDWR);
if(fd.el < 0) {
BOOST_LOG(error) << "Couldn't open: "sv << path << ": "sv << strerror(errno);
return -1;
}
if(drmSetClientCap(fd.el, DRM_CLIENT_CAP_UNIVERSAL_PLANES, 1)) {
BOOST_LOG(error) << "Couldn't expose some/all drm planes for card: "sv << path;
return -1;
}
if(drmSetClientCap(fd.el, DRM_CLIENT_CAP_ATOMIC, 1)) {
BOOST_LOG(warning) << "Couldn't expose some properties for card: "sv << path;
}
plane_res.reset(drmModeGetPlaneResources(fd.el));
if(!plane_res) {
BOOST_LOG(error) << "Couldn't get drm plane resources"sv;
return -1;
}
return 0;
}
fb_t fb(plane_t::pointer plane) {
return drmModeGetFB(fd.el, plane->fb_id);
}
crtc_t crtc(std::uint32_t id) {
return drmModeGetCrtc(fd.el, id);
}
file_t handleFD(std::uint32_t handle) {
file_t fb_fd;
auto status = drmPrimeHandleToFD(fd.el, handle, 0 /* flags */, &fb_fd.el);
if(status) {
return {};
}
return fb_fd;
}
std::vector<std::pair<prop_t, std::uint64_t>> props(std::uint32_t id, std::uint32_t type) {
obj_prop_t obj_prop = drmModeObjectGetProperties(fd.el, id, type);
std::vector<std::pair<prop_t, std::uint64_t>> props;
props.reserve(obj_prop->count_props);
for(auto x = 0; x < obj_prop->count_props; ++x) {
props.emplace_back(drmModeGetProperty(fd.el, obj_prop->props[x]), obj_prop->prop_values[x]);
}
return props;
}
std::vector<std::pair<prop_t, std::uint64_t>> plane_props(std::uint32_t id) {
return props(id, DRM_MODE_OBJECT_PLANE);
}
std::vector<std::pair<prop_t, std::uint64_t>> crtc_props(std::uint32_t id) {
return props(id, DRM_MODE_OBJECT_CRTC);
}
std::vector<std::pair<prop_t, std::uint64_t>> connector_props(std::uint32_t id) {
return props(id, DRM_MODE_OBJECT_CONNECTOR);
}
plane_t
operator[](std::uint32_t index) {
return drmModeGetPlane(fd.el, plane_res->planes[index]);
}
std::uint32_t count() {
return plane_res->count_planes;
}
plane_it_t begin() const {
return plane_it_t { fd.el, plane_res->planes, plane_res->planes + plane_res->count_planes };
}
plane_it_t end() const {
return plane_it_t { fd.el, plane_res->planes + plane_res->count_planes };
}
file_t fd;
plane_res_t plane_res;
};
struct kms_img_t : public img_t {
~kms_img_t() override {
delete[] data;
data = nullptr;
}
};
void print(plane_t::pointer plane, fb_t::pointer fb, crtc_t::pointer crtc) {
if(crtc) {
BOOST_LOG(debug) << "crtc("sv << crtc->x << ", "sv << crtc->y << ')';
BOOST_LOG(debug) << "crtc("sv << crtc->width << ", "sv << crtc->height << ')';
BOOST_LOG(debug) << "plane->possible_crtcs == "sv << plane->possible_crtcs;
}
BOOST_LOG(debug)
<< "x("sv << plane->x
<< ") y("sv << plane->y
<< ") crtc_x("sv << plane->crtc_x
<< ") crtc_y("sv << plane->crtc_y
<< ") crtc_id("sv << plane->crtc_id
<< ')';
BOOST_LOG(debug)
<< "Resolution: "sv << fb->width << 'x' << fb->height
<< ": Pitch: "sv << fb->pitch
<< ": bpp: "sv << fb->bpp
<< ": depth: "sv << fb->depth;
std::stringstream ss;
ss << "Format ["sv;
std::for_each_n(plane->formats, plane->count_formats - 1, [&ss](auto format) {
ss << util::view(format) << ", "sv;
});
ss << util::view(plane->formats[plane->count_formats - 1]) << ']';
BOOST_LOG(debug) << ss.str();
}
class display_t : public platf::display_t {
public:
display_t(mem_type_e mem_type) : platf::display_t(), mem_type { mem_type } {}
~display_t() {
while(!thread_pool.cancel(loop_id))
;
}
mem_type_e mem_type;
std::chrono::nanoseconds delay;
// Done on a seperate thread to prevent additional latency to capture code
// This code detects if the framebuffer has been removed from KMS
void task_loop() {
capture_e capture = capture_e::reinit;
std::uint32_t framebuffer_count = 0;
auto end = std::end(card);
for(auto plane = std::begin(card); plane != end; ++plane) {
if(++framebuffer_count != framebuffer_index) {
continue;
}
auto fb = card.fb(plane.get());
if(!fb) {
BOOST_LOG(error) << "Couldn't get drm fb for plane ["sv << plane->fb_id << "]: "sv << strerror(errno);
capture = capture_e::error;
}
auto crct = card.crtc(plane->crtc_id);
bool different =
fb->width != img_width ||
fb->height != img_height ||
fb->pitch != pitch ||
crct->x != offset_x ||
crct->y != offset_y;
if(!different) {
capture = capture_e::ok;
break;
}
}
this->status = capture;
loop_id = thread_pool.pushDelayed(&display_t::task_loop, 2s, this).task_id;
}
int init(const std::string &display_name, int framerate) {
delay = std::chrono::nanoseconds { 1s } / framerate;
int monitor_index = util::from_view(display_name);
int monitor = 0;
fs::path card_dir { "/dev/dri"sv };
for(auto &entry : fs::directory_iterator { card_dir }) {
auto file = entry.path().filename();
auto filestring = file.generic_u8string();
if(std::string_view { filestring }.substr(0, 4) != "card"sv) {
continue;
}
kms::card_t card;
if(card.init(entry.path().c_str())) {
return {};
}
std::uint32_t framebuffer_index = 0;
auto end = std::end(card);
for(auto plane = std::begin(card); plane != end; ++plane) {
++framebuffer_index;
bool cursor = false;
auto props = card.plane_props(plane->plane_id);
for(auto &[prop, val] : props) {
if(prop->name == "type"sv) {
BOOST_LOG(verbose) << prop->name << "::"sv << kms::plane_type(val);
if(val == DRM_PLANE_TYPE_CURSOR) {
// Don't count as a monitor when it is a cursor
cursor = true;
break;
}
}
}
if(cursor) {
continue;
}
if(monitor != monitor_index) {
++monitor;
continue;
}
auto fb = card.fb(plane.get());
if(!fb) {
BOOST_LOG(error) << "Couldn't get drm fb for plane ["sv << plane->fb_id << "]: "sv << strerror(errno);
return -1;
}
if(!fb->handle) {
BOOST_LOG(error)
<< "Couldn't get handle for DRM Framebuffer ["sv << plane->fb_id << "]: Possibly not permitted: do [sudo setcap cap_sys_admin+ep sunshine]"sv;
return -1;
}
fb_fd = card.handleFD(fb->handle);
if(fb_fd.el < 0) {
BOOST_LOG(error) << "Couldn't get primary file descriptor for Framebuffer ["sv << fb->fb_id << "]: "sv << strerror(errno);
continue;
}
BOOST_LOG(info) << "Found monitor for DRM screencasting"sv;
auto crct = card.crtc(plane->crtc_id);
kms::print(plane.get(), fb.get(), crct.get());
img_width = fb->width;
img_height = fb->height;
width = crct->width;
height = crct->height;
pitch = fb->pitch;
this->env_width = ::platf::kms::env_width;
this->env_height = ::platf::kms::env_height;
offset_x = crct->x;
offset_y = crct->y;
this->card = std::move(card);
this->framebuffer_index = framebuffer_index;
goto break_loop;
}
}
// Neatly break from nested for loop
break_loop:
if(monitor != monitor_index) {
BOOST_LOG(error) << "Couldn't find monitor ["sv << monitor_index << ']';
return -1;
}
cursor_opt = x11::cursor_t::make();
status = capture_e::ok;
thread_pool.start(1);
loop_id = thread_pool.pushDelayed(&display_t::task_loop, 2s, this).task_id;
return 0;
}
// When the framebuffer is reinitialized, this id can no longer be found
std::uint32_t framebuffer_index;
capture_e status;
int img_width, img_height;
int pitch;
card_t card;
file_t fb_fd;
std::optional<x11::cursor_t> cursor_opt;
util::TaskPool::task_id_t loop_id;
util::ThreadPool thread_pool;
};
class display_ram_t : public display_t {
public:
display_ram_t(mem_type_e mem_type) : display_t(mem_type) {}
int init(const std::string &display_name, int framerate) {
if(!gbm::create_device) {
BOOST_LOG(warning) << "libgbm not initialized"sv;
return -1;
}
if(display_t::init(display_name, framerate)) {
return -1;
}
gbm.reset(gbm::create_device(card.fd.el));
if(!gbm) {
BOOST_LOG(error) << "Couldn't create GBM device: ["sv << util::hex(eglGetError()).to_string_view() << ']';
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);
auto rgb_opt = egl::import_source(display.get(),
{
fb_fd.el,
img_width,
img_height,
0,
pitch,
});
if(!rgb_opt) {
return -1;
}
rgb = std::move(*rgb_opt);
return 0;
}
capture_e capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr<img_t> img, bool *cursor) {
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:
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;
}
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);
}
return std::make_shared<hwdevice_t>();
}
capture_e snapshot(img_t *img_out_base, std::chrono::milliseconds timeout, bool cursor) {
gl::ctx.BindTexture(GL_TEXTURE_2D, rgb->tex[0]);
gl::ctx.GetTextureSubImage(rgb->tex[0], 0, offset_x, offset_y, 0, width, height, 1, GL_BGRA, GL_UNSIGNED_BYTE, img_out_base->height * img_out_base->row_pitch, img_out_base->data);
if(cursor_opt && cursor) {
cursor_opt->blend(*img_out_base, offset_x, offset_y);
}
return status;
}
std::shared_ptr<img_t> alloc_img() override {
auto img = std::make_shared<kms_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 {
snapshot(img, 1s, false);
return 0;
}
gbm::gbm_t gbm;
egl::display_t display;
egl::ctx_t ctx;
egl::rgb_t rgb;
};
class display_vram_t : public display_t {
public:
display_vram_t(mem_type_e mem_type) : display_t(mem_type) {}
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, dup(card.fd.el), offset_x, offset_y,
{
fb_fd.el,
img_width,
img_height,
0,
pitch,
});
}
BOOST_LOG(error) << "Unsupported pixel format for egl::display_vram_t: "sv << platf::from_pix_fmt(pix_fmt);
return nullptr;
}
std::shared_ptr<img_t> alloc_img() override {
auto img = std::make_shared<egl::cursor_t>();
img->serial = std::numeric_limits<decltype(img->serial)>::max();
img->data = nullptr;
img->pixel_pitch = 4;
return img;
}
int dummy_img(platf::img_t *img) override {
return 0;
}
capture_e capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr<img_t> img, bool *cursor) {
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:
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) {
if(!cursor || !cursor_opt) {
img_out_base->data = nullptr;
return capture_e::ok;
}
auto img = (egl::cursor_t *)img_out_base;
cursor_opt->capture(*img);
img->x -= offset_x;
img->y -= offset_y;
return status;
}
int init(const std::string &display_name, int framerate) {
if(display_t::init(display_name, framerate)) {
return -1;
}
if(!va::validate(card.fd.el)) {
BOOST_LOG(warning) << "Monitor "sv << display_name << " doesn't support hardware encoding. Reverting back to GPU -> RAM -> GPU"sv;
return -1;
}
return 0;
}
};
} // namespace kms
std::shared_ptr<display_t> kms_display(mem_type_e hwdevice_type, const std::string &display_name, int framerate) {
if(hwdevice_type == mem_type_e::vaapi) {
auto disp = std::make_shared<kms::display_vram_t>(hwdevice_type);
if(!disp->init(display_name, framerate)) {
return disp;
}
// In the case of failure, attempt the old method for VAAPI
}
auto disp = std::make_shared<kms::display_ram_t>(hwdevice_type);
if(disp->init(display_name, framerate)) {
return nullptr;
}
return disp;
}
// A list of names of displays accepted as display_name
std::vector<std::string> kms_display_names() {
kms::env_width = 0;
kms::env_height = 0;
int count = 0;
if(!gbm::create_device) {
BOOST_LOG(warning) << "libgbm not initialized"sv;
return {};
}
std::vector<std::string> display_names;
fs::path card_dir { "/dev/dri"sv };
for(auto &entry : fs::directory_iterator { card_dir }) {
auto file = entry.path().filename();
auto filestring = file.generic_u8string();
if(std::string_view { filestring }.substr(0, 4) != "card"sv) {
continue;
}
kms::card_t card;
if(card.init(entry.path().c_str())) {
return {};
}
auto end = std::end(card);
for(auto plane = std::begin(card); plane != end; ++plane) {
auto fb = card.fb(plane.get());
if(!fb) {
BOOST_LOG(error) << "Couldn't get drm fb for plane ["sv << plane->fb_id << "]: "sv << strerror(errno);
continue;
}
if(!fb->handle) {
BOOST_LOG(error)
<< "Couldn't get handle for DRM Framebuffer ["sv << plane->fb_id << "]: Possibly not permitted: do [sudo setcap cap_sys_admin+ep sunshine]"sv;
break;
}
bool cursor = false;
{
BOOST_LOG(verbose) << "PLANE INFO ["sv << count << ']';
auto props = card.plane_props(plane->plane_id);
for(auto &[prop, val] : props) {
if(prop->name == "type"sv) {
BOOST_LOG(verbose) << prop->name << "::"sv << kms::plane_type(val);
if(val == DRM_PLANE_TYPE_CURSOR) {
cursor = true;
}
}
else {
BOOST_LOG(verbose) << prop->name << "::"sv << val;
}
}
}
{
BOOST_LOG(verbose) << "CRTC INFO"sv;
auto props = card.crtc_props(plane->crtc_id);
for(auto &[prop, val] : props) {
BOOST_LOG(verbose) << prop->name << "::"sv << val;
}
}
// This appears to return the offset of the monitor
auto crtc = card.crtc(plane->crtc_id);
if(!crtc) {
BOOST_LOG(error) << "Couldn't get crtc info: "sv << strerror(errno);
return {};
}
kms::env_width = std::max(kms::env_width, (int)(crtc->x + crtc->width));
kms::env_height = std::max(kms::env_height, (int)(crtc->y + crtc->height));
kms::print(plane.get(), fb.get(), crtc.get());
if(!cursor) {
display_names.emplace_back(std::to_string(count++));
}
}
}
return display_names;
}
} // namespace platf

View File

@ -7,7 +7,10 @@
#include <fstream>
#include "graphics.h"
#include "misc.h"
#include "vaapi.h"
#include "sunshine/main.h"
#include "sunshine/platform/common.h"
@ -20,6 +23,47 @@
using namespace std::literals;
namespace fs = std::filesystem;
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>;
@ -93,46 +137,94 @@ std::string get_mac_address(const std::string_view &address) {
BOOST_LOG(warning) << "Unable to find MAC address for "sv << address;
return "00:00:00:00:00:00"s;
}
} // namespace platf
namespace dyn {
void *handle(const std::vector<const char *> &libs) {
void *handle;
enum class source_e {
#ifdef SUNSHINE_BUILD_DRM
KMS,
#endif
#ifdef SUNSHINE_BUILD_X11
X11,
#endif
};
static source_e source;
for(auto lib : libs) {
handle = dlopen(lib, RTLD_LAZY | RTLD_LOCAL);
if(handle) {
return handle;
}
#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 !x11_display_names().empty();
}
#endif
std::vector<std::string> display_names() {
switch(source) {
#ifdef SUNSHINE_BUILD_DRM
case source_e::KMS:
return kms_display_names();
#endif
#ifdef SUNSHINE_BUILD_X11
case source_e::X11:
return x11_display_names();
#endif
}
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;
});
return {};
}
ss << ']';
BOOST_LOG(error) << ss.str();
std::shared_ptr<display_t> display(mem_type_e hwdevice_type, const std::string &display_name, int framerate) {
switch(source) {
#ifdef SUNSHINE_BUILD_DRM
case source_e::KMS:
return kms_display(hwdevice_type, display_name, framerate);
#endif
#ifdef SUNSHINE_BUILD_X11
case source_e::X11:
return x11_display(hwdevice_type, display_name, framerate);
#endif
}
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);
std::unique_ptr<deinit_t> init() {
// These are allowed to fail.
gbm::init();
va::init();
*fn = SUNSHINE_GNUC_EXTENSION(apiproc) dlsym(handle, name);
#ifdef SUNSHINE_BUILD_DRM
if(verify_kms()) {
BOOST_LOG(info) << "Using KMS for screencasting"sv;
source = source_e::KMS;
goto found_source;
}
#endif
#ifdef SUNSHINE_BUILD_X11
if(verify_x11()) {
BOOST_LOG(info) << "Using X11 for screencasting"sv;
source = source_e::X11;
goto found_source;
}
#endif
// Did not find a source
return nullptr;
if(!*fn && strict) {
BOOST_LOG(error) << "Couldn't find function: "sv << name;
err = -1;
}
// Normally, I would simply use if-else statements to achieve this result,
// but due to the macro's, (*spits on ground*), it would be too messy
found_source:
if(!gladLoaderLoadEGL(EGL_NO_DISPLAY) || !eglGetPlatformDisplay) {
BOOST_LOG(warning) << "Couldn't load EGL library"sv;
}
return err;
return std::make_unique<deinit_t>();
}
} // namespace dyn
} // namespace platf

View File

@ -1,8 +1,17 @@
#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);
}
});
namespace dyn {
typedef void (*apiproc)(void);

View File

@ -426,4 +426,4 @@ public:
return std::make_unique<deinit_t>(std::thread { avahi::simple_poll_loop, poll.get() });
}
}; // namespace platf::publish
} // namespace platf::publish

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,19 @@
#ifndef SUNSHINE_DISPLAY_H
#define SUNSHINE_DISPLAY_H
#ifndef SUNSHINE_VAAPI_H
#define SUNSHINE_VAAPI_H
#include "misc.h"
#include "sunshine/platform/common.h"
namespace platf::egl {
std::shared_ptr<hwdevice_t> make_hwdevice(int width, int height);
} // namespace platf::egl
namespace egl {
struct surface_descriptor_t;
}
namespace va {
std::shared_ptr<platf::hwdevice_t> make_hwdevice(int width, int height);
std::shared_ptr<platf::hwdevice_t> make_hwdevice(int width, int height, file_t &&card, int offset_x, int offset_y, const egl::surface_descriptor_t &sd);
// Ensure the render device pointed to by fd is capable of encoding h264
bool validate(int fd);
int init();
} // namespace va
#endif

View File

@ -20,25 +20,252 @@
#include "sunshine/main.h"
#include "sunshine/task_pool.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::safe_ptr<xcb_connection_t, xcb_disconnect>;
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 xdisplay_t = util::safe_ptr_v2<Display, int, XCloseDisplay>;
using xdisplay_t = util::dyn_safe_ptr_v2<Display, int, &x11::CloseDisplay>;
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>;
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:
@ -86,8 +313,8 @@ struct shm_img_t : public img_t {
}
};
void blend_cursor(Display *display, img_t &img, int offsetX, int offsetY) {
xcursor_t overlay { XFixesGetCursorImage(display) };
static void blend_cursor(Display *display, img_t &img, int offsetX, int offsetY) {
xcursor_t overlay { x11::fix::GetCursorImage(display) };
if(!overlay) {
BOOST_LOG(error) << "Couldn't get cursor from XFixesGetCursorImage"sv;
@ -151,11 +378,11 @@ struct x11_attr_t : public display_t {
*/
// int env_width, env_height;
x11_attr_t(mem_type_e mem_type) : xdisplay { XOpenDisplay(nullptr) }, xwindow {}, xattr {}, mem_type { mem_type } {
XInitThreads();
x11_attr_t(mem_type_e mem_type) : xdisplay { x11::OpenDisplay(nullptr) }, xwindow {}, xattr {}, mem_type { mem_type } {
x11::InitThreads();
}
int init(int framerate, const std::string &output_name) {
int init(const std::string &display_name, int framerate) {
if(!xdisplay) {
BOOST_LOG(error) << "Could not open X11 display"sv;
return -1;
@ -168,19 +395,19 @@ struct x11_attr_t : public display_t {
refresh();
int streamedMonitor = -1;
if(!output_name.empty()) {
streamedMonitor = (int)util::from_view(output_name);
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 { XRRGetScreenResources(xdisplay.get(), xwindow) };
screen_res_t screenr { x11::rr::GetScreenResources(xdisplay.get(), xwindow) };
int output = screenr->noutput;
output_info_t result;
int monitor = 0;
for(int x = 0; x < output; ++x) {
output_info_t out_info { XRRGetOutputInfo(xdisplay.get(), screenr.get(), screenr->outputs[x]) };
output_info_t out_info { x11::rr::GetOutputInfo(xdisplay.get(), screenr.get(), screenr->outputs[x]) };
if(out_info && out_info->connection == RR_Connected) {
if(monitor++ == streamedMonitor) {
result = std::move(out_info);
@ -194,7 +421,7 @@ struct x11_attr_t : public display_t {
return -1;
}
crtc_info_t crt_info { XRRGetCrtcInfo(xdisplay.get(), screenr.get(), result->crtc) };
crtc_info_t crt_info { x11::rr::GetCrtcInfo(xdisplay.get(), screenr.get(), result->crtc) };
BOOST_LOG(info)
<< "Streaming display: "sv << result->name << " with res "sv << crt_info->width << 'x' << crt_info->height << " offset by "sv << crt_info->x << 'x' << crt_info->y;
@ -218,7 +445,7 @@ struct x11_attr_t : public display_t {
* Called when the display attributes should change.
*/
void refresh() {
XGetWindowAttributes(xdisplay.get(), xwindow, &xattr); //Update xattr's
x11::GetWindowAttributes(xdisplay.get(), xwindow, &xattr); //Update xattr's
}
capture_e capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr<img_t> img, bool *cursor) override {
@ -263,7 +490,7 @@ struct x11_attr_t : public display_t {
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) };
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;
@ -286,7 +513,7 @@ struct x11_attr_t : public display_t {
std::shared_ptr<hwdevice_t> make_hwdevice(pix_fmt_e pix_fmt) override {
if(mem_type == mem_type_e::vaapi) {
return egl::make_hwdevice(width, height);
return va::make_hwdevice(width, height);
}
return std::make_shared<hwdevice_t>();
@ -316,7 +543,7 @@ struct shm_attr_t : public x11_attr_t {
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 { XOpenDisplay(nullptr) } {
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;
}
@ -366,9 +593,9 @@ struct shm_attr_t : public x11_attr_t {
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);
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) };
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;
@ -399,26 +626,26 @@ struct shm_attr_t : public x11_attr_t {
return 0;
}
int init(int framerate, const std::string &output_name) {
if(x11_attr_t::init(framerate, output_name)) {
int init(const std::string &display_name, int framerate) {
if(x11_attr_t::init(display_name, framerate)) {
return 1;
}
shm_xdisplay.reset(XOpenDisplay(nullptr));
xcb.reset(xcb_connect(nullptr, nullptr));
if(xcb_connection_has_error(xcb.get())) {
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) {
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()));
auto iter = xcb::setup_roots_iterator(xcb::get_setup(xcb.get()));
display = iter.data;
seg = xcb_generate_id(xcb.get());
seg = xcb::generate_id(xcb.get());
shm_id.id = shmget(IPC_PRIVATE, frame_size(), IPC_CREAT | 0777);
if(shm_id.id == -1) {
@ -426,7 +653,7 @@ struct shm_attr_t : public x11_attr_t {
return -1;
}
xcb_shm_attach(xcb.get(), seg, shm_id.id, false);
xcb::shm_attach(xcb.get(), seg, shm_id.id, false);
data.data = shmat(shm_id.id, nullptr, 0);
if((uintptr_t)data.data == -1) {
@ -443,16 +670,22 @@ struct shm_attr_t : public x11_attr_t {
}
};
std::shared_ptr<display_t> display(platf::mem_type_e hwdevice_type, const std::string &output_name, int framerate) {
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 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(framerate, output_name);
auto status = shm_disp->init(display_name, framerate);
if(status > 0) {
// x11_attr_t::init() failed, don't bother trying again.
return nullptr;
@ -464,28 +697,34 @@ std::shared_ptr<display_t> display(platf::mem_type_e hwdevice_type, const std::s
// Fallback
auto x11_disp = std::make_shared<x11_attr_t>(hwdevice_type);
if(x11_disp->init(framerate, output_name)) {
if(x11_disp->init(display_name, framerate)) {
return nullptr;
}
return x11_disp;
}
std::vector<std::string> display_names() {
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;
xdisplay_t xdisplay { XOpenDisplay(nullptr) };
xdisplay_t xdisplay { x11::OpenDisplay(nullptr) };
if(!xdisplay) {
return {};
}
auto xwindow = DefaultRootWindow(xdisplay.get());
screen_res_t screenr { XRRGetScreenResources(xdisplay.get(), xwindow) };
screen_res_t screenr { x11::rr::GetScreenResources(xdisplay.get(), xwindow) };
int output = screenr->noutput;
int monitor = 0;
for(int x = 0; x < output; ++x) {
output_info_t out_info { XRRGetOutputInfo(xdisplay.get(), screenr.get(), screenr->outputs[x]) };
output_info_t out_info { x11::rr::GetOutputInfo(xdisplay.get(), screenr.get(), screenr->outputs[x]) };
if(out_info && out_info->connection == RR_Connected) {
++monitor;
}
@ -505,6 +744,69 @@ void freeImage(XImage *p) {
XDestroyImage(p);
}
void freeX(XFixesCursorImage *p) {
XFree(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 = 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);
}
void freeCursorCtx(cursor_ctx_t::pointer ctx) {
x11::CloseDisplay((xdisplay_t::pointer)ctx);
}
} // namespace x11
} // namespace platf

View File

@ -0,0 +1,48 @@
#ifndef SUNSHINE_X11_GRAB
#define SUNSHINE_X11_GRAB
#include <optional>
#include "sunshine/platform/common.h"
#include "sunshine/utility.h"
namespace egl {
class cursor_t;
}
namespace platf::x11 {
#ifdef SUNSHINE_BUILD_X11
struct cursor_ctx_raw_t;
void freeCursorCtx(cursor_ctx_raw_t *ctx);
using cursor_ctx_t = util::safe_ptr<cursor_ctx_raw_t, freeCursorCtx>;
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;
};
#else
class cursor_t {
public:
static std::optional<cursor_t> make() { return std::nullopt; }
void capture(egl::cursor_t &) {}
void blend(img_t &, int, int) {}
};
#endif
} // namespace platf::x11
#endif

View File

@ -69,9 +69,11 @@ struct cursor_t {
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) {
void set_pos(LONG rel_x, LONG rel_y, bool visible) {
cursor_view.TopLeftX = rel_x;
cursor_view.TopLeftY = rel_y;
this->visible = visible;
}
void set_texture(LONG width, LONG height, texture2d_t &&texture) {
@ -85,6 +87,8 @@ public:
shader_res_t input_res;
D3D11_VIEWPORT cursor_view;
bool visible;
};
class duplication_t {

View File

@ -673,7 +673,7 @@ capture_e display_vram_t::snapshot(platf::img_t *img_base, std::chrono::millisec
}
if(frame_info.LastMouseUpdateTime.QuadPart) {
cursor.set_pos(frame_info.PointerPosition.Position.x, frame_info.PointerPosition.Position.y);
cursor.set_pos(frame_info.PointerPosition.Position.x, frame_info.PointerPosition.Position.y, frame_info.PointerPosition.Visible && cursor_visible);
}
if(frame_update_flag) {
@ -687,10 +687,10 @@ capture_e display_vram_t::snapshot(platf::img_t *img_base, std::chrono::millisec
}
device_ctx->CopyResource(img->texture.get(), src.get());
if(frame_info.PointerPosition.Visible && cursor_visible) {
if(cursor.visible) {
D3D11_VIEWPORT view {
0.0f, 0.0f,
width, height,
(float)width, (float)height,
0.0f, 1.0f
};

View File

@ -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"
@ -109,14 +110,17 @@ int proc_t::execute(int app_id) {
if(proc.cmd.empty()) {
BOOST_LOG(debug) << "Executing [Desktop]"sv;
placebo = true;
}
else if(proc.output.empty() || proc.output == "null"sv) {
BOOST_LOG(info) << "Executing: ["sv << proc.cmd << ']';
_process = bp::child(_process_handle, proc.cmd, _env, 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::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) {
@ -275,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) {
@ -312,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);

View File

@ -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,6 +53,7 @@ struct ctx_t {
std::string name;
std::string cmd;
std::string working_dir;
std::string output;
};

View File

@ -389,6 +389,10 @@ auto enm(T &val) -> std::underlying_type_t<T> & {
}
inline std::int64_t from_chars(const char *begin, const char *end) {
if(begin == end) {
return 0;
}
std::int64_t res {};
std::int64_t mul = 1;
while(begin != --end) {
@ -592,6 +596,14 @@ bool operator!=(std::nullptr_t, const uniq_ptr<T, D> &y) {
return (bool)y;
}
template<class P>
using shared_t = std::shared_ptr<typename P::element_type>;
template<class P, class T>
shared_t<P> make_shared(T *pointer) {
return shared_t<P>(reinterpret_cast<typename P::pointer>(pointer), typename P::deleter_type());
}
template<class T>
class wrap_ptr {
public:

View File

@ -88,7 +88,7 @@ public:
data[0] = sw_frame->data[0] + offsetY;
if(sw_frame->format == AV_PIX_FMT_NV12) {
data[1] = sw_frame->data[1] + offsetUV;
data[1] = sw_frame->data[1] + offsetUV * 2;
data[2] = nullptr;
}
else {
@ -237,10 +237,11 @@ public:
};
enum flag_e {
DEFAULT = 0x00,
SYSTEM_MEMORY = 0x01,
H264_ONLY = 0x02,
LIMITED_GOP_SIZE = 0x04,
DEFAULT = 0x00,
PARALLEL_ENCODING = 0x01,
H264_ONLY = 0x02, // When HEVC is to heavy
LIMITED_GOP_SIZE = 0x04, // Some encoders don't like it when you have an infinite GOP_SIZE. *cough* VAAPI *cough*
SINGLE_SLICE_ONLY = 0x08, // Never use multiple slices <-- Older intel iGPU's ruin it for everyone else :P
};
struct encoder_t {
@ -440,7 +441,7 @@ static encoder_t nvenc {
DEFAULT,
dxgi_make_hwdevice_ctx
#else
SYSTEM_MEMORY,
PARALLEL_ENCODING,
cuda_make_hwdevice_ctx
#endif
};
@ -506,7 +507,7 @@ static encoder_t software {
std::make_optional<encoder_t::option_t>("qp"s, &config::video.qp),
"libx264"s,
},
H264_ONLY | SYSTEM_MEMORY,
H264_ONLY | PARALLEL_ENCODING,
nullptr
};
@ -534,7 +535,7 @@ static encoder_t vaapi {
std::make_optional<encoder_t::option_t>("qp"s, &config::video.qp),
"h264_vaapi"s,
},
LIMITED_GOP_SIZE | SYSTEM_MEMORY,
LIMITED_GOP_SIZE | PARALLEL_ENCODING | SINGLE_SLICE_ONLY,
vaapi_make_hwdevice_ctx
};
@ -675,11 +676,10 @@ void captureThread(
img.reset();
}
// Some classes of display cannot have multiple instances at once
disp.reset();
// display_wp is modified in this thread only
while(!display_wp->expired()) {
// Wait for the other shared_ptr's of display to be destroyed.
// New displays will only be created in this thread.
while(display_wp->use_count() != 1) {
std::this_thread::sleep_for(100ms);
}
@ -696,6 +696,7 @@ void captureThread(
}
display_wp = disp;
// Re-allocate images
for(auto &img : imgs) {
img = disp->alloc_img();
@ -1371,7 +1372,7 @@ void capture(
auto idr_events = mail->event<bool>(mail::idr);
idr_events->raise(true);
if(encoders.front().flags & SYSTEM_MEMORY) {
if(encoders.front().flags & PARALLEL_ENCODING) {
capture_async(std::move(mail), config, channel_data);
}
else {
@ -1537,8 +1538,13 @@ retry:
std::vector<std::pair<encoder_t::flag_e, config_t>> configs {
{ encoder_t::DYNAMIC_RANGE, { 1920, 1080, 60, 1000, 1, 0, 3, 1, 1 } },
{ encoder_t::SLICE, { 1920, 1080, 60, 1000, 2, 1, 1, 0, 0 } },
};
if(!(encoder.flags & SINGLE_SLICE_ONLY)) {
configs.emplace_back(
std::pair<encoder_t::flag_e, config_t> { encoder_t::SLICE, { 1920, 1080, 60, 1000, 2, 1, 1, 0, 0 } });
}
for(auto &[flag, config] : configs) {
auto h264 = config;
auto hevc = config;
@ -1552,6 +1558,11 @@ retry:
}
}
if(encoder.flags & SINGLE_SLICE_ONLY) {
encoder.h264.capabilities[encoder_t::SLICE] = false;
encoder.hevc.capabilities[encoder_t::SLICE] = false;
}
encoder.h264[encoder_t::VUI_PARAMETERS] = encoder.h264[encoder_t::VUI_PARAMETERS] && !config::sunshine.flags[config::flag::FORCE_VIDEO_HEADER_REPLACE];
encoder.hevc[encoder_t::VUI_PARAMETERS] = encoder.hevc[encoder_t::VUI_PARAMETERS] && !config::sunshine.flags[config::flag::FORCE_VIDEO_HEADER_REPLACE];