feat(linux): Add Vulkan video encoder (#4603)

This commit is contained in:
neatnoise
2026-03-31 04:17:00 +02:00
committed by GitHub
parent 356c6a2c3f
commit 0752f641b1
29 changed files with 1566 additions and 28 deletions
+3
View File
@@ -113,6 +113,9 @@ jobs:
devel/pkgconf \
ftp/curl \
graphics/libdrm \
graphics/shaderc \
graphics/vulkan-headers \
graphics/vulkan-loader \
graphics/wayland \
lang/python314 \
multimedia/libva \
+67
View File
@@ -120,6 +120,73 @@ if(LIBVA_FOUND)
"${CMAKE_SOURCE_DIR}/src/platform/linux/vaapi.cpp")
endif()
# vulkan video encoding (via FFmpeg)
if(${SUNSHINE_ENABLE_VULKAN})
# use Vulkan headers from build-deps submodule (system headers may be too old, e.g. Ubuntu 22.04)
set(VULKAN_HEADERS_DIR "${CMAKE_SOURCE_DIR}/third-party/build-deps/third-party/FFmpeg/Vulkan-Headers/include")
if(NOT EXISTS "${VULKAN_HEADERS_DIR}/vulkan/vulkan.h")
message(FATAL_ERROR "Vulkan headers not found in build-deps submodule")
endif()
find_library(VULKAN_LIBRARY NAMES vulkan vulkan-1)
if(NOT VULKAN_LIBRARY)
message(FATAL_ERROR "libvulkan not found")
endif()
# prefer glslc, fall back to glslangValidator
find_program(GLSLC_EXECUTABLE glslc)
if(NOT GLSLC_EXECUTABLE)
find_program(GLSLANG_EXECUTABLE glslangValidator)
endif()
if(NOT GLSLC_EXECUTABLE AND NOT GLSLANG_EXECUTABLE)
message(FATAL_ERROR "Vulkan shader compiler not found (need glslc or glslangValidator)")
endif()
list(APPEND SUNSHINE_DEFINITIONS SUNSHINE_BUILD_VULKAN=1)
include_directories(SYSTEM ${VULKAN_HEADERS_DIR})
list(APPEND PLATFORM_LIBRARIES ${VULKAN_LIBRARY})
list(APPEND PLATFORM_TARGET_FILES
"${CMAKE_SOURCE_DIR}/src/platform/linux/vulkan_encode.h"
"${CMAKE_SOURCE_DIR}/src/platform/linux/vulkan_encode.cpp")
# compile GLSL -> SPIR-V -> C include at build time
set(VULKAN_SHADER_DIR "${CMAKE_BINARY_DIR}/generated-src/shaders")
set(VULKAN_SHADER_SOURCE "${SUNSHINE_SOURCE_ASSETS_DIR}/linux/assets/shaders/vulkan/rgb2yuv.comp")
set(VULKAN_SHADER_SPV "${VULKAN_SHADER_DIR}/rgb2yuv.spv")
set(VULKAN_SHADER_DATA "${VULKAN_SHADER_DIR}/rgb2yuv.spv.inc")
file(MAKE_DIRECTORY "${VULKAN_SHADER_DIR}")
if(GLSLC_EXECUTABLE)
add_custom_command(
OUTPUT "${VULKAN_SHADER_SPV}"
COMMAND ${GLSLC_EXECUTABLE} -O "${VULKAN_SHADER_SOURCE}" -o "${VULKAN_SHADER_SPV}"
DEPENDS "${VULKAN_SHADER_SOURCE}"
COMMENT "Compiling Vulkan shader rgb2yuv.comp (glslc)"
VERBATIM)
else()
add_custom_command(
OUTPUT "${VULKAN_SHADER_SPV}"
COMMAND ${GLSLANG_EXECUTABLE} -V -o "${VULKAN_SHADER_SPV}" "${VULKAN_SHADER_SOURCE}"
DEPENDS "${VULKAN_SHADER_SOURCE}"
COMMENT "Compiling Vulkan shader rgb2yuv.comp (glslangValidator)"
VERBATIM)
endif()
add_custom_command(
OUTPUT "${VULKAN_SHADER_DATA}"
COMMAND ${CMAKE_COMMAND} -DSPV_FILE=${VULKAN_SHADER_SPV} -DOUT_FILE=${VULKAN_SHADER_DATA}
-P "${CMAKE_SOURCE_DIR}/cmake/scripts/binary_to_c.cmake"
DEPENDS "${VULKAN_SHADER_SPV}"
COMMENT "Generating C include from rgb2yuv.spv"
VERBATIM)
add_custom_target(vulkan_shaders
DEPENDS "${VULKAN_SHADER_DATA}"
COMMENT "Vulkan shader compilation")
set(SUNSHINE_TARGET_DEPENDENCIES ${SUNSHINE_TARGET_DEPENDENCIES} vulkan_shaders)
endif()
# wayland
if(${SUNSHINE_ENABLE_WAYLAND})
find_package(Wayland REQUIRED)
+2
View File
@@ -58,6 +58,8 @@ elseif(UNIX) # Linux
"Enable KMS grab if available." ON)
option(SUNSHINE_ENABLE_VAAPI
"Enable building vaapi specific code." ON)
option(SUNSHINE_ENABLE_VULKAN
"Enable Vulkan video encoding." ON)
option(SUNSHINE_ENABLE_WAYLAND
"Enable building wayland specific code." ON)
option(SUNSHINE_ENABLE_X11
+35
View File
@@ -0,0 +1,35 @@
# binary_to_c.cmake - Convert a binary file to a C uint32_t initializer list.
# Input: SPV_FILE - path to SPIR-V binary
# Output: OUT_FILE - path to write C initializer (e.g. {0x07230203, ...})
file(READ "${SPV_FILE}" data HEX)
string(LENGTH "${data}" hex_len)
math(EXPR num_bytes "${hex_len} / 2")
math(EXPR num_words "${num_bytes} / 4")
math(EXPR last "${num_words} - 1")
set(_out "{")
set(_idx 0)
while(_idx LESS_EQUAL last)
math(EXPR off "${_idx} * 8")
math(EXPR off1 "${off} + 2")
math(EXPR off2 "${off} + 4")
math(EXPR off3 "${off} + 6")
string(SUBSTRING "${data}" ${off} 2 b0)
string(SUBSTRING "${data}" ${off1} 2 b1)
string(SUBSTRING "${data}" ${off2} 2 b2)
string(SUBSTRING "${data}" ${off3} 2 b3)
# little-endian to uint32_t
string(APPEND _out "0x${b3}${b2}${b1}${b0}")
if(NOT _idx EQUAL last)
string(APPEND _out ",")
endif()
math(EXPR _col "(${_idx} + 1) % 8")
if(_col EQUAL 0)
string(APPEND _out "\n")
endif()
math(EXPR _idx "${_idx} + 1")
endwhile()
string(APPEND _out "}\n")
file(WRITE "${OUT_FILE}" "${_out}")
+100
View File
@@ -2155,6 +2155,11 @@ editing the `conf` file in a text editor. Use the examples as reference.
<td>vaapi</td>
<td>Use VA-API (AMD, Intel)</td>
</tr>
<tr>
<td>vulkan</td>
<td>Use Vulkan encoder (AMD, Intel, NVIDIA).
@note{Applies to Linux only.}</td>
</tr>
<tr>
<td>software</td>
<td>Encoding occurs on the CPU</td>
@@ -2920,6 +2925,101 @@ editing the `conf` file in a text editor. Use the examples as reference.
</tr>
</table>
## Vulkan Encoder
### vk_tune
<table>
<tr>
<td>Description</td>
<td colspan="2">
Encoder tuning preset. Low latency modes reduce encoding delay at the cost of quality.
@note{This option only applies when using Vulkan [encoder](#encoder).}
</td>
</tr>
<tr>
<td>Default</td>
<td colspan="2">@code{}
2
@endcode</td>
</tr>
<tr>
<td>Example</td>
<td colspan="2">@code{}
vk_tune = 1
@endcode</td>
</tr>
<tr>
<td>Options</td>
<td>0 (default)</td>
<td>Let the driver decide</td>
</tr>
<tr>
<td></td>
<td>1 (hq)</td>
<td>High Quality</td>
</tr>
<tr>
<td></td>
<td>2 (ll)</td>
<td>Low Latency</td>
</tr>
<tr>
<td></td>
<td>3 (ull)</td>
<td>Ultra Low Latency</td>
</tr>
<tr>
<td></td>
<td>4 (lossless)</td>
<td>Lossless</td>
</tr>
</table>
### vk_rc_mode
<table>
<tr>
<td>Description</td>
<td colspan="2">
Rate control mode for encoding. Auto lets the driver decide.
@note{This option only applies when using Vulkan [encoder](#encoder).}
</td>
</tr>
<tr>
<td>Default</td>
<td colspan="2">@code{}
4
@endcode</td>
</tr>
<tr>
<td>Example</td>
<td colspan="2">@code{}
vk_rc_mode = 2
@endcode</td>
</tr>
<tr>
<td>Options</td>
<td>0</td>
<td>Auto (driver decides)</td>
</tr>
<tr>
<td></td>
<td>1</td>
<td>CQP (Constant QP)</td>
</tr>
<tr>
<td></td>
<td>2</td>
<td>CBR (Constant Bitrate)</td>
</tr>
<tr>
<td></td>
<td>4</td>
<td>VBR (Variable Bitrate)</td>
</tr>
</table>
## Software Encoder
### sw_preset
+2
View File
@@ -50,6 +50,7 @@ depends=(
'openssl'
'opus'
'udev'
'vulkan-icd-loader'
'which'
)
@@ -65,6 +66,7 @@ makedepends=(
'npm'
'python-jinja' # required by the glad OpenGL/EGL loader generator
'python-setuptools' # required for glad OpenGL/EGL loader generated, v2.0.0
'shaderc'
)
checkdepends=(
+17
View File
@@ -51,7 +51,9 @@ BuildRequires: which
# Fedora-specific BuildRequires
BuildRequires: appstream
# BuildRequires: boost-devel >= 1.86.0
BuildRequires: glslc
BuildRequires: libappstream-glib
BuildRequires: vulkan-loader-devel
%if 0%{fedora} > 43
# needed for npm from nvm
BuildRequires: libatomic
@@ -91,7 +93,13 @@ BuildRequires: npm
BuildRequires: python311
BuildRequires: python311-Jinja2
BuildRequires: python311-setuptools
%if !0%{?sle_version}
BuildRequires: shaderc
%endif
BuildRequires: udev
%if !0%{?sle_version}
BuildRequires: vulkan-devel
%endif
# for unit tests
BuildRequires: xvfb-run
%endif
@@ -157,6 +165,7 @@ Requires: libX11 >= 1.7.3.1
Requires: numactl-libs >= 2.0.14
Requires: openssl >= 3.0.2
Requires: pulseaudio-libs >= 10.0
Requires: vulkan-loader
%endif
%if 0%{?suse_version}
@@ -173,6 +182,9 @@ Requires: libX11-6
Requires: libnuma1
Requires: libopenssl3
Requires: libpulse0
%if !0%{?sle_version}
Requires: libvulkan1
%endif
%endif
%description
@@ -323,6 +335,11 @@ export BRANCH=%{branch}
export BUILD_VERSION=v%{build_version}
export COMMIT=%{commit}
# Disable Vulkan on openSUSE Leap (shaderc/glslang not in official repos)
%if 0%{?sle_version}
cmake_args+=("-DSUNSHINE_ENABLE_VULKAN=OFF")
%endif
# cmake
cd %{_builddir}/Sunshine
echo "cmake args:"
+2
View File
@@ -96,7 +96,9 @@ class Sunshine < Formula
depends_on "pango"
depends_on "pipewire"
depends_on "pulseaudio"
depends_on "shaderc"
depends_on "systemd"
depends_on "vulkan-loader"
depends_on "wayland"
# Jinja2 is required at build time by the glad OpenGL/EGL loader generator (Linux only).
+6
View File
@@ -194,7 +194,9 @@ function add_arch_deps() {
'opus'
'python-jinja' # glad OpenGL/EGL loader generator
'python-setuptools' # glad OpenGL/EGL loader generated, v2.0.0
'shaderc'
'udev'
'vulkan-icd-loader'
'wayland'
)
@@ -249,6 +251,8 @@ function add_debian_based_deps() {
"libxfixes-dev" # X11
"libxrandr-dev" # X11
"libxtst-dev" # X11
"libvulkan-dev" # Vulkan
"glslang-tools" # Vulkan shader compiler
"ninja-build"
"npm" # web-ui
"python3-jinja2" # glad OpenGL/EGL loader generator
@@ -332,6 +336,8 @@ function add_fedora_deps() {
"python3-jinja2" # glad OpenGL/EGL loader generator
"python3-setuptools" # glad OpenGL/EGL loader generated, v2.0.0
"rpm-build" # if you want to build an RPM binary package
"vulkan-loader-devel"
"glslc"
"wget" # necessary for cuda install with `run` file
"which" # necessary for cuda install with `run` file
"xorg-x11-server-Xvfb" # necessary for headless unit testing
+8
View File
@@ -486,6 +486,11 @@ namespace config {
false, // strict_rc_buffer
}, // vaapi
{
2, // vk.tune (default: ll - low latency)
4, // vk.rc_mode (default: vbr)
},
{}, // capture
{}, // encoder
{}, // adapter_name
@@ -1137,6 +1142,9 @@ namespace config {
bool_f(vars, "vaapi_strict_rc_buffer", video.vaapi.strict_rc_buffer);
int_f(vars, "vk_tune", video.vk.tune);
int_f(vars, "vk_rc_mode", video.vk.rc_mode);
string_f(vars, "capture", video.capture);
string_f(vars, "encoder", video.encoder);
string_f(vars, "adapter_name", video.adapter_name);
+5
View File
@@ -80,6 +80,11 @@ namespace config {
bool strict_rc_buffer;
} vaapi;
struct {
int tune; // 0=default, 1=hq, 2=ll, 3=ull, 4=lossless
int rc_mode; // 0=driver, 1=cqp, 2=cbr, 4=vbr
} vk;
std::string capture;
std::string encoder;
std::string adapter_name;
+1
View File
@@ -232,6 +232,7 @@ namespace platf {
dxgi, ///< DXGI
cuda, ///< CUDA
videotoolbox, ///< VideoToolbox
vulkan, ///< Vulkan
unknown ///< Unknown
};
+3
View File
@@ -305,6 +305,9 @@ namespace egl {
// Increment sequence when new rgb_t needs to be created
std::uint64_t sequence;
// Frame is vertically flipped (GL convention)
bool y_invert {false};
// PipeWire metadata
std::optional<uint64_t> pts;
std::optional<uint64_t> seq;
+9 -5
View File
@@ -27,6 +27,7 @@
#include "src/utility.h"
#include "src/video.h"
#include "vaapi.h"
#include "vulkan_encode.h"
#include "wayland.h"
using namespace std::literals;
@@ -960,10 +961,7 @@ namespace platf {
} else if (plane->fb_id != captured_cursor.fb_id) {
BOOST_LOG(debug) << "Refreshing cursor image after FB changed"sv;
cursor_dirty = true;
} else if (*prop_src_x != captured_cursor.prop_src_x ||
*prop_src_y != captured_cursor.prop_src_y ||
*prop_src_w != captured_cursor.prop_src_w ||
*prop_src_h != captured_cursor.prop_src_h) {
} else if (*prop_src_x != captured_cursor.prop_src_x || *prop_src_y != captured_cursor.prop_src_y || *prop_src_w != captured_cursor.prop_src_w || *prop_src_h != captured_cursor.prop_src_h) {
BOOST_LOG(debug) << "Refreshing cursor image after source dimensions changed"sv;
cursor_dirty = true;
}
@@ -1379,6 +1377,12 @@ namespace platf {
}
#endif
#ifdef SUNSHINE_BUILD_VULKAN
if (mem_type == mem_type_e::vulkan) {
return vk::make_avcodec_encode_device_vram(width, height, img_offset_x, img_offset_y);
}
#endif
#ifdef SUNSHINE_BUILD_CUDA
if (mem_type == mem_type_e::cuda) {
return cuda::make_avcodec_gl_encode_device(width, height, img_offset_x, img_offset_y);
@@ -1524,7 +1528,7 @@ namespace platf {
} // namespace kms
std::shared_ptr<display_t> kms_display(mem_type_e hwdevice_type, const std::string &display_name, const ::video::config_t &config) {
if (hwdevice_type == mem_type_e::vaapi || hwdevice_type == mem_type_e::cuda) {
if (hwdevice_type == mem_type_e::vaapi || hwdevice_type == mem_type_e::cuda || hwdevice_type == mem_type_e::vulkan) {
auto disp = std::make_shared<kms::display_vram_t>(hwdevice_type);
if (!disp->init(display_name, config)) {
+4 -2
View File
@@ -250,8 +250,7 @@ namespace platf {
if (!interface_name.empty()) {
// Find the AF_LINK entry for this interface to get MAC address
for (auto pos = ifaddrs.get(); pos != nullptr; pos = pos->ifa_next) {
if (pos->ifa_addr && pos->ifa_addr->sa_family == AF_LINK &&
interface_name == pos->ifa_name) {
if (pos->ifa_addr && pos->ifa_addr->sa_family == AF_LINK && interface_name == pos->ifa_name) {
auto sdl = (struct sockaddr_dl *) pos->ifa_addr;
auto mac = (unsigned char *) LLADDR(sdl);
@@ -1087,6 +1086,9 @@ namespace platf {
// https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/30039
set_env("AMD_DEBUG", "lowlatencyenc");
// enable Vulkan video extensions for AMD RADV
set_env("RADV_PERFTEST", "video_encode");
// These are allowed to fail.
gbm::init();
+11 -5
View File
@@ -29,6 +29,7 @@
#include "src/platform/common.h"
#include "src/video.h"
#include "vaapi.h"
#include "vulkan_encode.h"
#include "wayland.h"
#if !defined(__FreeBSD__)
@@ -877,6 +878,7 @@ namespace portal {
// On hybrid GPU systems (Intel+NVIDIA), DMA-BUFs come from the Intel GPU and cannot
// be imported into CUDA, so we fall back to memory buffers in that case.
bool use_dmabuf = n_dmabuf_infos > 0 && (mem_type == platf::mem_type_e::vaapi ||
mem_type == platf::mem_type_e::vulkan ||
(mem_type == platf::mem_type_e::cuda && display_is_nvidia));
if (use_dmabuf) {
for (int i = 0; i < n_dmabuf_infos; i++) {
@@ -1269,8 +1271,7 @@ namespace portal {
}
// Check previous logical dimensions
if (previous_width.load() == width &&
previous_height.load() == height) {
if (previous_width.load() == width && previous_height.load() == height) {
if (capture_running.load()) {
{
std::scoped_lock lock(pipewire.frame_mutex());
@@ -1283,8 +1284,7 @@ namespace portal {
previous_height.store(height);
}
if (negotiated_w > 0 && negotiated_h > 0 &&
(negotiated_w != width || negotiated_h != height)) {
if (negotiated_w > 0 && negotiated_h > 0 && (negotiated_w != width || negotiated_h != height)) {
BOOST_LOG(info) << "Using negotiated resolution "sv
<< negotiated_w << "x" << negotiated_h;
@@ -1429,6 +1429,12 @@ namespace portal {
}
#endif
#ifdef SUNSHINE_BUILD_VULKAN
if (mem_type == platf::mem_type_e::vulkan && n_dmabuf_infos > 0) {
return vk::make_avcodec_encode_device_vram(width, height, 0, 0);
}
#endif
#ifdef SUNSHINE_BUILD_CUDA
if (mem_type == platf::mem_type_e::cuda) {
if (display_is_nvidia && n_dmabuf_infos > 0) {
@@ -1612,7 +1618,7 @@ namespace portal {
namespace platf {
std::shared_ptr<display_t> portal_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config) {
using enum platf::mem_type_e;
if (hwdevice_type != system && hwdevice_type != vaapi && hwdevice_type != cuda) {
if (hwdevice_type != system && hwdevice_type != vaapi && hwdevice_type != cuda && hwdevice_type != vulkan) {
BOOST_LOG(error) << "Could not initialize display with the given hw device type."sv;
return nullptr;
}
+975
View File
@@ -0,0 +1,975 @@
/**
* @file src/platform/linux/vulkan_encode.cpp
* @brief Vulkan-native encoder: DMA-BUF -> Vulkan compute (RGB->YUV) -> Vulkan Video encode.
* No EGL/GL dependency — all GPU work stays in a single Vulkan queue.
*/
#include <array>
#include <cstdint>
#include <drm_fourcc.h>
#include <sys/stat.h>
#if defined(__FreeBSD__)
#include <sys/types.h>
#else
#include <sys/sysmacros.h>
#endif
#include <vector>
#include <vulkan/vulkan.h>
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavutil/hwcontext.h>
#include <libavutil/hwcontext_vulkan.h>
}
#include "graphics.h"
#include "src/config.h"
#include "src/logging.h"
#include "src/video_colorspace.h"
#include "vulkan_encode.h"
// SPIR-V data generated at build time
static const std::vector<uint32_t> rgb2yuv_comp_spv_data
#include "shaders/rgb2yuv.spv.inc"
;
static const size_t rgb2yuv_comp_spv_size = rgb2yuv_comp_spv_data.size() * sizeof(uint32_t);
using namespace std::literals;
namespace vk {
// Match a DRI render node path to a Vulkan device index via VK_EXT_physical_device_drm.
// Returns the index as a string (e.g. "1"), or empty string if no match.
static std::string find_vulkan_index_for_render_node(const char *render_path) {
struct stat node_stat;
if (stat(render_path, &node_stat) < 0) {
return {};
}
auto target_major = major(node_stat.st_rdev);
auto target_minor = minor(node_stat.st_rdev);
VkApplicationInfo app = {VK_STRUCTURE_TYPE_APPLICATION_INFO};
app.apiVersion = VK_API_VERSION_1_1;
VkInstanceCreateInfo ci = {VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO};
ci.pApplicationInfo = &app;
VkInstance inst = VK_NULL_HANDLE;
if (vkCreateInstance(&ci, nullptr, &inst) != VK_SUCCESS) {
return {};
}
uint32_t count = 0;
vkEnumeratePhysicalDevices(inst, &count, nullptr);
std::vector<VkPhysicalDevice> devs(count);
vkEnumeratePhysicalDevices(inst, &count, devs.data());
std::string result;
for (uint32_t i = 0; i < count; i++) {
VkPhysicalDeviceDrmPropertiesEXT drm = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRM_PROPERTIES_EXT};
VkPhysicalDeviceProperties2 props2 = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2};
props2.pNext = &drm;
vkGetPhysicalDeviceProperties2(devs[i], &props2);
if (drm.hasRender && drm.renderMajor == (int64_t) target_major && drm.renderMinor == (int64_t) target_minor) {
result = std::to_string(i);
break;
}
}
vkDestroyInstance(inst, nullptr);
return result;
}
static int create_vulkan_hwdevice(AVBufferRef **hw_device_buf) {
// Resolve render device path to Vulkan device index
if (auto render_path = config::video.adapter_name.empty() ? "/dev/dri/renderD128" : config::video.adapter_name; render_path[0] == '/') {
if (auto idx = find_vulkan_index_for_render_node(render_path.c_str()); !idx.empty() && av_hwdevice_ctx_create(hw_device_buf, AV_HWDEVICE_TYPE_VULKAN, idx.c_str(), nullptr, 0) >= 0) {
return 0;
}
} else {
// Non-path: treat as device name substring or numeric index
if (av_hwdevice_ctx_create(hw_device_buf, AV_HWDEVICE_TYPE_VULKAN, render_path.c_str(), nullptr, 0) >= 0) {
return 0;
}
}
// Final fallback: let FFmpeg pick default
if (av_hwdevice_ctx_create(hw_device_buf, AV_HWDEVICE_TYPE_VULKAN, nullptr, nullptr, 0) >= 0) {
return 0;
}
return -1;
}
struct PushConstants {
std::array<float, 4> color_vec_y;
std::array<float, 4> color_vec_u;
std::array<float, 4> color_vec_v;
std::array<float, 2> range_y;
std::array<float, 2> range_uv;
std::array<int32_t, 2> src_offset;
std::array<int32_t, 2> src_size;
std::array<int32_t, 2> dst_size;
std::array<int32_t, 2> cursor_pos;
std::array<int32_t, 2> cursor_size;
int32_t y_invert;
};
// Helper to check VkResult
#define VK_CHECK(expr) \
do { \
VkResult _r = (expr); \
if (_r != VK_SUCCESS) { \
BOOST_LOG(error) << #expr << " failed: " << _r; \
return -1; \
} \
} while (0)
#define VK_CHECK_BOOL(expr) \
do { \
VkResult _r = (expr); \
if (_r != VK_SUCCESS) { \
BOOST_LOG(error) << #expr << " failed: " << _r; \
return false; \
} \
} while (0)
class vk_vram_t: public platf::avcodec_encode_device_t {
public:
~vk_vram_t() override {
cleanup_pipeline();
}
int init(int in_width, int in_height, int in_offset_x = 0, int in_offset_y = 0) {
width = in_width;
height = in_height;
offset_x = in_offset_x;
offset_y = in_offset_y;
this->data = (void *) &init_hw_device;
return 0;
}
void init_codec_options(AVCodecContext *ctx, AVDictionary **options) override {
// When VBR mode is selected (rc_mode=4), don't pin rc_min_rate to the target bitrate.
// Having rc_min_rate == rc_max_rate == bit_rate in VBR mode prevents the encoder from
// undershooting on simple frames, which builds up headroom that causes large overshoots
// on complex frames.
if (config::video.vk.rc_mode == 4) {
ctx->rc_min_rate = 0;
}
}
int set_frame(AVFrame *new_frame, AVBufferRef *hw_frames_ctx_buf) override {
this->hwframe.reset(new_frame);
this->frame = new_frame;
this->hw_frames_ctx = hw_frames_ctx_buf;
auto *frames_ctx = (AVHWFramesContext *) hw_frames_ctx_buf->data;
auto *dev_ctx = (AVHWDeviceContext *) frames_ctx->device_ref->data;
vk_dev.ctx = (AVVulkanDeviceContext *) dev_ctx->hwctx;
vk_dev.dev = vk_dev.ctx->act_dev;
vk_dev.phys_dev = vk_dev.ctx->phys_dev;
is_10bit = (frames_ctx->sw_format == AV_PIX_FMT_P010);
{
VkPhysicalDeviceProperties p;
vkGetPhysicalDeviceProperties(vk_dev.phys_dev, &p);
BOOST_LOG(info) << "Vulkan encode using GPU: " << p.deviceName;
}
// Find a compute-capable queue family from FFmpeg's context
vk_dev.compute_qf = -1;
for (int i = 0; i < vk_dev.ctx->nb_qf; i++) {
if (vk_dev.ctx->qf[i].flags & VK_QUEUE_COMPUTE_BIT) {
vk_dev.compute_qf = vk_dev.ctx->qf[i].idx;
break;
}
}
if (vk_dev.compute_qf < 0) {
BOOST_LOG(error) << "No compute queue family in Vulkan device"sv;
return -1;
}
vkGetDeviceQueue(vk_dev.dev, vk_dev.compute_qf, 0, &vk_dev.compute_queue);
// Load extension functions
vk_dev.getMemoryFdProperties = (PFN_vkGetMemoryFdPropertiesKHR)
vkGetDeviceProcAddr(vk_dev.dev, "vkGetMemoryFdPropertiesKHR");
if (!create_compute_pipeline()) {
return -1;
}
if (!create_command_resources()) {
return -1;
}
return 0;
}
void apply_colorspace() override {
auto *colors = video::color_vectors_from_colorspace(colorspace, true);
if (colors) {
memcpy(push.color_vec_y.data(), colors->color_vec_y, sizeof(push.color_vec_y));
memcpy(push.color_vec_u.data(), colors->color_vec_u, sizeof(push.color_vec_u));
memcpy(push.color_vec_v.data(), colors->color_vec_v, sizeof(push.color_vec_v));
memcpy(push.range_y.data(), colors->range_y, sizeof(push.range_y));
memcpy(push.range_uv.data(), colors->range_uv, sizeof(push.range_uv));
}
}
void init_hwframes(AVHWFramesContext *frames) override {
frames->initial_pool_size = 4;
auto *vk_frames = (AVVulkanFramesContext *) frames->hwctx;
vk_frames->tiling = VK_IMAGE_TILING_OPTIMAL;
vk_frames->usage = (VkImageUsageFlagBits) (VK_IMAGE_USAGE_STORAGE_BIT |
VK_IMAGE_USAGE_TRANSFER_DST_BIT |
VK_IMAGE_USAGE_SAMPLED_BIT);
}
int convert(platf::img_t &img) override {
auto &descriptor = (egl::img_descriptor_t &) img;
// Get encoder target frame
if (!frame->buf[0]) {
if (av_hwframe_get_buffer(hw_frames_ctx, frame, 0) < 0) {
BOOST_LOG(error) << "Failed to get Vulkan frame"sv;
return -1;
}
}
// Import new DMA-BUF as VkImage when capture sequence changes
if (descriptor.sequence == 0) {
// Dummy frame — clear the target
return 0;
}
if (descriptor.sequence > sequence) {
sequence = descriptor.sequence;
if (!import_dmabuf(descriptor.sd)) {
BOOST_LOG(error) << "Failed to import DMA-BUF"sv;
return -1;
}
descriptors_dirty = true;
}
if (src.image == VK_NULL_HANDLE) {
return -1;
}
// Setup Y/UV image views for the encoder target (once)
if (!target.views_created) {
if (!create_target_views()) {
return -1;
}
target.views_created = true;
descriptors_dirty = true;
}
// Update descriptor set only when source or target changed
if (descriptors_dirty) {
update_descriptors();
descriptors_dirty = false;
}
if (descriptor.data && descriptor.serial != cursor_serial) {
cursor_serial = descriptor.serial;
if (!create_cursor_image(descriptor.src_w, descriptor.src_h, descriptor.data)) {
return -1;
}
update_descriptors();
descriptors_dirty = false;
}
// Fill push constants
push.src_offset[0] = offset_x;
push.src_offset[1] = offset_y;
push.src_size[0] = width;
push.src_size[1] = height;
push.dst_size[0] = frame->width;
push.dst_size[1] = frame->height;
push.y_invert = descriptor.y_invert ? 1 : 0;
if (descriptor.data) {
float scale_x = (float) frame->width / width;
float scale_y = (float) frame->height / height;
push.cursor_pos[0] = (int32_t) ((descriptor.x - offset_x) * scale_x);
push.cursor_pos[1] = (int32_t) ((descriptor.y - offset_y) * scale_y);
push.cursor_size[0] = (int32_t) (descriptor.width * scale_x);
push.cursor_size[1] = (int32_t) (descriptor.height * scale_y);
} else {
push.cursor_size[0] = 0;
}
// Record and submit compute dispatch
return dispatch_compute();
}
private:
bool create_compute_pipeline() {
// Shader module
VkShaderModuleCreateInfo shader_ci = {VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
shader_ci.codeSize = rgb2yuv_comp_spv_size;
shader_ci.pCode = rgb2yuv_comp_spv_data.data();
VK_CHECK_BOOL(vkCreateShaderModule(vk_dev.dev, &shader_ci, nullptr, &compute.shader_module));
// Descriptor set layout: binding 0=sampler, 1=Y storage, 2=UV storage, 3=cursor sampler
std::array<VkDescriptorSetLayoutBinding, 4> bindings = {};
bindings[0] = {0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr};
bindings[1] = {1, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr};
bindings[2] = {2, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr};
bindings[3] = {3, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr};
VkDescriptorSetLayoutCreateInfo ds_layout_ci = {VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO};
ds_layout_ci.bindingCount = bindings.size();
ds_layout_ci.pBindings = bindings.data();
VK_CHECK_BOOL(vkCreateDescriptorSetLayout(vk_dev.dev, &ds_layout_ci, nullptr, &compute.ds_layout));
// Push constant range
VkPushConstantRange pc_range = {VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(PushConstants)};
VkPipelineLayoutCreateInfo pl_ci = {VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO};
pl_ci.setLayoutCount = 1;
pl_ci.pSetLayouts = &compute.ds_layout;
pl_ci.pushConstantRangeCount = 1;
pl_ci.pPushConstantRanges = &pc_range;
VK_CHECK_BOOL(vkCreatePipelineLayout(vk_dev.dev, &pl_ci, nullptr, &compute.pipeline_layout));
// Compute pipeline
VkComputePipelineCreateInfo comp_ci = {VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO};
comp_ci.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO};
comp_ci.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT;
comp_ci.stage.module = compute.shader_module;
comp_ci.stage.pName = "main";
comp_ci.layout = compute.pipeline_layout;
VK_CHECK_BOOL(vkCreateComputePipelines(vk_dev.dev, VK_NULL_HANDLE, 1, &comp_ci, nullptr, &compute.pipeline));
// Descriptor pool
std::array<VkDescriptorPoolSize, 2> pool_sizes = {{
{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 2},
{VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 2},
}};
VkDescriptorPoolCreateInfo pool_ci = {VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO};
pool_ci.maxSets = 1;
pool_ci.poolSizeCount = pool_sizes.size();
pool_ci.pPoolSizes = pool_sizes.data();
VK_CHECK_BOOL(vkCreateDescriptorPool(vk_dev.dev, &pool_ci, nullptr, &compute.desc_pool));
VkDescriptorSetAllocateInfo alloc_info = {VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO};
alloc_info.descriptorPool = compute.desc_pool;
alloc_info.descriptorSetCount = 1;
alloc_info.pSetLayouts = &compute.ds_layout;
VK_CHECK_BOOL(vkAllocateDescriptorSets(vk_dev.dev, &alloc_info, &compute.desc_set));
// Sampler for source image
VkSamplerCreateInfo sampler_ci = {VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO};
sampler_ci.magFilter = VK_FILTER_LINEAR;
sampler_ci.minFilter = VK_FILTER_LINEAR;
sampler_ci.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
sampler_ci.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
VK_CHECK_BOOL(vkCreateSampler(vk_dev.dev, &sampler_ci, nullptr, &compute.sampler));
if (!create_cursor_image(1, 1, nullptr)) {
return false;
}
return true;
}
bool create_command_resources() {
VkCommandPoolCreateInfo pool_ci = {VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
pool_ci.queueFamilyIndex = vk_dev.compute_qf;
pool_ci.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
VK_CHECK_BOOL(vkCreateCommandPool(vk_dev.dev, &pool_ci, nullptr, &cmd.pool));
VkCommandBufferAllocateInfo alloc_ci = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO};
alloc_ci.commandPool = cmd.pool;
alloc_ci.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
alloc_ci.commandBufferCount = CMD_RING_SIZE;
VK_CHECK_BOOL(vkAllocateCommandBuffers(vk_dev.dev, &alloc_ci, cmd.ring.data()));
return true;
}
static VkFormat drm_fourcc_to_vk_format(uint32_t fourcc) {
switch (fourcc) {
case DRM_FORMAT_XRGB8888:
case DRM_FORMAT_ARGB8888:
return VK_FORMAT_B8G8R8A8_UNORM;
case DRM_FORMAT_XBGR8888:
case DRM_FORMAT_ABGR8888:
return VK_FORMAT_R8G8B8A8_UNORM;
case DRM_FORMAT_XRGB2101010:
case DRM_FORMAT_ARGB2101010:
return VK_FORMAT_A2R10G10B10_UNORM_PACK32;
case DRM_FORMAT_XBGR2101010:
case DRM_FORMAT_ABGR2101010:
return VK_FORMAT_A2B10G10R10_UNORM_PACK32;
default:
BOOST_LOG(warning) << "Unknown DRM fourcc 0x" << std::hex << fourcc << std::dec << ", assuming B8G8R8A8";
return VK_FORMAT_B8G8R8A8_UNORM;
}
}
bool import_dmabuf(const egl::surface_descriptor_t &sd) {
destroy_src_image();
int fd = dup(sd.fds[0]);
if (fd < 0) {
return false;
}
// Query memory requirements for this DMA-BUF
VkMemoryFdPropertiesKHR fd_props = {VK_STRUCTURE_TYPE_MEMORY_FD_PROPERTIES_KHR};
if (vk_dev.getMemoryFdProperties) {
vk_dev.getMemoryFdProperties(vk_dev.dev, VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT, fd, &fd_props);
}
// Create VkImage for the DMA-BUF
VkExternalMemoryImageCreateInfo ext_ci = {VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO};
ext_ci.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT;
std::array<VkSubresourceLayout, 4> drm_layouts = {};
VkImageDrmFormatModifierExplicitCreateInfoEXT drm_ci = {
VK_STRUCTURE_TYPE_IMAGE_DRM_FORMAT_MODIFIER_EXPLICIT_CREATE_INFO_EXT
};
VkImageTiling tiling;
if (sd.modifier != DRM_FORMAT_MOD_INVALID) {
int plane_count = 0;
for (int i = 0; i < 4 && sd.fds[i] >= 0; ++i) {
drm_layouts[i].offset = sd.offsets[i];
drm_layouts[i].rowPitch = sd.pitches[i];
plane_count++;
}
drm_ci.drmFormatModifier = sd.modifier;
drm_ci.drmFormatModifierPlaneCount = plane_count;
drm_ci.pPlaneLayouts = drm_layouts.data();
ext_ci.pNext = &drm_ci;
tiling = VK_IMAGE_TILING_DRM_FORMAT_MODIFIER_EXT;
} else {
tiling = VK_IMAGE_TILING_LINEAR;
}
auto vk_format = drm_fourcc_to_vk_format(sd.fourcc);
VkImageCreateInfo img_ci = {VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
img_ci.pNext = &ext_ci;
img_ci.imageType = VK_IMAGE_TYPE_2D;
img_ci.format = vk_format;
img_ci.extent = {(uint32_t) sd.width, (uint32_t) sd.height, 1};
img_ci.mipLevels = 1;
img_ci.arrayLayers = 1;
img_ci.samples = VK_SAMPLE_COUNT_1_BIT;
img_ci.tiling = tiling;
img_ci.usage = VK_IMAGE_USAGE_SAMPLED_BIT;
img_ci.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
auto res = vkCreateImage(vk_dev.dev, &img_ci, nullptr, &src.image);
if (res != VK_SUCCESS) {
close(fd);
BOOST_LOG(error) << "vkCreateImage for DMA-BUF failed: " << res
<< " (modifier=0x" << std::hex << sd.modifier << std::dec
<< ", pitch=" << sd.pitches[0] << ", offset=" << sd.offsets[0] << ")";
return false;
}
// Bind imported DMA-BUF memory
VkMemoryRequirements mem_req;
vkGetImageMemoryRequirements(vk_dev.dev, src.image, &mem_req);
VkImportMemoryFdInfoKHR import_fd = {VK_STRUCTURE_TYPE_IMPORT_MEMORY_FD_INFO_KHR};
import_fd.handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT;
import_fd.fd = fd; // Vulkan takes ownership
VkMemoryAllocateInfo alloc_info = {VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
alloc_info.pNext = &import_fd;
alloc_info.allocationSize = mem_req.size;
alloc_info.memoryTypeIndex = find_memory_type(
fd_props.memoryTypeBits ? fd_props.memoryTypeBits : mem_req.memoryTypeBits,
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT
);
VkDeviceMemory src_mem = VK_NULL_HANDLE;
res = vkAllocateMemory(vk_dev.dev, &alloc_info, nullptr, &src_mem);
if (res != VK_SUCCESS) {
BOOST_LOG(error) << "vkAllocateMemory for DMA-BUF failed: " << res;
vkDestroyImage(vk_dev.dev, src.image, nullptr);
src.image = VK_NULL_HANDLE;
return false;
}
vkBindImageMemory(vk_dev.dev, src.image, src_mem, 0);
// Create image view (Vulkan sampling always returns RGBA order regardless of memory layout)
VkImageViewCreateInfo view_ci = {VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
view_ci.image = src.image;
view_ci.viewType = VK_IMAGE_VIEW_TYPE_2D;
view_ci.format = vk_format;
view_ci.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
VK_CHECK_BOOL(vkCreateImageView(vk_dev.dev, &view_ci, nullptr, &src.view));
src.mem = src_mem;
return true;
}
bool create_cursor_image(int w, int h, const uint8_t *pixels) {
destroy_cursor_image();
VkImageCreateInfo img_ci = {VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
img_ci.imageType = VK_IMAGE_TYPE_2D;
img_ci.format = VK_FORMAT_B8G8R8A8_UNORM;
img_ci.extent = {(uint32_t) w, (uint32_t) h, 1};
img_ci.mipLevels = 1;
img_ci.arrayLayers = 1;
img_ci.samples = VK_SAMPLE_COUNT_1_BIT;
img_ci.tiling = VK_IMAGE_TILING_LINEAR;
img_ci.usage = VK_IMAGE_USAGE_SAMPLED_BIT;
img_ci.initialLayout = VK_IMAGE_LAYOUT_PREINITIALIZED;
VK_CHECK_BOOL(vkCreateImage(vk_dev.dev, &img_ci, nullptr, &cursor.image));
VkMemoryRequirements mem_req;
vkGetImageMemoryRequirements(vk_dev.dev, cursor.image, &mem_req);
VkMemoryAllocateInfo alloc = {VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
alloc.allocationSize = mem_req.size;
alloc.memoryTypeIndex = find_memory_type(mem_req.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
VK_CHECK_BOOL(vkAllocateMemory(vk_dev.dev, &alloc, nullptr, &cursor.mem));
VK_CHECK_BOOL(vkBindImageMemory(vk_dev.dev, cursor.image, cursor.mem, 0));
if (pixels) {
void *mapped;
VK_CHECK_BOOL(vkMapMemory(vk_dev.dev, cursor.mem, 0, VK_WHOLE_SIZE, 0, &mapped));
VkImageSubresource subres = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0};
VkSubresourceLayout layout;
vkGetImageSubresourceLayout(vk_dev.dev, cursor.image, &subres, &layout);
for (int y = 0; y < h; y++) {
memcpy((uint8_t *) mapped + layout.offset + y * layout.rowPitch, pixels + y * w * 4, w * 4);
}
vkUnmapMemory(vk_dev.dev, cursor.mem);
}
VkImageViewCreateInfo view_ci = {VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
view_ci.image = cursor.image;
view_ci.viewType = VK_IMAGE_VIEW_TYPE_2D;
view_ci.format = VK_FORMAT_B8G8R8A8_UNORM;
view_ci.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
VK_CHECK_BOOL(vkCreateImageView(vk_dev.dev, &view_ci, nullptr, &cursor.view));
cursor.needs_transition = true;
descriptors_dirty = true;
return true;
}
void destroy_cursor_image() {
if (cursor.view) {
vkDestroyImageView(vk_dev.dev, cursor.view, nullptr);
cursor.view = VK_NULL_HANDLE;
}
if (cursor.image) {
vkDestroyImage(vk_dev.dev, cursor.image, nullptr);
cursor.image = VK_NULL_HANDLE;
}
if (cursor.mem) {
vkFreeMemory(vk_dev.dev, cursor.mem, nullptr);
cursor.mem = VK_NULL_HANDLE;
}
}
bool create_target_views() {
auto *vk_frame = (AVVkFrame *) frame->data[0];
if (!vk_frame) {
return false;
}
auto y_fmt = is_10bit ? VK_FORMAT_R16_UNORM : VK_FORMAT_R8_UNORM;
auto uv_fmt = is_10bit ? VK_FORMAT_R16G16_UNORM : VK_FORMAT_R8G8_UNORM;
// Detect multiplane vs multi-image layout
int num_imgs = 0;
for (int i = 0; i < AV_NUM_DATA_POINTERS && vk_frame->img[i]; i++) {
num_imgs++;
}
if (num_imgs == 1) {
// Single multiplane image — create plane views
VkImageViewCreateInfo view_ci = {VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
view_ci.image = vk_frame->img[0];
view_ci.viewType = VK_IMAGE_VIEW_TYPE_2D;
// Y plane
view_ci.format = y_fmt;
view_ci.subresourceRange = {VK_IMAGE_ASPECT_PLANE_0_BIT, 0, 1, 0, 1};
VK_CHECK_BOOL(vkCreateImageView(vk_dev.dev, &view_ci, nullptr, &target.y_view));
// UV plane
view_ci.format = uv_fmt;
view_ci.subresourceRange = {VK_IMAGE_ASPECT_PLANE_1_BIT, 0, 1, 0, 1};
VK_CHECK_BOOL(vkCreateImageView(vk_dev.dev, &view_ci, nullptr, &target.uv_view));
} else {
// Separate images per plane
VkImageViewCreateInfo view_ci = {VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
view_ci.viewType = VK_IMAGE_VIEW_TYPE_2D;
view_ci.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
view_ci.image = vk_frame->img[0];
view_ci.format = y_fmt;
VK_CHECK_BOOL(vkCreateImageView(vk_dev.dev, &view_ci, nullptr, &target.y_view));
view_ci.image = vk_frame->img[1];
view_ci.format = uv_fmt;
VK_CHECK_BOOL(vkCreateImageView(vk_dev.dev, &view_ci, nullptr, &target.uv_view));
}
return true;
}
void update_descriptors() {
VkDescriptorImageInfo src_info = {compute.sampler, src.view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL};
VkDescriptorImageInfo y_info = {VK_NULL_HANDLE, target.y_view, VK_IMAGE_LAYOUT_GENERAL};
VkDescriptorImageInfo uv_info = {VK_NULL_HANDLE, target.uv_view, VK_IMAGE_LAYOUT_GENERAL};
VkDescriptorImageInfo cursor_info = {compute.sampler, cursor.view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL};
std::array<VkWriteDescriptorSet, 4> writes = {};
writes[0] = {VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, nullptr, compute.desc_set, 0, 0, 1, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, &src_info, nullptr, nullptr};
writes[1] = {VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, nullptr, compute.desc_set, 1, 0, 1, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, &y_info, nullptr, nullptr};
writes[2] = {VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, nullptr, compute.desc_set, 2, 0, 1, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, &uv_info, nullptr, nullptr};
writes[3] = {VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, nullptr, compute.desc_set, 3, 0, 1, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, &cursor_info, nullptr, nullptr};
vkUpdateDescriptorSets(vk_dev.dev, writes.size(), writes.data(), 0, nullptr);
}
int dispatch_compute() {
auto *vk_frame = (AVVkFrame *) frame->data[0];
int num_imgs = 0;
for (int i = 0; i < AV_NUM_DATA_POINTERS && vk_frame->img[i]; i++) {
num_imgs++;
}
// Rotate to next command buffer. With CMD_RING_SIZE slots, the buffer
// we're about to reuse was submitted CMD_RING_SIZE frames ago.
// At 60fps that's ~50ms for a <1ms compute dispatch — always complete.
// No fences, no semaphore waits, no CPU blocking.
auto cmd_buf = cmd.ring[cmd.ring_idx];
cmd.ring_idx = (cmd.ring_idx + 1) % CMD_RING_SIZE;
VkCommandBufferBeginInfo begin_ci = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
begin_ci.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
VK_CHECK(vkBeginCommandBuffer(cmd_buf, &begin_ci));
// Transition source image to SHADER_READ_ONLY
VkImageMemoryBarrier src_barrier = {VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
src_barrier.srcAccessMask = 0;
src_barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
src_barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
src_barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
src_barrier.image = src.image;
src_barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
src_barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_EXTERNAL;
src_barrier.dstQueueFamilyIndex = vk_dev.compute_qf;
vkCmdPipelineBarrier(cmd_buf, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &src_barrier);
// Transition cursor image if needed
if (cursor.needs_transition) {
VkImageMemoryBarrier cursor_barrier = {VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
cursor_barrier.srcAccessMask = VK_ACCESS_HOST_WRITE_BIT;
cursor_barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
cursor_barrier.oldLayout = VK_IMAGE_LAYOUT_PREINITIALIZED;
cursor_barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
cursor_barrier.image = cursor.image;
cursor_barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
cursor_barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
cursor_barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
vkCmdPipelineBarrier(cmd_buf, VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &cursor_barrier);
cursor.needs_transition = false;
}
// Transition target planes to GENERAL for storage writes
std::array<VkImageMemoryBarrier, 2> dst_barriers = {};
int num_dst_barriers = (num_imgs == 1) ? 1 : 2;
for (int i = 0; i < num_dst_barriers; i++) {
dst_barriers[i] = {VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
dst_barriers[i].srcAccessMask = target.initialized ? VK_ACCESS_SHADER_READ_BIT : 0;
dst_barriers[i].dstAccessMask = VK_ACCESS_SHADER_WRITE_BIT;
dst_barriers[i].oldLayout = target.initialized ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_UNDEFINED;
dst_barriers[i].newLayout = VK_IMAGE_LAYOUT_GENERAL;
dst_barriers[i].image = vk_frame->img[num_imgs == 1 ? 0 : i];
dst_barriers[i].subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
dst_barriers[i].srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
dst_barriers[i].dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
}
vkCmdPipelineBarrier(cmd_buf, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 0, nullptr, 0, nullptr, num_dst_barriers, dst_barriers.data());
// Bind pipeline and dispatch
vkCmdBindPipeline(cmd_buf, VK_PIPELINE_BIND_POINT_COMPUTE, compute.pipeline);
vkCmdBindDescriptorSets(cmd_buf, VK_PIPELINE_BIND_POINT_COMPUTE, compute.pipeline_layout, 0, 1, &compute.desc_set, 0, nullptr);
vkCmdPushConstants(cmd_buf, compute.pipeline_layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(PushConstants), &push);
uint32_t gx = (frame->width + 15) / 16;
uint32_t gy = (frame->height + 15) / 16;
vkCmdDispatch(cmd_buf, gx, gy, 1);
VK_CHECK(vkEndCommandBuffer(cmd_buf));
// Submit with timeline semaphore signaling for FFmpeg
VkTimelineSemaphoreSubmitInfo timeline_info = {VK_STRUCTURE_TYPE_TIMELINE_SEMAPHORE_SUBMIT_INFO};
std::array<VkSemaphore, AV_NUM_DATA_POINTERS> wait_sems = {};
std::array<VkSemaphore, AV_NUM_DATA_POINTERS> signal_sems = {};
std::array<uint64_t, AV_NUM_DATA_POINTERS> wait_vals = {};
std::array<uint64_t, AV_NUM_DATA_POINTERS> signal_vals = {};
std::array<VkPipelineStageFlags, AV_NUM_DATA_POINTERS> wait_stages = {};
int sem_count = 0;
for (int i = 0; i < AV_NUM_DATA_POINTERS && vk_frame->sem[i]; i++) {
wait_sems[sem_count] = vk_frame->sem[i];
wait_vals[sem_count] = vk_frame->sem_value[i];
wait_stages[sem_count] = VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
signal_sems[sem_count] = vk_frame->sem[i];
signal_vals[sem_count] = vk_frame->sem_value[i] + 1;
vk_frame->sem_value[i]++;
sem_count++;
}
timeline_info.waitSemaphoreValueCount = sem_count;
timeline_info.pWaitSemaphoreValues = wait_vals.data();
timeline_info.signalSemaphoreValueCount = sem_count;
timeline_info.pSignalSemaphoreValues = signal_vals.data();
VkSubmitInfo submit = {VK_STRUCTURE_TYPE_SUBMIT_INFO};
submit.pNext = &timeline_info;
submit.waitSemaphoreCount = sem_count;
submit.pWaitSemaphores = wait_sems.data();
submit.pWaitDstStageMask = wait_stages.data();
submit.commandBufferCount = 1;
submit.pCommandBuffers = &cmd_buf;
submit.signalSemaphoreCount = sem_count;
submit.pSignalSemaphores = signal_sems.data();
// Lock the queue (FFmpeg requires this)
vk_dev.ctx->lock_queue(
(AVHWDeviceContext *) ((AVHWFramesContext *) hw_frames_ctx->data)->device_ref->data,
vk_dev.compute_qf,
0
);
auto res = vkQueueSubmit(vk_dev.compute_queue, 1, &submit, VK_NULL_HANDLE);
vk_dev.ctx->unlock_queue(
(AVHWDeviceContext *) ((AVHWFramesContext *) hw_frames_ctx->data)->device_ref->data,
vk_dev.compute_qf,
0
);
if (res != VK_SUCCESS) {
BOOST_LOG(error) << "vkQueueSubmit failed: " << res;
return -1;
}
// Update frame layouts for FFmpeg
for (int i = 0; i < AV_NUM_DATA_POINTERS && vk_frame->img[i]; i++) {
vk_frame->layout[i] = VK_IMAGE_LAYOUT_GENERAL;
vk_frame->access[i] = VK_ACCESS_SHADER_WRITE_BIT;
}
target.initialized = true;
return 0;
}
uint32_t find_memory_type(uint32_t type_bits, VkMemoryPropertyFlags props) {
VkPhysicalDeviceMemoryProperties mem_props;
vkGetPhysicalDeviceMemoryProperties(vk_dev.phys_dev, &mem_props);
for (uint32_t i = 0; i < mem_props.memoryTypeCount; i++) {
if ((type_bits & (1 << i)) && (mem_props.memoryTypes[i].propertyFlags & props) == props) {
return i;
}
}
// Fallback: any matching type bit
for (uint32_t i = 0; i < mem_props.memoryTypeCount; i++) {
if (type_bits & (1 << i)) {
return i;
}
}
return 0;
}
void destroy_src_image() {
if (src.image) {
// Defer destruction — the GPU may still be using this image.
// By the time we wrap around (4 frames later), it's guaranteed done.
auto &slot = defer_ring[defer_idx];
if (slot.view) {
vkDestroyImageView(vk_dev.dev, slot.view, nullptr);
}
if (slot.image) {
vkDestroyImage(vk_dev.dev, slot.image, nullptr);
}
if (slot.mem) {
vkFreeMemory(vk_dev.dev, slot.mem, nullptr);
}
slot = src;
defer_idx = (defer_idx + 1) % DEFER_RING_SIZE;
}
src = {};
}
void cleanup_pipeline() {
if (!vk_dev.dev) {
return;
}
vkDeviceWaitIdle(vk_dev.dev);
destroy_src_image();
// Flush deferred destroys
for (auto &slot : defer_ring) {
if (slot.view) {
vkDestroyImageView(vk_dev.dev, slot.view, nullptr);
}
if (slot.image) {
vkDestroyImage(vk_dev.dev, slot.image, nullptr);
}
if (slot.mem) {
vkFreeMemory(vk_dev.dev, slot.mem, nullptr);
}
slot = {};
}
if (target.y_view) {
vkDestroyImageView(vk_dev.dev, target.y_view, nullptr);
}
if (target.uv_view) {
vkDestroyImageView(vk_dev.dev, target.uv_view, nullptr);
}
destroy_cursor_image();
if (cmd.pool) {
vkDestroyCommandPool(vk_dev.dev, cmd.pool, nullptr);
}
if (compute.sampler) {
vkDestroySampler(vk_dev.dev, compute.sampler, nullptr);
}
if (compute.desc_pool) {
vkDestroyDescriptorPool(vk_dev.dev, compute.desc_pool, nullptr);
}
if (compute.pipeline) {
vkDestroyPipeline(vk_dev.dev, compute.pipeline, nullptr);
}
if (compute.pipeline_layout) {
vkDestroyPipelineLayout(vk_dev.dev, compute.pipeline_layout, nullptr);
}
if (compute.ds_layout) {
vkDestroyDescriptorSetLayout(vk_dev.dev, compute.ds_layout, nullptr);
}
if (compute.shader_module) {
vkDestroyShaderModule(vk_dev.dev, compute.shader_module, nullptr);
}
}
static int init_hw_device(platf::avcodec_encode_device_t *, AVBufferRef **hw_device_buf) {
return create_vulkan_hwdevice(hw_device_buf);
}
// Dimensions
int width = 0;
int height = 0;
int offset_x = 0;
int offset_y = 0;
bool is_10bit = false;
AVBufferRef *hw_frames_ctx = nullptr;
frame_t hwframe;
std::uint64_t sequence = 0;
// Vulkan device (from FFmpeg)
struct vk_device_t {
VkDevice dev = VK_NULL_HANDLE;
VkPhysicalDevice phys_dev = VK_NULL_HANDLE;
AVVulkanDeviceContext *ctx = nullptr;
int compute_qf = -1;
VkQueue compute_queue = VK_NULL_HANDLE;
PFN_vkGetMemoryFdPropertiesKHR getMemoryFdProperties = nullptr;
};
vk_device_t vk_dev = {};
// Compute pipeline
struct compute_pipeline_t {
VkShaderModule shader_module = VK_NULL_HANDLE;
VkDescriptorSetLayout ds_layout = VK_NULL_HANDLE;
VkPipelineLayout pipeline_layout = VK_NULL_HANDLE;
VkPipeline pipeline = VK_NULL_HANDLE;
VkDescriptorPool desc_pool = VK_NULL_HANDLE;
VkDescriptorSet desc_set = VK_NULL_HANDLE;
VkSampler sampler = VK_NULL_HANDLE;
};
compute_pipeline_t compute = {};
// Command submission — ring of buffers to avoid reuse while in-flight.
// No CPU waits: by the time we wrap around, the old submission is long done.
static constexpr int CMD_RING_SIZE = 3;
struct cmd_submission_t {
VkCommandPool pool = VK_NULL_HANDLE;
std::array<VkCommandBuffer, CMD_RING_SIZE> ring = {};
int ring_idx = 0;
};
cmd_submission_t cmd = {};
// Source DMA-BUF image with deferred destruction
struct src_image_t {
VkImage image = VK_NULL_HANDLE;
VkDeviceMemory mem = VK_NULL_HANDLE;
VkImageView view = VK_NULL_HANDLE;
};
src_image_t src = {};
static constexpr int DEFER_RING_SIZE = 4;
std::array<src_image_t, DEFER_RING_SIZE> defer_ring = {};
int defer_idx = 0;
// Target NV12 plane views
struct target_state_t {
VkImageView y_view = VK_NULL_HANDLE;
VkImageView uv_view = VK_NULL_HANDLE;
bool views_created = false;
bool initialized = false;
};
target_state_t target = {};
bool descriptors_dirty = false;
// Cursor image
struct {
VkImage image = VK_NULL_HANDLE;
VkDeviceMemory mem = VK_NULL_HANDLE;
VkImageView view = VK_NULL_HANDLE;
bool needs_transition = false;
} cursor = {};
unsigned long cursor_serial = 0;
// Push constants (color matrix)
PushConstants push = {};
};
// Free functions
int vulkan_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *, AVBufferRef **hw_device_buf) {
return create_vulkan_hwdevice(hw_device_buf);
}
bool validate() {
if (!avcodec_find_encoder_by_name("h264_vulkan") && !avcodec_find_encoder_by_name("hevc_vulkan")) {
return false;
}
AVBufferRef *dev = nullptr;
if (create_vulkan_hwdevice(&dev) < 0) {
return false;
}
av_buffer_unref(&dev);
return true;
}
std::unique_ptr<platf::avcodec_encode_device_t> make_avcodec_encode_device_vram(int w, int h, int offset_x, int offset_y) {
auto dev = std::make_unique<vk_vram_t>();
if (dev->init(w, h, offset_x, offset_y) < 0) {
return nullptr;
}
return dev;
}
std::unique_ptr<platf::avcodec_encode_device_t> make_avcodec_encode_device_ram(int, int) {
return nullptr;
}
} // namespace vk
+36
View File
@@ -0,0 +1,36 @@
/**
* @file src/platform/linux/vulkan_encode.h
* @brief Declarations for FFmpeg Vulkan Video encoder.
*/
#pragma once
#include "src/platform/common.h"
extern "C" struct AVBufferRef;
namespace vk {
/**
* @brief Initialize Vulkan hardware device for FFmpeg encoding.
* @param encode_device The encode device (vk_t).
* @param hw_device_buf Output hardware device buffer.
* @return 0 on success, negative on error.
*/
int vulkan_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *encode_device, AVBufferRef **hw_device_buf);
/**
* @brief Create a Vulkan encode device for RAM capture.
*/
std::unique_ptr<platf::avcodec_encode_device_t> make_avcodec_encode_device_ram(int width, int height);
/**
* @brief Create a Vulkan encode device for VRAM capture.
*/
std::unique_ptr<platf::avcodec_encode_device_t> make_avcodec_encode_device_vram(int width, int height, int offset_x, int offset_y);
/**
* @brief Check if FFmpeg Vulkan Video encoding is available.
*/
bool validate();
} // namespace vk
+1 -1
View File
@@ -374,7 +374,7 @@ namespace wl {
}
// Create GBM buffer
current_bo = gbm_bo_create(gbm_device, dmabuf_info.width, dmabuf_info.height, dmabuf_info.format, GBM_BO_USE_RENDERING);
current_bo = gbm_bo_create(gbm_device, dmabuf_info.width, dmabuf_info.height, dmabuf_info.format, GBM_BO_USE_RENDERING | GBM_BO_USE_LINEAR);
if (!current_bo) {
BOOST_LOG(error) << "Failed to create GBM buffer"sv;
zwlr_screencopy_frame_v1_destroy(frame);
+1 -1
View File
@@ -69,6 +69,7 @@ namespace wl {
std::array<frame_t, 2> frames;
frame_t *current_frame;
zwlr_screencopy_frame_v1_listener listener;
bool y_invert {false};
private:
bool init_gbm();
@@ -95,7 +96,6 @@ namespace wl {
struct gbm_device *gbm_device {nullptr};
struct gbm_bo *current_bo {nullptr};
struct wl_buffer *current_wl_buffer {nullptr};
bool y_invert {false};
};
class monitor_t {
+10 -2
View File
@@ -11,6 +11,7 @@
#include "src/platform/common.h"
#include "src/video.h"
#include "vaapi.h"
#include "vulkan_encode.h"
#include "wayland.h"
using namespace std::literals;
@@ -348,6 +349,7 @@ namespace wl {
img->sd = current_frame->sd;
img->frame_timestamp = current_frame->frame_timestamp;
img->y_invert = dmabuf.y_invert;
// Prevent dmabuf from closing the file descriptors.
std::fill_n(current_frame->sd.fds, 4, -1);
@@ -377,6 +379,12 @@ namespace wl {
}
#endif
#ifdef SUNSHINE_BUILD_VULKAN
if (mem_type == platf::mem_type_e::vulkan) {
return vk::make_avcodec_encode_device_vram(width, height, 0, 0);
}
#endif
#ifdef SUNSHINE_BUILD_CUDA
if (mem_type == platf::mem_type_e::cuda) {
return cuda::make_avcodec_gl_encode_device(width, height, 0, 0);
@@ -398,12 +406,12 @@ namespace wl {
namespace platf {
std::shared_ptr<display_t> wl_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config) {
if (hwdevice_type != platf::mem_type_e::system && hwdevice_type != platf::mem_type_e::vaapi && hwdevice_type != platf::mem_type_e::cuda) {
if (hwdevice_type != platf::mem_type_e::system && hwdevice_type != platf::mem_type_e::vaapi && hwdevice_type != platf::mem_type_e::cuda && hwdevice_type != platf::mem_type_e::vulkan) {
BOOST_LOG(error) << "[wlgrab] Could not initialize display with the given hw device type."sv;
return nullptr;
}
if (hwdevice_type == platf::mem_type_e::vaapi || hwdevice_type == platf::mem_type_e::cuda) {
if (hwdevice_type == platf::mem_type_e::vaapi || hwdevice_type == platf::mem_type_e::cuda || hwdevice_type == platf::mem_type_e::vulkan) {
auto wlr = std::make_shared<wl::wlr_vram_t>();
if (wlr->init(hwdevice_type, display_name, config)) {
return nullptr;
+122 -10
View File
@@ -3,6 +3,7 @@
* @brief Definitions for video.
*/
// standard includes
#include <array>
#include <atomic>
#include <bitset>
#include <list>
@@ -122,6 +123,7 @@ namespace video {
util::Either<avcodec_buffer_t, int> vaapi_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *);
util::Either<avcodec_buffer_t, int> cuda_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *);
util::Either<avcodec_buffer_t, int> vt_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *);
util::Either<avcodec_buffer_t, int> vulkan_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *);
class avcodec_software_encode_device_t: public platf::avcodec_encode_device_t {
public:
@@ -1012,7 +1014,78 @@ namespace video {
// RC buffer size will be set in platform code if supported
LIMITED_GOP_SIZE | PARALLEL_ENCODING | NO_RC_BUF_LIMIT
};
#endif
#ifdef SUNSHINE_BUILD_VULKAN
encoder_t vulkan {
"vulkan"sv,
std::make_unique<encoder_platform_formats_avcodec>(
AV_HWDEVICE_TYPE_VULKAN,
AV_HWDEVICE_TYPE_NONE,
AV_PIX_FMT_VULKAN,
AV_PIX_FMT_NV12,
AV_PIX_FMT_P010,
AV_PIX_FMT_NONE,
AV_PIX_FMT_NONE,
vulkan_init_avcodec_hardware_input_buffer
),
{
// AV1
{
{"idr_interval"s, std::numeric_limits<int>::max()},
{"tune"s, &config::video.vk.tune},
{"rc_mode"s, &config::video.vk.rc_mode},
{"units"s, 0},
{"usage"s, "stream"s},
{"content"s, "rendered"s},
{"async_depth"s, 1},
},
{}, // SDR-specific options
{}, // HDR-specific options
{}, // YUV444 SDR-specific options
{}, // YUV444 HDR-specific options
{}, // Fallback options
"av1_vulkan"s,
},
{
// HEVC
{
{"idr_interval"s, std::numeric_limits<int>::max()},
{"tune"s, &config::video.vk.tune},
{"rc_mode"s, &config::video.vk.rc_mode},
{"units"s, 0},
{"usage"s, "stream"s},
{"content"s, "rendered"s},
{"async_depth"s, 1},
},
{}, // SDR-specific options
{}, // HDR-specific options
{}, // YUV444 SDR-specific options
{}, // YUV444 HDR-specific options
{}, // Fallback options
"hevc_vulkan"s,
},
{
// H.264
{
{"idr_interval"s, std::numeric_limits<int>::max()},
{"tune"s, &config::video.vk.tune},
{"rc_mode"s, &config::video.vk.rc_mode},
{"units"s, 0},
{"usage"s, "stream"s},
{"content"s, "rendered"s},
{"async_depth"s, 1},
},
{}, // SDR-specific options
{}, // HDR-specific options
{}, // YUV444 SDR-specific options
{}, // YUV444 HDR-specific options
{}, // Fallback options
"h264_vulkan"s,
},
LIMITED_GOP_SIZE | PARALLEL_ENCODING
};
#endif // SUNSHINE_BUILD_VULKAN
#endif // linux
#ifdef __APPLE__
encoder_t videotoolbox {
@@ -1092,6 +1165,9 @@ namespace video {
&mediafoundation,
#endif
#if defined(__linux__) || defined(linux) || defined(__linux) || defined(__FreeBSD__)
#ifdef SUNSHINE_BUILD_VULKAN
&vulkan,
#endif
&vaapi,
#endif
#ifdef __APPLE__
@@ -2677,9 +2753,7 @@ namespace video {
// Test 4:4:4 HDR first. If 4:4:4 is supported, 4:2:0 should also be supported.
config.chromaSamplingType = 1;
if ((encoder.flags & YUV444_SUPPORT) &&
disp->is_codec_supported(encoder_codec_name, config) &&
validate_config(disp, encoder, config) >= 0) {
if ((encoder.flags & YUV444_SUPPORT) && disp->is_codec_supported(encoder_codec_name, config) && validate_config(disp, encoder, config) >= 0) {
flag_map[encoder_t::DYNAMIC_RANGE] = true;
flag_map[encoder_t::YUV444] = true;
return;
@@ -2689,8 +2763,7 @@ namespace video {
// Test 4:2:0 HDR
config.chromaSamplingType = 0;
if (disp->is_codec_supported(encoder_codec_name, config) &&
validate_config(disp, encoder, config) >= 0) {
if (disp->is_codec_supported(encoder_codec_name, config) && validate_config(disp, encoder, config) >= 0) {
flag_map[encoder_t::DYNAMIC_RANGE] = true;
} else {
flag_map[encoder_t::DYNAMIC_RANGE] = false;
@@ -2798,15 +2871,13 @@ namespace video {
}
// Skip it if it doesn't support the specified codec at all
if ((active_hevc_mode >= 2 && !encoder->hevc[encoder_t::PASSED]) ||
(active_av1_mode >= 2 && !encoder->av1[encoder_t::PASSED])) {
if ((active_hevc_mode >= 2 && !encoder->hevc[encoder_t::PASSED]) || (active_av1_mode >= 2 && !encoder->av1[encoder_t::PASSED])) {
pos++;
continue;
}
// Skip it if it doesn't support HDR on the specified codec
if ((active_hevc_mode == 3 && !encoder->hevc[encoder_t::DYNAMIC_RANGE]) ||
(active_av1_mode == 3 && !encoder->av1[encoder_t::DYNAMIC_RANGE])) {
if ((active_hevc_mode == 3 && !encoder->hevc[encoder_t::DYNAMIC_RANGE]) || (active_av1_mode == 3 && !encoder->av1[encoder_t::DYNAMIC_RANGE])) {
pos++;
continue;
}
@@ -2935,6 +3006,43 @@ namespace video {
return hw_device_buf;
}
#ifdef SUNSHINE_BUILD_VULKAN
using vulkan_init_avcodec_hardware_input_buffer_fn = int (*)(platf::avcodec_encode_device_t *encode_device, AVBufferRef **hw_device_buf);
util::Either<avcodec_buffer_t, int> vulkan_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *encode_device) {
avcodec_buffer_t hw_device_buf;
if (encode_device && encode_device->data) {
if (((vulkan_init_avcodec_hardware_input_buffer_fn) encode_device->data)(encode_device, &hw_device_buf)) {
return -1;
}
return hw_device_buf;
}
// Try render device path first (like VAAPI does), then fallback to device indices
auto render_device = config::video.adapter_name.empty() ? "/dev/dri/renderD128" : config::video.adapter_name.c_str();
auto status = av_hwdevice_ctx_create(&hw_device_buf, AV_HWDEVICE_TYPE_VULKAN, render_device, nullptr, 0);
if (status >= 0) {
BOOST_LOG(info) << "Using Vulkan device: "sv << render_device;
return hw_device_buf;
}
// Fallback: try device indices for multi-GPU systems
const std::array<const char *, 4> devices = {"1", "0", "2", "3"};
for (auto device : devices) {
status = av_hwdevice_ctx_create(&hw_device_buf, AV_HWDEVICE_TYPE_VULKAN, device, nullptr, 0);
if (status >= 0) {
BOOST_LOG(info) << "Using Vulkan device index: "sv << device;
return hw_device_buf;
}
}
BOOST_LOG(error) << "Failed to create a Vulkan device"sv;
return -1;
}
#endif
util::Either<avcodec_buffer_t, int> cuda_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *encode_device) {
avcodec_buffer_t hw_device_buf;
@@ -3032,6 +3140,10 @@ namespace video {
return platf::mem_type_e::dxgi;
case AV_HWDEVICE_TYPE_VAAPI:
return platf::mem_type_e::vaapi;
#ifdef SUNSHINE_BUILD_VULKAN
case AV_HWDEVICE_TYPE_VULKAN:
return platf::mem_type_e::vulkan;
#endif
case AV_HWDEVICE_TYPE_CUDA:
return platf::mem_type_e::cuda;
case AV_HWDEVICE_TYPE_NONE:
+11 -2
View File
@@ -332,6 +332,14 @@
"vaapi_strict_rc_buffer": "disabled",
},
},
{
id: "vulkan",
name: "Vulkan Encoder",
options: {
"vk_tune": 2,
"vk_rc_mode": 4,
},
},
{
id: "sw",
name: "Software Encoder",
@@ -383,7 +391,7 @@
var app = document.getElementById("app");
if (this.platform === "windows") {
this.tabs = this.tabs.filter((el) => {
return el.id !== "vt" && el.id !== "vaapi";
return el.id !== "vt" && el.id !== "vaapi" && el.id !== "vulkan";
});
}
if (this.platform === "freebsd" || this.platform === "linux") {
@@ -393,7 +401,7 @@
}
if (this.platform === "macos") {
this.tabs = this.tabs.filter((el) => {
return el.id !== "amd" && el.id !== "nv" && el.id !== "qsv" && el.id !== "vaapi";
return el.id !== "amd" && el.id !== "nv" && el.id !== "qsv" && el.id !== "vaapi" && el.id !== "vulkan";
});
}
@@ -437,6 +445,7 @@
'qsv': 'Gpu',
'vaapi': 'Gpu',
'vt': 'Gpu',
'vulkan': 'Gpu',
'sw': 'Cpu',
};
return iconMap[tabId] || 'Settings';
@@ -97,11 +97,13 @@ const config = ref(props.config)
<option value="amdvce">AMD AMF/VCE</option>
</template>
<template #freebsd>
<option value="vulkan">Vulkan</option>
<option value="vaapi">VA-API</option>
</template>
<template #linux>
<option value="nvenc">NVIDIA NVENC</option>
<option value="vaapi">VA-API</option>
<option value="vulkan">Vulkan</option>
</template>
<template #macos>
<option value="videotoolbox">VideoToolbox</option>
@@ -6,6 +6,7 @@ import AmdAmfEncoder from './encoders/AmdAmfEncoder.vue'
import VideotoolboxEncoder from './encoders/VideotoolboxEncoder.vue'
import SoftwareEncoder from './encoders/SoftwareEncoder.vue'
import VAAPIEncoder from './encoders/VAAPIEncoder.vue'
import VulkanEncoder from './encoders/VulkanEncoder.vue'
const props = defineProps([
'platform',
@@ -53,6 +54,13 @@ const config = ref(props.config)
:config="config"
/>
<!-- Vulkan Encoder Tab -->
<VulkanEncoder
v-if="currentTab === 'vulkan'"
:platform="platform"
:config="config"
/>
<!-- Software Encoder Tab -->
<SoftwareEncoder
v-if="currentTab === 'sw'"
@@ -0,0 +1,38 @@
<script setup>
import { ref } from 'vue'
const props = defineProps([
'platform',
'config',
])
const config = ref(props.config)
</script>
<template>
<div id="vulkan-encoder" class="config-page">
<!-- Tuning -->
<div class="mb-3">
<label for="vk_tune" class="form-label">{{ $t('config.vk_tune') }}</label>
<select id="vk_tune" class="form-select" v-model="config.vk_tune">
<option value="0">{{ $t('_common.auto') }}</option>
<option value="1">{{ $t('config.vk_tune_hq') }}</option>
<option value="2">{{ $t('config.vk_tune_ll') }}</option>
<option value="3">{{ $t('config.vk_tune_ull') }}</option>
</select>
<div class="form-text">{{ $t('config.vk_tune_desc') }}</div>
</div>
<!-- Rate Control -->
<div class="mb-3">
<label for="vk_rc_mode" class="form-label">{{ $t('config.vk_rc_mode') }}</label>
<select id="vk_rc_mode" class="form-select" v-model="config.vk_rc_mode">
<option value="0">{{ $t('_common.auto') }}</option>
<option value="1">{{ $t('config.vk_rc_cqp') }}</option>
<option value="2">{{ $t('config.vk_rc_cbr') }}</option>
<option value="4">{{ $t('config.vk_rc_vbr') }}</option>
</select>
<div class="form-text">{{ $t('config.vk_rc_mode_desc') }}</div>
</div>
</div>
</template>
@@ -378,6 +378,16 @@
"upnp_desc": "Automatically configure port forwarding for streaming over the Internet",
"vaapi_strict_rc_buffer": "Strictly enforce frame bitrate limits for H.264/HEVC on AMD GPUs",
"vaapi_strict_rc_buffer_desc": "Enabling this option can avoid dropped frames over the network during scene changes, but video quality may be reduced during motion.",
"vk_rc_cbr": "CBR (Constant Bitrate)",
"vk_rc_cqp": "CQP (Constant QP)",
"vk_rc_mode": "Rate Control",
"vk_rc_mode_desc": "Rate control mode for encoding. Auto lets the driver decide.",
"vk_rc_vbr": "VBR (Variable Bitrate) (default)",
"vk_tune": "Tuning",
"vk_tune_desc": "Low latency modes reduce encoding delay at the cost of quality.",
"vk_tune_hq": "High Quality (hq)",
"vk_tune_ll": "Low Latency (ll) (default)",
"vk_tune_ull": "Ultra Low Latency (ull)",
"virtual_sink": "Virtual Sink",
"virtual_sink_desc": "Manually specify a virtual audio device to use. If unset, the device is chosen automatically. We strongly recommend leaving this field blank to use automatic device selection!",
"virtual_sink_placeholder": "Steam Streaming Speakers",
@@ -0,0 +1,76 @@
#version 450
layout(local_size_x = 16, local_size_y = 16) in;
layout(set = 0, binding = 0) uniform sampler2D rgb_in;
layout(set = 0, binding = 1) uniform writeonly image2D y_out;
layout(set = 0, binding = 2) uniform writeonly image2D uv_out;
layout(set = 0, binding = 3) uniform sampler2D cursor_in;
layout(push_constant) uniform PushConstants {
vec4 color_vec_y;
vec4 color_vec_u;
vec4 color_vec_v;
vec2 range_y;
vec2 range_uv;
ivec2 src_offset;
ivec2 src_size;
ivec2 dst_size;
ivec2 cursor_pos;
ivec2 cursor_size; // w=0 means no cursor
int y_invert;
} pc;
vec3 blend_cursor(vec3 rgb, ivec2 pos) {
ivec2 cp = pos - pc.cursor_pos;
if (cp.x >= 0 && cp.y >= 0 && cp.x < pc.cursor_size.x && cp.y < pc.cursor_size.y) {
vec4 c = texture(cursor_in, (vec2(cp) + 0.5) / vec2(pc.cursor_size));
rgb = mix(rgb, c.bgr, c.a);
}
return rgb;
}
void main() {
ivec2 pos = ivec2(gl_GlobalInvocationID.xy);
if (pos.x >= pc.dst_size.x || pos.y >= pc.dst_size.y)
return;
vec2 inv_tex = 1.0 / vec2(textureSize(rgb_in, 0));
vec2 scale = vec2(pc.src_size) / vec2(pc.dst_size);
vec2 uv = (vec2(pc.src_offset) + (vec2(pos) + 0.5) * scale) * inv_tex;
if (pc.y_invert != 0)
uv.y = 1.0 - uv.y;
vec3 rgb = texture(rgb_in, uv).rgb;
if (pc.cursor_size.x > 0)
rgb = blend_cursor(rgb, pos);
// Y plane
float y = dot(pc.color_vec_y.xyz, rgb) + pc.color_vec_y.w;
imageStore(y_out, pos, vec4(y * pc.range_y.x + pc.range_y.y, 0, 0, 0));
// UV plane (half resolution, one thread per 2x2 block)
if ((pos.x & 1) == 0 && (pos.y & 1) == 0) {
vec2 step = scale * inv_tex;
if (pc.y_invert != 0)
step.y = -step.y;
vec3 rgb_r = texture(rgb_in, uv + vec2(step.x, 0)).rgb;
vec3 rgb_b = texture(rgb_in, uv + vec2(0, step.y)).rgb;
vec3 rgb_br = texture(rgb_in, uv + vec2(step.x, step.y)).rgb;
if (pc.cursor_size.x > 0) {
rgb_r = blend_cursor(rgb_r, pos + ivec2(1, 0));
rgb_b = blend_cursor(rgb_b, pos + ivec2(0, 1));
rgb_br = blend_cursor(rgb_br, pos + ivec2(1, 1));
}
vec3 avg = (rgb + rgb_r + rgb_b + rgb_br) * 0.25;
float cb = dot(pc.color_vec_u.xyz, avg) + pc.color_vec_u.w;
float cr = dot(pc.color_vec_v.xyz, avg) + pc.color_vec_v.w;
imageStore(uv_out, pos >> 1, vec4(cb * pc.range_uv.x + pc.range_uv.y,
cr * pc.range_uv.x + pc.range_uv.y, 0, 0));
}
}
@@ -36,6 +36,7 @@ protected:
{"AMD AMF Encoder", "amd"},
{"VideoToolbox Encoder", "vt"},
{"VA-API Encoder", "vaapi"},
{"Vulkan Encoder", "vulkan"},
{"Software Encoder", "sw"}
};
}