diff --git a/CMakeLists.txt b/CMakeLists.txt index cd4e7037..45d4bf74 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/README.md b/README.md index b78bc234..c666dc59 100644 --- a/README.md +++ b/README.md @@ -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_BUILD_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` diff --git a/FindFFmpeg.cmake b/cmake/FindFFMPEG.cmake similarity index 100% rename from FindFFmpeg.cmake rename to cmake/FindFFMPEG.cmake diff --git a/cmake/FindLIBDRM.cmake b/cmake/FindLIBDRM.cmake new file mode 100644 index 00000000..fd0548c1 --- /dev/null +++ b/cmake/FindLIBDRM.cmake @@ -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) \ No newline at end of file diff --git a/gen-deb.in b/gen-deb.in index 09ec1025..8dae0d09 100755 --- a/gen-deb.in +++ b/gen-deb.in @@ -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.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, libdrm2 Description: Gamestream host for Moonlight EOF @@ -88,6 +88,8 @@ if [ -f /etc/sunshine/sunshine.conf ]; then echo "chmod 666 /etc/sunshine/sunshine.conf" chmod 666 /etc/sunshine/sunshine.conf fi + +setcap cap_sys_admin+ep /usr/bin/sunshine EOF cat << 'EOF' > $RULES/85-sunshine-rules.rules diff --git a/sunshine/audio.cpp b/sunshine/audio.cpp index cb4fac28..4279eb4c 100644 --- a/sunshine/audio.cpp +++ b/sunshine/audio.cpp @@ -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(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; } diff --git a/sunshine/platform/common.h b/sunshine/platform/common.h index 1f6b8498..4fe87b7c 100644 --- a/sunshine/platform/common.h +++ b/sunshine/platform/common.h @@ -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; }; diff --git a/sunshine/platform/linux/graphics.cpp b/sunshine/platform/linux/graphics.cpp new file mode 100644 index 00000000..d006d33b --- /dev/null +++ b/sunshine/platform/linux/graphics.cpp @@ -0,0 +1,707 @@ +#include "graphics.h" +#include "sunshine/video.h" + +#include + +// 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::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 &&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 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::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 program_t::uniform(const char *block, std::pair *members, std::size_t count) { + auto i = ctx.GetUniformBlockIndex(handle(), block); + if(i == GL_INVALID_INDEX) { + BOOST_LOG(error) << "Couldn't find index of ["sv << block << ']'; + return std::nullopt; + } + + int size; + ctx.GetActiveUniformBlockiv(handle(), i, GL_UNIFORM_BLOCK_DATA_SIZE, &size); + + bool error_flag = false; + + util::buffer_t offsets { count }; + auto indices = (std::uint32_t *)alloca(count * sizeof(std::uint32_t)); + auto names = (const char **)alloca(count * sizeof(const char *)); + auto names_p = names; + + 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 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> 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 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 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 import_target(display_t::pointer egl_display, std::array &&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::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::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 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 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::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); +} \ No newline at end of file diff --git a/sunshine/platform/linux/graphics.h b/sunshine/platform/linux/graphics.h new file mode 100644 index 00000000..3513047f --- /dev/null +++ b/sunshine/platform/linux/graphics.h @@ -0,0 +1,267 @@ +#ifndef SUNSHINE_PLATFORM_LINUX_OPENGL_H +#define SUNSHINE_PLATFORM_LINUX_OPENGL_H + +#include +#include + +#include +#include + +#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; + +namespace gl { +extern GladGLContext ctx; +void drain_errors(const std::string_view &prefix); + +class tex_t : public util::buffer_t { + using util::buffer_t::buffer_t; + +public: + tex_t(tex_t &&) = default; + tex_t &operator=(tex_t &&) = default; + + ~tex_t(); + + static tex_t make(std::size_t count); +}; + +class frame_buf_t : public util::buffer_t { + using util::buffer_t::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 + 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::max(), { + if(el != std::numeric_limits::max()) { + ctx.DeleteShader(el); + } + }); + +public: + std::string err_str(); + + static util::Either 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::max(), { + if(el != std::numeric_limits::max()) { + ctx.DeleteBuffers(1, &el); + } + }); + +public: + static buffer_t make(util::buffer_t &&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 _offsets; + + buffer_internal_t _buffer; +}; + +class program_t { + KITTY_USING_MOVE_T(program_internal_t, GLuint, std::numeric_limits::max(), { + if(el != std::numeric_limits::max()) { + ctx.DeleteProgram(el); + } + }); + +public: + std::string err_str(); + + static util::Either link(const shader_t &vert, const shader_t &frag); + + void bind(const buffer_t &buffer); + + std::optional uniform(const char *block, std::pair *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; + +int init(); + +} // namespace gbm + +namespace egl { +using display_t = util::dyn_safe_ptr_v2; + +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 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), , { + 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 make_ctx(display_t::pointer display); + +std::optional import_source( + display_t::pointer egl_display, + const surface_descriptor_t &xrgb); + +std::optional import_target( + display_t::pointer egl_display, + std::array &&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 buffer; +}; + +class sws_t { +public: + static std::optional make(int in_width, int in_height, int out_width, int out_heigth, gl::tex_t &&tex); + static std::optional 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 \ No newline at end of file diff --git a/sunshine/platform/linux/kmsgrab.cpp b/sunshine/platform/linux/kmsgrab.cpp new file mode 100644 index 00000000..99e87b6d --- /dev/null +++ b/sunshine/platform/linux/kmsgrab.cpp @@ -0,0 +1,737 @@ +#include +#include +#include +#include +#include +#include + +#include + +#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; +using plane_t = util::safe_ptr; +using fb_t = util::safe_ptr; +using fb2_t = util::safe_ptr; +using crtc_t = util::safe_ptr; +using obj_prop_t = util::safe_ptr; +using prop_t = util::safe_ptr; + +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 { +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.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; +}; + +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); + } + + fb2_t fb2(plane_t::pointer plane) { + return drmModeGetFB2(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> props(std::uint32_t id, std::uint32_t type) { + obj_prop_t obj_prop = drmModeObjectGetProperties(fd.el, id, type); + + std::vector> 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> plane_props(std::uint32_t id) { + return props(id, DRM_MODE_OBJECT_PLANE); + } + + std::vector> crtc_props(std::uint32_t id) { + return props(id, DRM_MODE_OBJECT_CRTC); + } + + std::vector> 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; + + 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); + capture = capture_e::error; + } + + if(fb->fb_id == framebuffer_id) { + 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 {}; + } + + auto end = std::end(card); + for(auto plane = std::begin(card); plane != end; ++plane) { + 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()); + + framebuffer_id = fb->fb_id; + + 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); + 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_id; + + capture_e status; + + int img_width, img_height; + int pitch; + + card_t card; + file_t fb_fd; + + std::optional 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, 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 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(); + } + + 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 alloc_img() override { + auto img = std::make_shared(); + 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 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 alloc_img() override { + auto img = std::make_shared(); + + img->serial = std::numeric_limitsserial)>::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, 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 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(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(hwdevice_type); + + if(disp->init(display_name, framerate)) { + return nullptr; + } + + return disp; +} + +// A list of names of displays accepted as display_name +std::vector 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 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)); + + auto fb_2 = card.fb2(plane.get()); + for(int x = 0; x < 4 && fb_2->handles[x]; ++x) { + BOOST_LOG(debug) << "handles::"sv << x << '(' << fb_2->handles[x] << ')'; + BOOST_LOG(debug) << "pixel_format::"sv << util::view(fb_2->pixel_format); + } + + kms::print(plane.get(), fb.get(), crtc.get()); + + if(!cursor) { + display_names.emplace_back(std::to_string(count++)); + } + } + } + + return display_names; +} + +} // namespace platf \ No newline at end of file diff --git a/sunshine/platform/linux/misc.cpp b/sunshine/platform/linux/misc.cpp index c4c63e84..0e80c379 100644 --- a/sunshine/platform/linux/misc.cpp +++ b/sunshine/platform/linux/misc.cpp @@ -7,7 +7,10 @@ #include +#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 &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> &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; @@ -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 &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 kms_display_names(); +std::shared_ptr 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 x11_display_names(); +std::shared_ptr 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 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(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> &funcs, bool strict) { - int err = 0; - for(auto &func : funcs) { - TUPLE_2D_REF(fn, name, func); +std::unique_ptr 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(); } -} // namespace dyn \ No newline at end of file +} // namespace platf \ No newline at end of file diff --git a/sunshine/platform/linux/misc.h b/sunshine/platform/linux/misc.h index 70c9d880..cdefff64 100644 --- a/sunshine/platform/linux/misc.h +++ b/sunshine/platform/linux/misc.h @@ -1,8 +1,17 @@ #ifndef SUNSHINE_PLATFORM_MISC_H #define SUNSHINE_PLATFORM_MISC_H +#include #include +#include "sunshine/utility.h" + +KITTY_USING_MOVE_T(file_t, int, -1, { + if(el >= 0) { + close(el); + } +}); + namespace dyn { typedef void (*apiproc)(void); diff --git a/sunshine/platform/linux/publish.cpp b/sunshine/platform/linux/publish.cpp index 05208cbb..825f3740 100644 --- a/sunshine/platform/linux/publish.cpp +++ b/sunshine/platform/linux/publish.cpp @@ -426,4 +426,4 @@ public: return std::make_unique(std::thread { avahi::simple_poll_loop, poll.get() }); } -}; // namespace platf::publish \ No newline at end of file +} // namespace platf::publish \ No newline at end of file diff --git a/sunshine/platform/linux/vaapi.cpp b/sunshine/platform/linux/vaapi.cpp index c9d0426b..966b6139 100644 --- a/sunshine/platform/linux/vaapi.cpp +++ b/sunshine/platform/linux/vaapi.cpp @@ -4,42 +4,21 @@ #include #include -#include extern "C" { #include } +#include "graphics.h" #include "misc.h" #include "sunshine/config.h" #include "sunshine/main.h" #include "sunshine/platform/common.h" #include "sunshine/utility.h" -#include "sunshine/video.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 SUNSHINE_SHADERS_DIR SUNSHINE_ASSETS_DIR "/shaders/opengl" - -#define STRINGIFY(x) #x -#define gl_drain_errors_helper(x) gl::drain_errors("line " STRINGIFY(x)) -#define gl_drain_errors gl_drain_errors_helper(__LINE__) using namespace std::literals; -static void free_frame(AVFrame *frame) { - av_frame_free(&frame); -} - -using frame_t = util::safe_ptr; +extern "C" struct AVBufferRef; namespace va { constexpr auto SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2 = 0x40000000; @@ -95,6 +74,119 @@ struct DRMPRIMESurfaceDescriptor { } layers[4]; }; +/** Currently defined profiles */ +enum class profile_e { + // Profile ID used for video processing. + None = -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); @@ -108,18 +200,20 @@ typedef VAStatus (*exportSurfaceHandle_fn)( uint32_t mem_type, uint32_t flags, void *descriptor); -getDisplayDRM_fn getDisplayDRM; -terminate_fn terminate; -initialize_fn initialize; -errorStr_fn errorStr; -setErrorCallback_fn setErrorCallback; -setInfoCallback_fn setInfoCallback; -queryVendorString_fn queryVendorString; -exportSurfaceHandle_fn exportSurfaceHandle; +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; -int init() { +int init_main_va() { static void *handle { nullptr }; static bool funcs_loaded = false; @@ -133,6 +227,8 @@ int init() { } std::vector> funcs { + { (dyn::apiproc *)&maxNumEntrypoints, "vaMaxNumEntrypoints" }, + { (dyn::apiproc *)&queryConfigEntrypoints, "vaQueryConfigEntrypoints" }, { (dyn::apiproc *)&terminate, "vaTerminate" }, { (dyn::apiproc *)&initialize, "vaInitialize" }, { (dyn::apiproc *)&errorStr, "vaErrorStr" }, @@ -150,8 +246,8 @@ int init() { return 0; } -int init_drm() { - if(init()) { +int init() { + if(init_main_va()) { return -1; } @@ -178,716 +274,43 @@ int init_drm() { funcs_loaded = true; return 0; } -} // namespace va - -namespace gbm { -struct device; -typedef void (*device_destroy_fn)(device *gbm); -typedef device *(*create_device_fn)(int fd); - -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> 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 gl { -static 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() << ']'; - } -} - -class tex_t : public util::buffer_t { - using util::buffer_t::buffer_t; - -public: - tex_t(tex_t &&) = default; - tex_t &operator=(tex_t &&) = default; - - ~tex_t() { - if(!size() == 0) { - ctx.DeleteTextures(size(), begin()); - } - } - - static 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; - } -}; - -class frame_buf_t : public util::buffer_t { - using util::buffer_t::buffer_t; - -public: - frame_buf_t(frame_buf_t &&) = default; - frame_buf_t &operator=(frame_buf_t &&) = default; - - ~frame_buf_t() { - if(begin()) { - ctx.DeleteFramebuffers(size(), begin()); - } - } - - static 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; - } - - template - void bind(It it_begin, It it_end) { - 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::max(), { - if(el != std::numeric_limits::max()) { - ctx.DeleteShader(el); - } - }); - -public: - std::string 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; - } - - static util::Either 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 handle() const { - return _shader.el; - } - -private: - shader_internal_t _shader; -}; - -class buffer_t { - KITTY_USING_MOVE_T(buffer_internal_t, GLuint, std::numeric_limits::max(), { - if(el != std::numeric_limits::max()) { - ctx.DeleteBuffers(1, &el); - } - }); - -public: - static buffer_t make(util::buffer_t &&offsets, const char *block, const std::string_view &data) { - buffer_t buffer; - buffer._block = block; - buffer._size = data.size(); - 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 handle() const { - return _buffer.el; - } - - const char *block() const { - return _block; - } - - void update(const std::string_view &view, std::size_t offset = 0) { - ctx.BindBuffer(GL_UNIFORM_BUFFER, handle()); - ctx.BufferSubData(GL_UNIFORM_BUFFER, offset, view.size(), (const void *)view.data()); - } - - void update(std::string_view *members, std::size_t count, std::size_t offset = 0) { - util::buffer_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); - } - -private: - const char *_block; - - std::size_t _size; - - util::buffer_t _offsets; - - buffer_internal_t _buffer; -}; - -class program_t { - KITTY_USING_MOVE_T(program_internal_t, GLuint, std::numeric_limits::max(), { - if(el != std::numeric_limits::max()) { - ctx.DeleteProgram(el); - } - }); - -public: - std::string 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; - } - - static util::Either 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 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 uniform(const char *block, std::pair *members, std::size_t count) { - auto i = ctx.GetUniformBlockIndex(handle(), block); - if(i == GL_INVALID_INDEX) { - BOOST_LOG(error) << "Couldn't find index of ["sv << block << ']'; - return std::nullopt; - } - - int size; - ctx.GetActiveUniformBlockiv(handle(), i, GL_UNIFORM_BLOCK_DATA_SIZE, &size); - - bool error_flag = false; - - util::buffer_t offsets { count }; - auto indices = (std::uint32_t *)alloca(count * sizeof(std::uint32_t)); - auto names = (const char **)alloca(count * sizeof(const char *)); - auto names_p = names; - - 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 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 handle() const { - return _program.el; - } - -private: - program_internal_t _program; -}; -} // namespace gl - -namespace platf { -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; - -using display_t = util::dyn_safe_ptr_v2; -using gbm_t = util::dyn_safe_ptr; int vaapi_make_hwdevice_ctx(platf::hwdevice_t *base, AVBufferRef **hw_device_buf); -KITTY_USING_MOVE_T(file_t, int, -1, { - if(el >= 0) { - close(el); - } -}); - -struct nv12_img_t { - display_t::pointer display; - EGLImage r8; - EGLImage bg88; - - gl::tex_t tex; - gl::frame_buf_t buf; - - static constexpr std::size_t num_fds = - sizeof(va::DRMPRIMESurfaceDescriptor::objects) / sizeof(va::DRMPRIMESurfaceDescriptor::objects[0]); - - std::array fds; -}; - -KITTY_USING_MOVE_T(nv12_t, nv12_img_t, , { - if(el.r8) { - eglDestroyImageKHR(el.display, el.r8); - } - - if(el.bg88) { - eglDestroyImageKHR(el.display, el.bg88); - } -}); - -KITTY_USING_MOVE_T(ctx_t, (std::tuple), , { - TUPLE_2D_REF(disp, ctx, el); - if(ctx) { - if(ctx == eglGetCurrentContext()) { - eglMakeCurrent(disp, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); - } - eglDestroyContext(disp, ctx); - } -}); - -bool fail() { - return eglGetError() != EGL_SUCCESS; -} - -class egl_t : public platf::hwdevice_t { +class va_t : public platf::hwdevice_t { public: - std::optional import(va::VASurfaceID surface) { - // No deallocation necessary - va::DRMPRIMESurfaceDescriptor prime; + int init(int in_width, int in_height, file_t &&render_device) { + file = std::move(render_device); - auto status = va::exportSurfaceHandle( - 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 std::nullopt; - } - - // Keep track of file descriptors - std::array fds; - for(int x = 0; x < prime.num_objects; ++x) { - fds[x] = prime.objects[x].fd; - } - - int img_attr_planes[2][13] { - { EGL_LINUX_DRM_FOURCC_EXT, DRM_FORMAT_R8, - EGL_WIDTH, (int)prime.width, - EGL_HEIGHT, (int)prime.height, - EGL_DMA_BUF_PLANE0_FD_EXT, prime.objects[prime.layers[0].object_index[0]].fd, - EGL_DMA_BUF_PLANE0_OFFSET_EXT, (int)prime.layers[0].offset[0], - EGL_DMA_BUF_PLANE0_PITCH_EXT, (int)prime.layers[0].pitch[0], - EGL_NONE }, - - { EGL_LINUX_DRM_FOURCC_EXT, DRM_FORMAT_GR88, - EGL_WIDTH, (int)prime.width / 2, - EGL_HEIGHT, (int)prime.height / 2, - EGL_DMA_BUF_PLANE0_FD_EXT, prime.objects[prime.layers[0].object_index[1]].fd, - EGL_DMA_BUF_PLANE0_OFFSET_EXT, (int)prime.layers[0].offset[1], - EGL_DMA_BUF_PLANE0_PITCH_EXT, (int)prime.layers[0].pitch[1], - EGL_NONE }, - }; - - nv12_t nv12 { - display.get(), - eglCreateImageKHR(display.get(), EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, img_attr_planes[0]), - eglCreateImageKHR(display.get(), 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 set_colorspace(std::uint32_t colorspace, std::uint32_t color_range) override { - 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]))); - } - - int init(int in_width, int in_height, const char *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; } - file.el = open(render_device, O_RDWR); - - if(file.el < 0) { - char error_buf[1024]; - BOOST_LOG(error) << "Couldn't open ["sv << render_device << "]: "sv << strerror_r(errno, error_buf, sizeof(error_buf)); - return -1; - } + this->data = (void *)vaapi_make_hwdevice_ctx; gbm.reset(gbm::create_device(file.el)); if(!gbm) { - BOOST_LOG(error) << "Couldn't create GBM device: ["sv << util::hex(eglGetError()).to_string_view() << ']'; + char string[1024]; + BOOST_LOG(error) << "Couldn't create GBM device: ["sv << strerror_r(errno, string, sizeof(string)) << ']'; return -1; } - constexpr auto EGL_PLATFORM_GBM_MESA = 0x31D7; - - display.reset(eglGetPlatformDisplay(EGL_PLATFORM_GBM_MESA, gbm.get(), nullptr)); - if(fail()) { - BOOST_LOG(error) << "Couldn't open EGL display: ["sv << util::hex(eglGetError()).to_string_view() << ']'; + display = egl::make_display(gbm.get()); + if(!display) { return -1; } - int major, minor; - if(!eglInitialize(display.get(), &major, &minor)) { - BOOST_LOG(error) << "Couldn't initialize EGL display: ["sv << util::hex(eglGetError()).to_string_view() << ']'; + auto ctx_opt = egl::make_ctx(display.get()); + if(!ctx_opt) { return -1; } - 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); + ctx = std::move(*ctx_opt); - 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 -1; - } - } - - constexpr int conf_attr[] { - EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, EGL_NONE - }; - - int count; - EGLConfig conf; - if(!eglChooseConfig(display.get(), conf_attr, &conf, 1, &count)) { - BOOST_LOG(error) << "Couldn't set config attributes: ["sv << util::hex(eglGetError()).to_string_view() << ']'; - return -1; - } - - if(!eglBindAPI(EGL_OPENGL_API)) { - BOOST_LOG(error) << "Couldn't bind API: ["sv << util::hex(eglGetError()).to_string_view() << ']'; - return -1; - } - - constexpr int attr[] { - EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE - }; - - ctx.el = { display.get(), eglCreateContext(display.get(), conf, EGL_NO_CONTEXT, attr) }; - if(fail()) { - BOOST_LOG(error) << "Couldn't create EGL context: ["sv << util::hex(eglGetError()).to_string_view() << ']'; - return -1; - } - - TUPLE_EL_REF(ctx_p, 1, ctx.el); - if(!eglMakeCurrent(display.get(), EGL_NO_SURFACE, EGL_NO_SURFACE, ctx_p)) { - BOOST_LOG(error) << "Couldn't make current display"sv; - return -1; - } - - if(!gladLoadGLContext(&gl::ctx, eglGetProcAddress)) { - BOOST_LOG(error) << "Couldn't load OpenGL library"sv; - return -1; - } - - 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); - - { - 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 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 -1; - } - - auto 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 -1; - } - - // UV - shader - this->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 -1; - } - - // Y - shader - this->program[0] = std::move(program.left()); - } - - auto color_p = &video::colors[0]; - std::pair 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 = program[0].uniform("ColorMatrix", members, sizeof(members) / sizeof(decltype(members[0]))); - if(!color_matrix) { - return -1; - } - - this->color_matrix = std::move(*color_matrix); - - tex_in = gl::tex_t::make(1); - - this->in_width = in_width; - this->in_height = in_height; - - data = (void *)vaapi_make_hwdevice_ctx; - gl_drain_errors; - return 0; - } - - int convert(platf::img_t &img) override { - auto tex = tex_in[0]; - - gl::ctx.BindTexture(GL_TEXTURE_2D, tex); - gl::ctx.TexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, in_width, in_height, GL_BGRA, GL_UNSIGNED_BYTE, img.data); - - 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]); - - 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; - } - - gl::ctx.BindTexture(GL_TEXTURE_2D, tex); - - gl::ctx.UseProgram(program[x].handle()); - program[x].bind(color_matrix); - - gl::ctx.Viewport(offsetX / (x + 1), offsetY / (x + 1), out_width / (x + 1), out_height / (x + 1)); - gl::ctx.DrawArrays(GL_TRIANGLES, 0, 3); - } + width = in_width; + height = in_height; return 0; } @@ -902,72 +325,126 @@ public: return -1; } + va::DRMPRIMESurfaceDescriptor prime; va::VASurfaceID surface = (std::uintptr_t)frame->data[3]; - auto nv12_opt = import(surface); + 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 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), + { + prime.objects[prime.layers[0].object_index[0]].fd, + (int)prime.width, + (int)prime.height, + (int)prime.layers[0].offset[0], + (int)prime.layers[0].pitch[0], + }, + { + prime.objects[prime.layers[0].object_index[1]].fd, + (int)prime.width / 2, + (int)prime.height / 2, + (int)prime.layers[0].offset[1], + (int)prime.layers[0].pitch[1], + }); + if(!nv12_opt) { return -1; } - nv12 = std::move(*nv12_opt); - - // // Ensure aspect ratio is maintained - auto scalar = std::fminf(frame->width / (float)in_width, frame->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 = (frame->width - out_width_f) / 2; - auto offsetY_f = (frame->height - out_height_f) / 2; - - out_width = out_width_f; - out_height = out_height_f; - - offsetX = offsetX_f; - offsetY = offsetY_f; - - auto tex = tex_in[0]; - - gl::ctx.BindTexture(GL_TEXTURE_2D, tex); - gl::ctx.TexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, in_width, in_height); - - auto loc_width_i = gl::ctx.GetUniformLocation(program[1].handle(), "width_i"); - if(loc_width_i < 0) { - BOOST_LOG(error) << "Couldn't find uniform [width_i]"sv; + auto sws_opt = egl::sws_t::make(width, height, frame->width, frame->height); + if(!sws_opt) { return -1; } - auto width_i = 1.0f / out_width; - gl::ctx.UseProgram(program[1].handle()); - gl::ctx.Uniform1fv(loc_width_i, 1, &width_i); + this->sws = std::move(*sws_opt); + this->nv12 = std::move(*nv12_opt); - gl_drain_errors; return 0; } - ~egl_t() override { - if(gl::ctx.GetError) { - gl_drain_errors; - } + void set_colorspace(std::uint32_t colorspace, std::uint32_t color_range) override { + sws.set_colorspace(colorspace, color_range); } - int in_width, in_height; - int out_width, out_height; - int offsetX, offsetY; + va::display_t::pointer va_display; + file_t file; frame_t hwframe; - va::display_t::pointer va_display; + gbm::gbm_t gbm; + egl::display_t display; + egl::ctx_t ctx; - file_t file; - gbm_t gbm; - display_t display; - ctx_t ctx; + egl::sws_t sws; + egl::nv12_t nv12; - gl::tex_t tex_in; - nv12_t nv12; - gl::program_t program[2]; - gl::buffer_t color_matrix; + 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); + return 0; + } +}; + +class va_vram_t : public va_t { +public: + int convert(platf::img_t &img) override { + sws.load_vram((egl::cursor_t &)img, offset_x, offset_y, framebuffer[0]); + + sws.convert(nv12); + return 0; + } + + int init(int in_width, int in_height, file_t &&render_device, int offset_x, int offset_y, const egl::surface_descriptor_t &sd) { + if(va_t::init(in_width, in_height, std::move(render_device))) { + return -1; + } + + auto rgb_opt = egl::import_source(display.get(), sd); + if(!rgb_opt) { + return -1; + } + + rgb = std::move(*rgb_opt); + + framebuffer = gl::frame_buf_t::make(1); + framebuffer.bind(std::begin(rgb->tex), std::end(rgb->tex)); + + this->offset_x = offset_x; + this->offset_y = offset_y; + + return 0; + } + + file_t fb_fd; + + egl::rgb_t rgb; + gl::frame_buf_t framebuffer; + + int offset_x, offset_y; }; /** @@ -1019,8 +496,8 @@ int vaapi_make_hwdevice_ctx(platf::hwdevice_t *base, AVBufferRef **hw_device_buf return -1; } - auto egl = (platf::egl::egl_t *)base; - auto fd = dup(egl->file.el); + auto va = (va::va_t *)base; + auto fd = dup(va->file.el); auto *priv = (VAAPIDevicePriv *)av_mallocz(sizeof(VAAPIDevicePriv)); priv->drm_fd = fd; @@ -1039,7 +516,7 @@ int vaapi_make_hwdevice_ctx(platf::hwdevice_t *base, AVBufferRef **hw_device_buf return -1; } - egl->va_display = display.get(); + va->va_display = display.get(); va::setErrorCallback(display.get(), __log, &error); va::setErrorCallback(display.get(), __log, &info); @@ -1070,26 +547,91 @@ int vaapi_make_hwdevice_ctx(platf::hwdevice_t *base, AVBufferRef **hw_device_buf return 0; } -std::shared_ptr make_hwdevice(int width, int height) { - auto egl = std::make_shared(); +static bool query(display_t::pointer display, profile_e profile) { + std::vector 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 make_hwdevice(int width, int height) { auto render_device = config::video.adapter_name.empty() ? "/dev/dri/renderD128" : config::video.adapter_name.c_str(); - if(egl->init(width, height, render_device)) { + + 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; + } + + auto egl = std::make_shared(); + if(egl->init(width, height, std::move(file))) { return nullptr; } return egl; } -} // namespace egl -std::unique_ptr init() { - gbm::init(); - va::init_drm(); - - if(!gladLoaderLoadEGL(EGL_NO_DISPLAY) || !eglGetPlatformDisplay) { - BOOST_LOG(warning) << "Couldn't load EGL library"sv; +std::shared_ptr make_hwdevice(int width, int height, file_t &&card, int offset_x, int offset_y, const egl::surface_descriptor_t &sd) { + auto egl = std::make_shared(); + if(egl->init(width, height, std::move(card), offset_x, offset_y, sd)) { + return nullptr; } - return std::make_unique(); + return egl; } -} // namespace platf \ No newline at end of file +} // namespace va \ No newline at end of file diff --git a/sunshine/platform/linux/vaapi.h b/sunshine/platform/linux/vaapi.h index 10b1b3f1..a7165166 100644 --- a/sunshine/platform/linux/vaapi.h +++ b/sunshine/platform/linux/vaapi.h @@ -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 make_hwdevice(int width, int height); -} // namespace platf::egl + +namespace egl { +struct surface_descriptor_t; +} +namespace va { +std::shared_ptr make_hwdevice(int width, int height); +std::shared_ptr 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 \ No newline at end of file diff --git a/sunshine/platform/linux/display.cpp b/sunshine/platform/linux/x11grab.cpp similarity index 52% rename from sunshine/platform/linux/display.cpp rename to sunshine/platform/linux/x11grab.cpp index 133e5905..8d4f2b4f 100644 --- a/sunshine/platform/linux/display.cpp +++ b/sunshine/platform/linux/x11grab.cpp @@ -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> 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> 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> 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> 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> 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; +using xcb_connect_t = util::dyn_safe_ptr; using xcb_img_t = util::c_ptr; -using xdisplay_t = util::safe_ptr_v2; +using xdisplay_t = util::dyn_safe_ptr_v2; using ximg_t = util::safe_ptr; using xcursor_t = util::safe_ptr; -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, 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 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(); @@ -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(platf::mem_type_e hwdevice_type, const std::string &output_name, int framerate) { +std::shared_ptr 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(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(platf::mem_type_e hwdevice_type, const std::s // Fallback auto x11_disp = std::make_shared(hwdevice_type); - if(x11_disp->init(framerate, output_name)) { + if(x11_disp->init(display_name, framerate)) { return nullptr; } return x11_disp; } -std::vector display_names() { +std::vector 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::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 diff --git a/sunshine/platform/linux/x11grab.h b/sunshine/platform/linux/x11grab.h new file mode 100644 index 00000000..1440ae76 --- /dev/null +++ b/sunshine/platform/linux/x11grab.h @@ -0,0 +1,48 @@ +#ifndef SUNSHINE_X11_GRAB +#define SUNSHINE_X11_GRAB + +#include + +#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; + +class cursor_t { +public: + static std::optional 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 make() { return std::nullopt; } + + void capture(egl::cursor_t &) {} + void blend(img_t &, int, int) {} +}; +#endif +} // namespace platf::x11 + +#endif \ No newline at end of file diff --git a/sunshine/utility.h b/sunshine/utility.h index 4740ae6a..af2da53e 100644 --- a/sunshine/utility.h +++ b/sunshine/utility.h @@ -389,6 +389,10 @@ auto enm(T &val) -> std::underlying_type_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 &y) { return (bool)y; } +template +using shared_t = std::shared_ptr; + +template +shared_t

make_shared(T *pointer) { + return shared_t

(reinterpret_cast(pointer), typename P::deleter_type()); +} + template class wrap_ptr { public: diff --git a/sunshine/video.cpp b/sunshine/video.cpp index 8039eac8..261d86bc 100644 --- a/sunshine/video.cpp +++ b/sunshine/video.cpp @@ -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("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("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(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> 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::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];