diff --git a/cmake/compile_definitions/linux.cmake b/cmake/compile_definitions/linux.cmake
index 485e419adb5..8fc72b1dc2f 100644
--- a/cmake/compile_definitions/linux.cmake
+++ b/cmake/compile_definitions/linux.cmake
@@ -124,6 +124,22 @@ if(LIBVA_FOUND)
"${CMAKE_SOURCE_DIR}/src/platform/linux/vaapi.cpp")
endif()
+# v4l2
+if(${SUNSHINE_ENABLE_V4L2})
+ include(CheckIncludeFiles)
+ check_include_files("linux/videodev2.h" V4L2_FOUND)
+else()
+ set(V4L2_FOUND OFF)
+endif()
+if(V4L2_FOUND)
+ add_compile_definitions(SUNSHINE_BUILD_V4L2)
+ list(APPEND PLATFORM_TARGET_FILES
+ "${CMAKE_SOURCE_DIR}/src/platform/linux/v4l2_wrapper.h"
+ "${CMAKE_SOURCE_DIR}/src/platform/linux/v4l2_wrapper.c"
+ "${CMAKE_SOURCE_DIR}/src/platform/linux/v4l2.h"
+ "${CMAKE_SOURCE_DIR}/src/platform/linux/v4l2.cpp")
+endif()
+
# vulkan video encoding (via FFmpeg)
if(${SUNSHINE_ENABLE_VULKAN})
if(NOT SUNSHINE_SYSTEM_VULKAN_HEADERS)
diff --git a/cmake/prep/options.cmake b/cmake/prep/options.cmake
index f6d4e5b4c66..0daed8c6a80 100644
--- a/cmake/prep/options.cmake
+++ b/cmake/prep/options.cmake
@@ -62,6 +62,8 @@ elseif(UNIX) # Linux
"Enable KMS grab if available." ON)
option(SUNSHINE_ENABLE_VAAPI
"Enable building vaapi specific code." ON)
+ option(SUNSHINE_ENABLE_V4L2
+ "Enable building v4l2 specific code." ON)
option(SUNSHINE_ENABLE_VULKAN
"Enable Vulkan video encoding." ON)
option(SUNSHINE_ENABLE_WAYLAND
diff --git a/docs/configuration.md b/docs/configuration.md
index d1b12b3c17a..664fd52d07e 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -2255,7 +2255,7 @@ supported on the current platform.
@endcode
- | Choices |
+ Choices |
nvenc |
For NVIDIA graphics cards |
@@ -2276,6 +2276,11 @@ supported on the current platform.
Use Vulkan encoder (AMD, Intel, NVIDIA).
@note{Applies to Linux only.} |
+
+ | v4l2 |
+ Use V4L2 encoder.
+ @note{Applies to Linux only.} |
+
| software |
Encoding occurs on the CPU |
diff --git a/src/cbs.cpp b/src/cbs.cpp
index 03f5f04e236..5cd901c977c 100644
--- a/src/cbs.cpp
+++ b/src/cbs.cpp
@@ -260,38 +260,39 @@ namespace cbs {
}
/**
- * @brief Check whether an encoded H.264 or HEVC packet contains active SPS VUI metadata.
+ * @brief Check whether the given encoded H.264 or HEVC packets contain active SPS VUI metadata.
*
- * @param packet Encoded packet to parse with FFmpeg's coded bitstream reader.
+ * @param packets Encoded packets to parse with FFmpeg's coded bitstream reader.
* @param codec_id FFmpeg codec identifier; expected to be AV_CODEC_ID_H264 or AV_CODEC_ID_H265.
* @return `true` when the packet's active SPS advertises VUI parameters.
*/
- bool validate_sps(const AVPacket *packet, int codec_id) {
+ bool validate_sps(const std::vector &packets, int codec_id) {
cbs::ctx_t ctx;
if (ff_cbs_init(&ctx, (AVCodecID) codec_id, nullptr)) {
return false;
}
- cbs::frag_t frag;
+ for (const auto &packet : packets) {
+ cbs::frag_t frag;
- int err = ff_cbs_read_packet(ctx.get(), &frag, packet);
- if (err < 0) {
- char err_str[AV_ERROR_MAX_STRING_SIZE] {0};
- BOOST_LOG(error) << "Couldn't read packet: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err);
+ int err = ff_cbs_read_packet(ctx.get(), &frag, packet);
+ if (err < 0) {
+ char err_str[AV_ERROR_MAX_STRING_SIZE] {0};
+ BOOST_LOG(error) << "Couldn't read packet: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err);
- return false;
+ return false;
+ }
}
if (codec_id == AV_CODEC_ID_H264) {
auto h264 = (CodedBitstreamH264Context *) ctx->priv_data;
-
- if (!h264->active_sps->vui_parameters_present_flag) {
- return false;
- }
-
- return true;
+ return h264 && h264->active_sps && h264->active_sps->vui_parameters_present_flag;
+ } else if (codec_id == AV_CODEC_ID_H265) {
+ auto h265 = (CodedBitstreamH265Context *) ctx->priv_data;
+ return h265 && h265->active_sps && h265->active_sps->vui_parameters_present_flag;
+ } else {
+ BOOST_LOG(error) << "Unsupported codec ID for validate_sps: "sv << codec_id;
+ return false;
}
-
- return ((CodedBitstreamH265Context *) ctx->priv_data)->active_sps->vui_parameters_present_flag;
}
} // namespace cbs
diff --git a/src/cbs.h b/src/cbs.h
index 13820edf87f..66832b3a04e 100644
--- a/src/cbs.h
+++ b/src/cbs.h
@@ -53,11 +53,11 @@ namespace cbs {
h264_t make_sps_h264(const AVCodecContext *ctx, const AVPacket *packet);
/**
- * @brief Check whether an encoded H.264 or HEVC packet contains active SPS VUI metadata.
+ * @brief Check whether the given encoded H.264 or HEVC packets contain active SPS VUI metadata.
*
- * @param packet Encoded packet to parse with FFmpeg's coded bitstream reader.
+ * @param packets Encoded packets to parse with FFmpeg's coded bitstream reader.
* @param codec_id FFmpeg codec identifier; expected to be AV_CODEC_ID_H264 or AV_CODEC_ID_H265.
* @return `true` when the packet's active SPS advertises VUI parameters.
*/
- bool validate_sps(const AVPacket *packet, int codec_id);
+ bool validate_sps(const std::vector &packets, int codec_id);
} // namespace cbs
diff --git a/src/platform/common.h b/src/platform/common.h
index e8aff70ec2c..755a024d74d 100644
--- a/src/platform/common.h
+++ b/src/platform/common.h
@@ -41,6 +41,17 @@ struct AVHWFramesContext;
struct AVCodecContext;
struct AVDictionary;
+extern "C" {
+/**
+ * @brief Submit a frame to an opened FFmpeg encoder.
+ *
+ * @param avctx FFmpeg codec context that receives the frame.
+ * @param frame Frame to submit, or nullptr to flush the encoder.
+ * @return 0 on success, AVERROR(EAGAIN) when output must be drained, or another negative AVERROR code on failure.
+ */
+int avcodec_send_frame(AVCodecContext *avctx, const AVFrame *frame);
+}
+
#ifdef _WIN32
// Forward declarations of boost classes to avoid having to include boost headers
// here, which results in issues with Windows.h and WinSock2.h include order.
@@ -331,6 +342,7 @@ namespace platf {
cuda, ///< CUDA
videotoolbox, ///< VideoToolbox
vulkan, ///< Vulkan
+ v4l2, ///< V4L2
unknown ///< Unknown
};
@@ -647,6 +659,28 @@ namespace platf {
virtual int prepare_to_derive_context(int hw_device_type) {
return 0;
};
+
+ /**
+ * @brief Load the opened context.
+ * @note Implementations may load the opened context after initialization.
+ *
+ * @param ctx FFmpeg codec context to load.
+ * @return Status from loading the opened context; 0 on success, nonzero to abort.
+ */
+ virtual int load_opened_context(AVCodecContext *ctx) {
+ return 0;
+ }
+
+ /**
+ * @brief Send a frame to the codec.
+ * @note Implementations may perform additional operations besides avcodec_send_frame().
+ *
+ * @param ctx FFmpeg codec context to use.
+ * @return Status from sending the frame; 0 on success, nonzero to abort.
+ */
+ virtual int send_frame(AVCodecContext *ctx) {
+ return avcodec_send_frame(ctx, frame);
+ }
};
/**
diff --git a/src/platform/linux/graphics.cpp b/src/platform/linux/graphics.cpp
index 1ada5a0c9e0..04b33548a7a 100644
--- a/src/platform/linux/graphics.cpp
+++ b/src/platform/linux/graphics.cpp
@@ -64,6 +64,9 @@ namespace gl {
return egl_image_target_texture_2d_fn;
}
+ PFNEGLCREATEIMAGEPROC egl_create_image = nullptr; ///< Selected core or KHR EGL image-creation entry point.
+ PFNEGLDESTROYIMAGEPROC egl_destroy_image = nullptr; ///< Selected core or KHR EGL image-destruction entry point.
+
/**
* @brief Drain and log pending OpenGL errors.
*/
@@ -335,6 +338,32 @@ namespace gbm {
} // namespace gbm
namespace egl {
+ /**
+ * @brief Owning type-erased EGL attribute array with a type-aware deleter.
+ *
+ * The deleter restores the EGL attribute element type used for allocation.
+ */
+ using egl_attribs_t = std::unique_ptr;
+
+ /**
+ * @brief Function pointer type for constructing attributes for the selected EGL image API.
+ *
+ * @param surface DMA-BUF surface descriptor to translate.
+ * @return Type-erased EGL attribute array terminated by EGL_NONE.
+ */
+ using surface_descriptor_to_egl_attribs_fn = egl_attribs_t (*)(const surface_descriptor_t &surface);
+
+ surface_descriptor_to_egl_attribs_fn surface_descriptor_to_egl_attribs; ///< Attribute builder selected for the active EGL image API.
+
+ /**
+ * @brief Build EGL DMA-BUF import attributes using a specific EGL attribute type.
+ *
+ * @tparam EglType EGLAttrib for the core API or EGLint for the KHR API.
+ * @param surface DMA-BUF surface descriptor to translate.
+ * @return Type-erased EGL attribute array terminated by EGL_NONE.
+ */
+ template
+ egl_attribs_t surface_descriptor_to_egl_attribs_impl(const surface_descriptor_t &surface);
namespace { // Keep privileged implementation details anonymous/local to this translation unit
@@ -639,6 +668,19 @@ namespace egl {
BOOST_LOG(warning) << "GL: glEGLImageTargetTexture2DOES not available; DMA-BUF import will fail"sv;
}
+ if (eglCreateImage != nullptr && eglDestroyImage != nullptr) {
+ gl::egl_create_image = (gl::PFNEGLCREATEIMAGEPROC) eglCreateImage;
+ gl::egl_destroy_image = (gl::PFNEGLDESTROYIMAGEPROC) eglDestroyImage;
+ egl::surface_descriptor_to_egl_attribs = surface_descriptor_to_egl_attribs_impl;
+ } else if (eglCreateImageKHR != nullptr && eglDestroyImageKHR != nullptr) {
+ gl::egl_create_image = (gl::PFNEGLCREATEIMAGEPROC) eglCreateImageKHR;
+ gl::egl_destroy_image = (gl::PFNEGLDESTROYIMAGEPROC) eglDestroyImageKHR;
+ egl::surface_descriptor_to_egl_attribs = surface_descriptor_to_egl_attribs_impl;
+ } else {
+ BOOST_LOG(error) << "Neither eglCreateImage/eglDestroyImage nor eglCreateImageKHR/eglDestroyImageKHR are available"sv;
+ return std::nullopt;
+ }
+
// GetString returns const GLubyte* (unsigned char*); convert to std::string safely (avoids sonar cpp:S6996).
auto gl_string = [](const GLubyte *s) {
std::string result;
@@ -719,12 +761,42 @@ namespace egl {
}
/**
- * @brief Get EGL attributes for eglCreateImage() to import the provided surface.
- * @param surface The surface descriptor.
- * @return Vector of EGL attributes.
+ * @brief Build EGL DMA-BUF import attributes using a specific EGL attribute type.
+ *
+ * eglCreateImage uses EGLAttrib while eglCreateImageKHR uses EGLint, so the
+ * selected specialization controls both allocation and type-aware deletion.
+ *
+ * @tparam EglType EGLAttrib for the core API or EGLint for the KHR API.
+ * @param surface DMA-BUF surface descriptor to translate.
+ * @return Type-erased EGL attribute array terminated by EGL_NONE.
*/
- std::vector surface_descriptor_to_egl_attribs(const surface_descriptor_t &surface) {
- std::vector attribs;
+ template
+ egl_attribs_t surface_descriptor_to_egl_attribs_impl(const surface_descriptor_t &surface) {
+ // Use a custom vector to abstract away the underlying element type.
+ struct {
+ static_assert(std::is_trivially_copyable_v);
+ static_assert(std::is_trivially_destructible_v);
+
+ static void deleter(void *ptr) {
+ delete[] reinterpret_cast(ptr);
+ };
+
+ size_t len = 0;
+ size_t cap = 32;
+ egl_attribs_t buf {new EglType[cap], &deleter};
+
+ void emplace_back(EglType value) {
+ if (len == cap) {
+ size_t new_cap = cap + 32;
+ egl_attribs_t new_buf {new EglType[new_cap], &deleter};
+ memcpy(new_buf.get(), buf.get(), len * sizeof(EglType));
+ buf = std::move(new_buf);
+ cap = new_cap;
+ }
+ reinterpret_cast(buf.get())[len] = value;
+ ++len;
+ }
+ } attribs;
attribs.emplace_back(EGL_WIDTH);
attribs.emplace_back(surface.width);
@@ -757,9 +829,14 @@ namespace egl {
}
attribs.emplace_back(EGL_NONE);
- return attribs;
+ return std::move(attribs.buf);
}
+ /** @copydoc surface_descriptor_to_egl_attribs_impl */
+ template egl_attribs_t surface_descriptor_to_egl_attribs_impl(const surface_descriptor_t &surface);
+ /** @copydoc surface_descriptor_to_egl_attribs_impl */
+ template egl_attribs_t surface_descriptor_to_egl_attribs_impl(const surface_descriptor_t &surface);
+
/**
* @brief Import the source frame texture for EGL/OpenGL conversion.
*
@@ -768,11 +845,11 @@ namespace egl {
* @return Imported RGB image, or empty when import fails.
*/
std::optional import_source(display_t::pointer egl_display, const surface_descriptor_t &xrgb) {
- auto attribs = surface_descriptor_to_egl_attribs(xrgb);
+ egl_attribs_t attribs = surface_descriptor_to_egl_attribs(xrgb);
rgb_t rgb {
egl_display,
- eglCreateImage(egl_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, attribs.data()),
+ gl::egl_create_image(egl_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, attribs.get()),
gl::tex_t::make(1)
};
@@ -877,13 +954,13 @@ namespace egl {
* @return Imported NV12 image, or empty when import fails.
*/
std::optional import_target(display_t::pointer egl_display, std::array &&fds, const surface_descriptor_t &y, const surface_descriptor_t &uv) {
- auto y_attribs = surface_descriptor_to_egl_attribs(y);
- auto uv_attribs = surface_descriptor_to_egl_attribs(uv);
+ egl_attribs_t y_attribs = surface_descriptor_to_egl_attribs(y);
+ egl_attribs_t uv_attribs = surface_descriptor_to_egl_attribs(uv);
nv12_t nv12 {
egl_display,
- eglCreateImage(egl_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, y_attribs.data()),
- eglCreateImage(egl_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, uv_attribs.data()),
+ gl::egl_create_image(egl_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, y_attribs.get()),
+ gl::egl_create_image(egl_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, uv_attribs.get()),
gl::tex_t::make(2),
gl::frame_buf_t::make(2),
std::move(fds)
@@ -935,9 +1012,9 @@ namespace egl {
yuv444_t yuv444 {
egl_display,
- eglCreateImage(egl_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, y_attribs.data()),
- eglCreateImage(egl_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, u_attribs.data()),
- eglCreateImage(egl_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, v_attribs.data()),
+ gl::egl_create_image(egl_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, y_attribs.get()),
+ gl::egl_create_image(egl_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, u_attribs.get()),
+ gl::egl_create_image(egl_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, v_attribs.get()),
gl::tex_t::make(3),
gl::frame_buf_t::make(3),
std::move(fds)
@@ -1527,6 +1604,35 @@ namespace egl {
return 0;
}
+
+ int sws_t::copy(gl::frame_buf_t &dst, gl::frame_buf_t &src) {
+ for (int plane = 0; plane < 2; ++plane) {
+ const int plane_width = out_width / (plane + 1); // UV plane is half the width of the Y plane
+ const int plane_height = out_height / (plane + 1);
+ const GLenum attachment = GL_COLOR_ATTACHMENT0 + plane;
+
+ gl::ctx.BindFramebuffer(GL_READ_FRAMEBUFFER, src[plane]);
+ gl::ctx.ReadBuffer(attachment);
+
+ gl::ctx.BindFramebuffer(GL_DRAW_FRAMEBUFFER, dst[plane]);
+ gl::ctx.DrawBuffers(1, &attachment);
+
+#ifndef NDEBUG
+ if (gl::ctx.CheckFramebufferStatus(GL_READ_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE || gl::ctx.CheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
+ BOOST_LOG(error) << "Copy: incomplete framebuffer for NV12 plane "sv << plane;
+ return -1;
+ }
+#endif
+
+ gl::ctx.BlitFramebuffer(0, 0, plane_width, plane_height, 0, 0, plane_width, plane_height, GL_COLOR_BUFFER_BIT, GL_NEAREST);
+ }
+
+ gl::ctx.BindFramebuffer(GL_FRAMEBUFFER, 0);
+
+ gl::ctx.Flush();
+
+ return 0;
+ }
} // namespace egl
/**
diff --git a/src/platform/linux/graphics.h b/src/platform/linux/graphics.h
index 60c0a1dfcde..45a62e5c1c7 100644
--- a/src/platform/linux/graphics.h
+++ b/src/platform/linux/graphics.h
@@ -81,6 +81,31 @@ namespace gl {
*/
PFNGLEGLIMAGETARGETTEXTURE2DOESPROC egl_image_target_texture_2d();
+ // Some old GL drivers only have eglCreateImageKHR and eglDestroyImageKHR (e.g. Raspberry Pi 4B),
+ // so we determine at runtime which one to use.
+ /**
+ * @brief Function pointer type for eglCreateImage.
+ *
+ * @param dpy EGL display that owns the image.
+ * @param ctx EGL context used for image creation.
+ * @param target EGL image target.
+ * @param buffer Native client buffer, when required by the target.
+ * @param attrib_list Type-erased EGL attribute list accepted by the selected core or KHR entry point.
+ * @return Created EGL image, or EGL_NO_IMAGE on failure.
+ */
+ using PFNEGLCREATEIMAGEPROC = void *(*) (EGLDisplay dpy, EGLContext ctx, EGLenum target, EGLClientBuffer buffer, const void *attrib_list);
+
+ /**
+ * @brief Function pointer type for eglDestroyImage.
+ *
+ * @param dpy EGL display that owns the image.
+ * @param image EGL image to destroy.
+ * @return EGL_TRUE on success, or EGL_FALSE on failure.
+ */
+ using PFNEGLDESTROYIMAGEPROC = EGLBoolean(GLAD_API_PTR *)(EGLDisplay dpy, EGLImage image);
+ extern PFNEGLCREATEIMAGEPROC egl_create_image; ///< Selected core or KHR EGL image-creation entry point.
+ extern PFNEGLDESTROYIMAGEPROC egl_destroy_image; ///< Selected core or KHR EGL image-destruction entry point.
+
/**
* @brief Drain and log pending OpenGL errors.
*
@@ -152,7 +177,7 @@ namespace gl {
int x = 0;
for (auto fb : (*this)) {
ctx.BindFramebuffer(GL_FRAMEBUFFER, fb);
- ctx.FramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + x, 0, 0);
+ ctx.FramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + x, GL_TEXTURE_2D, 0, 0);
++x;
}
@@ -160,7 +185,7 @@ namespace gl {
}
/**
- * @brief Bind textures to this object's framebuffers as color attachments.
+ * @brief Bind 2D textures to this object's framebuffers as color attachments.
*
* @param it_begin First texture object to attach.
* @param it_end One-past-the-end iterator for texture objects to attach.
@@ -178,7 +203,7 @@ namespace gl {
ctx.BindFramebuffer(GL_FRAMEBUFFER, (*this)[x]);
ctx.BindTexture(GL_TEXTURE_2D, tex);
- ctx.FramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + x, tex, 0);
+ ctx.FramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + x, GL_TEXTURE_2D, tex, 0);
++x;
});
@@ -433,31 +458,31 @@ namespace egl {
#ifndef DOXYGEN
KITTY_USING_MOVE_T(rgb_t, rgb_img_t, , {
if (el.xrgb8) {
- eglDestroyImage(el.display, el.xrgb8);
+ gl::egl_destroy_image(el.display, el.xrgb8);
}
});
KITTY_USING_MOVE_T(nv12_t, nv12_img_t, , {
if (el.r8) {
- eglDestroyImage(el.display, el.r8);
+ gl::egl_destroy_image(el.display, el.r8);
}
if (el.bg88) {
- eglDestroyImage(el.display, el.bg88);
+ gl::egl_destroy_image(el.display, el.bg88);
}
});
KITTY_USING_MOVE_T(yuv444_t, yuv444_img_t, , {
if (el.r8) {
- eglDestroyImage(el.display, el.r8);
+ gl::egl_destroy_image(el.display, el.r8);
}
if (el.g8) {
- eglDestroyImage(el.display, el.g8);
+ gl::egl_destroy_image(el.display, el.g8);
}
if (el.b8) {
- eglDestroyImage(el.display, el.b8);
+ gl::egl_destroy_image(el.display, el.b8);
}
});
@@ -724,6 +749,15 @@ namespace egl {
*/
int blank(gl::frame_buf_t &fb, int offsetX_, int offsetY_, int width, int height, bool is_yuv444);
+ /**
+ * @brief Copy the contents of one framebuffer to another.
+ *
+ * @param dst Destination framebuffer.
+ * @param src Source framebuffer.
+ * @return 0 when the copy is successful; nonzero on OpenGL failure.
+ */
+ int copy(gl::frame_buf_t &dst, gl::frame_buf_t &src);
+
/**
* @brief Load ram data from the backing API or store.
*
diff --git a/src/platform/linux/kmsgrab.cpp b/src/platform/linux/kmsgrab.cpp
index 909b84a06a0..5b369f12888 100644
--- a/src/platform/linux/kmsgrab.cpp
+++ b/src/platform/linux/kmsgrab.cpp
@@ -29,6 +29,7 @@
#include "src/round_robin.h"
#include "src/utility.h"
#include "src/video.h"
+#include "v4l2.h"
#include "vaapi.h"
#include "vulkan_encode.h"
#include "wayland.h"
@@ -1723,6 +1724,12 @@ namespace platf {
}
#endif
+#ifdef SUNSHINE_BUILD_V4L2
+ if (mem_type == mem_type_e::v4l2) {
+ return v4l2::make_avcodec_encode_device(width, height, false);
+ }
+#endif
+
return std::make_unique();
}
@@ -1901,6 +1908,12 @@ namespace platf {
}
#endif
+#ifdef SUNSHINE_BUILD_V4L2
+ if (mem_type == mem_type_e::v4l2) {
+ return v4l2::make_avcodec_encode_device(width, height, dup(card.render_fd.el), img_offset_x, img_offset_y, true);
+ }
+#endif
+
BOOST_LOG(error) << "Unsupported pixel format for egl::display_vram_t: "sv << platf::from_pix_fmt(pix_fmt);
return nullptr;
}
@@ -2063,7 +2076,7 @@ namespace platf {
* @return KMS display backend, or nullptr when initialization fails.
*/
std::shared_ptr 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 || hwdevice_type == mem_type_e::vulkan) {
+ if (hwdevice_type == mem_type_e::vaapi || hwdevice_type == mem_type_e::cuda || hwdevice_type == mem_type_e::vulkan || hwdevice_type == mem_type_e::v4l2) {
auto disp = std::make_shared(hwdevice_type);
if (!disp->init(display_name, config)) {
@@ -2295,7 +2308,15 @@ namespace platf {
kms::card_descriptors = std::move(cds);
- BOOST_LOG(debug) << "Final KMS display_names return list: " << (display_names | std::views::join_with(' ') | std::ranges::to());
+ std::string display_names_str;
+ for (const auto &name : display_names) {
+ if (!display_names_str.empty()) {
+ display_names_str += ' ';
+ }
+ display_names_str += name;
+ }
+
+ BOOST_LOG(debug) << "Final KMS display_names return list: " << display_names_str;
return display_names;
}
diff --git a/src/platform/linux/pipewire.cpp b/src/platform/linux/pipewire.cpp
index 7d9b9076c29..a08c6282182 100644
--- a/src/platform/linux/pipewire.cpp
+++ b/src/platform/linux/pipewire.cpp
@@ -20,6 +20,7 @@
#include "src/main.h"
#include "src/platform/common.h"
#include "src/video.h"
+#include "v4l2.h"
#include "vaapi.h"
#include "vulkan_encode.h"
#include "wayland.h"
@@ -336,11 +337,12 @@ namespace pipewire {
std::array params;
// Add preferred parameters for DMA-BUF with modifiers
- // Use DMA-BUF for VAAPI, or for CUDA when the display GPU is NVIDIA (pure NVIDIA system).
+ // Use DMA-BUF for VAAPI, VULKAN, V4L2, or for CUDA when the display GPU is NVIDIA (pure NVIDIA system).
// 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::v4l2 ||
(mem_type == platf::mem_type_e::cuda && display_is_nvidia));
if (use_dmabuf) {
for (int i = 0; i < n_dmabuf_infos; i++) {
@@ -989,6 +991,7 @@ namespace pipewire {
case vaapi:
case cuda:
case vulkan:
+ case v4l2:
return true;
default:
return false;
@@ -1312,6 +1315,12 @@ namespace pipewire {
}
#endif
+#ifdef SUNSHINE_BUILD_V4L2
+ if (mem_type == platf::mem_type_e::v4l2) {
+ return v4l2::make_avcodec_encode_device(width, height, n_dmabuf_infos > 0);
+ }
+#endif
+
return std::make_unique();
}
diff --git a/src/platform/linux/v4l2.cpp b/src/platform/linux/v4l2.cpp
new file mode 100644
index 00000000000..f1646b0bd35
--- /dev/null
+++ b/src/platform/linux/v4l2.cpp
@@ -0,0 +1,428 @@
+/**
+ * @file src/platform/linux/v4l2.cpp
+ * @brief Definitions for V4L2 M2M zero-copy encode devices.
+ */
+
+// standard includes
+#include
+#include
+#include
+#include
+
+extern "C" {
+#include
+#include
+}
+
+// local includes
+#include "graphics.h"
+#include "v4l2.h"
+#include "v4l2_wrapper.h"
+
+namespace v4l2 {
+ /**
+ * @brief Common V4L2 M2M encode device backed by EGL-imported output buffers.
+ */
+ class v4l2_t: public platf::avcodec_encode_device_t {
+ public:
+ /**
+ * @brief Initialize the GBM and EGL resources used for frame conversion.
+ *
+ * @param in_width Input image width in pixels.
+ * @param in_height Input image height in pixels.
+ * @param render_device Open render-device descriptor whose ownership is transferred to this object.
+ * @return 0 on success, or -1 when GBM or EGL initialization fails.
+ */
+ int init(int in_width, int in_height, file_t &&render_device) {
+ render_device_fd = std::move(render_device);
+
+ if (!gbm::create_device) {
+ BOOST_LOG(warning) << "libgbm not initialized"sv;
+ return -1;
+ }
+
+ gbm_device.reset(gbm::create_device(render_device_fd.el));
+ if (!gbm_device) {
+ char string[1024];
+ BOOST_LOG(error) << "Couldn't create GBM device: ["sv << strerror_r(errno, string, sizeof(string)) << ']';
+ return -1;
+ }
+
+ display = egl::make_display(gbm_device.get());
+ if (!display) {
+ return -1;
+ }
+
+ auto egl_ctx_opt = egl::make_ctx(display.get());
+ if (!egl_ctx_opt) {
+ return -1;
+ }
+
+ egl_ctx = std::move(*egl_ctx_opt);
+
+ width = in_width;
+ height = in_height;
+
+ // V4L2 encoder has no need to create hw frames context,
+ // so we just set this to a non-null value to avoid falling back to software encoding.
+ data = (void *) 0x1;
+
+ last_output_frame_idx = -1;
+ current_output_frame_idx = -1;
+
+ return 0;
+ }
+
+ /**
+ * @brief Import the output buffers allocated by an opened FFmpeg V4L2 M2M encoder.
+ *
+ * @param ctx Opened FFmpeg V4L2 M2M encoder context.
+ * @return 0 on success, or -1 when the context, buffer layout, export, or EGL import is invalid.
+ */
+ int load_opened_context(AVCodecContext *ctx) override {
+ if (v4l2_wrapper_context_init(&encoder_ctx, ctx)) {
+ BOOST_LOG(error) << "Failed to load V4L2 context from AVCodecContext (open failed?)"sv;
+ return -1;
+ }
+
+ auto &fmt = encoder_ctx.format.fmt;
+ bool is_mplane = V4L2_TYPE_IS_MULTIPLANAR(encoder_ctx.format.type);
+ auto num_buffers = encoder_ctx.num_buffers;
+ auto num_planes = is_mplane ? fmt.pix_mp.num_planes : 1;
+ auto pixel_format = is_mplane ? fmt.pix_mp.pixelformat : fmt.pix.pixelformat;
+
+ // Check validity first to avoid unnecessary work
+ if (num_buffers <= 0) {
+ BOOST_LOG(error) << "V4L2 encoder has no output buffers to bind?"sv;
+ return -1;
+ }
+ switch (pixel_format) {
+ case V4L2_PIX_FMT_NV12:
+ if (num_planes != 1) {
+ BOOST_LOG(error) << "Unexpected number of planes for NV12 V4L2 output: expected 1, got "sv << num_planes;
+ return -1;
+ }
+ break;
+ case V4L2_PIX_FMT_NV12M:
+ if (num_planes != 2) {
+ BOOST_LOG(error) << "Unexpected number of planes for NV12M V4L2 output: expected 2, got "sv << num_planes;
+ return -1;
+ }
+ break;
+ default:
+ BOOST_LOG(error) << "Unsupported V4L2 output pixel format: "sv << util::hex(pixel_format).to_string_view();
+ return -1;
+ }
+
+ // Export the V4L2 output buffers
+ v4l2_exportbuffer export_req = {0};
+ export_req.type = encoder_ctx.format.type;
+ export_req.flags = O_CLOEXEC;
+ output_frames.reserve(num_buffers);
+ for (int i = 0; i < num_buffers; i++) {
+ std::array dmabuf_fds;
+ export_req.index = i;
+ for (int plane = 0; plane < num_planes; plane++) {
+ export_req.plane = plane;
+ if (ioctl(encoder_ctx.fd, VIDIOC_EXPBUF, &export_req) < 0) {
+ char string[1024];
+ BOOST_LOG(error) << "Couldn't export V4L2 buffer "sv << i << ": " << strerror_r(errno, string, sizeof(string));
+ return -1;
+ }
+ dmabuf_fds[plane] = export_req.fd;
+ }
+
+ egl::surface_descriptor_t sds[2] = {};
+ std::fill_n(sds[0].fds, 4, -1);
+ std::fill_n(sds[1].fds, 4, -1);
+ switch (pixel_format) {
+ case V4L2_PIX_FMT_NV12:
+ {
+ auto width = is_mplane ? fmt.pix_mp.width : fmt.pix.width;
+ auto height = is_mplane ? fmt.pix_mp.height : fmt.pix.height;
+ auto bytesperline = is_mplane ? fmt.pix_mp.plane_fmt[0].bytesperline : fmt.pix.bytesperline;
+
+ sds[0].width = width;
+ sds[0].height = height;
+ sds[0].fds[0] = dmabuf_fds[0].el;
+ sds[0].fourcc = DRM_FORMAT_R8;
+ sds[0].modifier = DRM_FORMAT_MOD_LINEAR;
+ sds[0].pitches[0] = bytesperline;
+ sds[0].offsets[0] = 0;
+
+ sds[1].width = width / 2; // UV plane is subsampled
+ sds[1].height = height / 2;
+ sds[1].fds[0] = dmabuf_fds[0].el;
+ sds[1].fourcc = DRM_FORMAT_GR88;
+ sds[1].modifier = DRM_FORMAT_MOD_LINEAR;
+ sds[1].pitches[0] = bytesperline;
+ sds[1].offsets[0] = bytesperline * height;
+
+ break;
+ }
+ case V4L2_PIX_FMT_NV12M:
+ {
+ sds[0].width = fmt.pix_mp.width;
+ sds[0].height = fmt.pix_mp.height;
+ sds[0].fds[0] = dmabuf_fds[0].el;
+ sds[0].fourcc = DRM_FORMAT_R8;
+ sds[0].modifier = DRM_FORMAT_MOD_LINEAR;
+ sds[0].pitches[0] = fmt.pix_mp.plane_fmt[0].bytesperline;
+ sds[0].offsets[0] = 0;
+
+ sds[1].width = fmt.pix_mp.width / 2; // UV plane is subsampled
+ sds[1].height = fmt.pix_mp.height / 2;
+ sds[1].fds[0] = dmabuf_fds[1].el;
+ sds[1].fourcc = DRM_FORMAT_GR88;
+ sds[1].modifier = DRM_FORMAT_MOD_LINEAR;
+ sds[1].pitches[0] = fmt.pix_mp.plane_fmt[1].bytesperline;
+ sds[1].offsets[0] = 0;
+
+ break;
+ }
+ default:
+ // Unreachable
+ return -1;
+ }
+
+ auto nv12_opt = egl::import_target(display.get(), std::move(dmabuf_fds), sds[0], sds[1]);
+ if (!nv12_opt) {
+ BOOST_LOG(error) << "Failed to import V4L2 output buffer "sv << i << " into EGL"sv;
+ return -1;
+ }
+ output_frames.push_back(std::move(*nv12_opt));
+ }
+
+ auto convertor_opt = egl::sws_t::make(width, height, encoder_ctx.width, encoder_ctx.height, (AVPixelFormat) frame->format, false);
+ if (!convertor_opt) {
+ return -1;
+ }
+ convertor = std::move(*convertor_opt);
+
+ return 0;
+ }
+
+ /**
+ * @brief Attach frame resources used by the next conversion or encode operation.
+ *
+ * @param frame Video or graphics frame being processed.
+ * @param hw_frames_ctx_buf Hardware frames context buffer.
+ * @return Status from updating frame.
+ */
+ int set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) override {
+ // Allocate a dummy buffer in case FFmpeg assumes this frame is invalid.
+ frame->buf[0] = av_buffer_alloc(1);
+ if (!frame->buf[0]) {
+ av_frame_free(&frame);
+ return -1;
+ }
+
+ this->frame_guard.reset(frame);
+ this->frame = frame;
+
+ return 0;
+ }
+
+ /**
+ * @brief Apply the configured colorspace metadata to the EGL converter.
+ */
+ void apply_colorspace() override {
+ convertor.apply_colorspace(colorspace, false);
+ }
+
+ /**
+ * @brief Select the converted V4L2 output buffer and submit its metadata frame to FFmpeg.
+ *
+ * @param ctx Opened FFmpeg V4L2 M2M encoder context.
+ * @return FFmpeg status code, or an AVERROR code when no suitable output buffer is available.
+ */
+ int send_frame(AVCodecContext *ctx) override {
+ if (current_output_frame_idx < 0) {
+ BOOST_LOG(error) << "No converted frame available to send to V4L2 encoder (convert() not called?)"sv;
+ return AVERROR(EINVAL);
+ }
+
+ // convert() not called, so just send the last converted frame again.
+ if (last_output_frame_idx == current_output_frame_idx) {
+ current_output_frame_idx = get_free_frame();
+ if (current_output_frame_idx < 0) {
+ return AVERROR(EAGAIN);
+ }
+ convertor.copy(output_frames[current_output_frame_idx]->buf, output_frames[last_output_frame_idx]->buf);
+ }
+
+ last_output_frame_idx = current_output_frame_idx;
+ v4l2_wrapper_set_current_buffer_index(&encoder_ctx, last_output_frame_idx);
+
+ return avcodec_send_frame(ctx, frame);
+ }
+
+ protected:
+ /**
+ * @brief Reclaim completed V4L2 output buffers and select one available for conversion.
+ *
+ * @return Zero-based output-buffer index, or -1 when no buffer is available.
+ */
+ int get_free_frame() {
+ if (!encoder_ctx.avctx) {
+ BOOST_LOG(error) << "Encoder context not set when trying to get a free V4L2 output buffer"sv;
+ return -1;
+ }
+
+ int frame_idx = v4l2_wrapper_getfree_v4l2buf_idx(&encoder_ctx);
+ if (frame_idx < 0) {
+ BOOST_LOG(error) << "No free V4L2 output buffer available"sv;
+ return -1;
+ }
+
+ return frame_idx;
+ }
+
+ frame_t frame_guard; ///< Metadata frame owner; pixel buffers are managed by the V4L2 kernel driver.
+
+ file_t render_device_fd; ///< Render-device descriptor used to create the GBM device.
+ gbm::gbm_t gbm_device; ///< GBM device associated with the selected render node.
+ egl::display_t display; ///< EGL display used to import and convert DMA-BUFs.
+ egl::ctx_t egl_ctx; ///< EGL context current while conversion resources are used.
+
+ v4l2_wrapper_context encoder_ctx; ///< Wrapper around the opened FFmpeg V4L2 context.
+ egl::sws_t convertor; ///< EGL color converter targeting the encoder's NV12 buffers.
+ std::vector output_frames; ///< EGL imports of all V4L2 output buffers.
+
+ int width; ///< Input image width in pixels.
+ int height; ///< Input image height in pixels.
+
+ int last_output_frame_idx; ///< Output-buffer index most recently submitted to FFmpeg.
+ int current_output_frame_idx; ///< Output-buffer index populated by the most recent conversion.
+ };
+
+ /**
+ * @brief V4L2 M2M encode device that uploads system-memory capture frames through EGL.
+ */
+ class v4l2_ram_t: public v4l2_t {
+ public:
+ /**
+ * @brief Upload and convert a system-memory image into a free V4L2 output buffer.
+ *
+ * @param img Captured system-memory image to convert.
+ * @return 0 on success, or -1 when no output buffer is available.
+ */
+ int convert(platf::img_t &img) override {
+ int frame_idx = get_free_frame();
+ if (frame_idx < 0) {
+ return -1;
+ }
+ convertor.load_ram(img);
+ convertor.convert_nv12(output_frames[frame_idx]->buf);
+ current_output_frame_idx = frame_idx;
+ return 0;
+ }
+ };
+
+ /**
+ * @brief V4L2 M2M encode device that imports and converts DMA-BUF capture frames.
+ */
+ class v4l2_vram_t: public v4l2_t {
+ public:
+ /**
+ * @brief Import and convert a DMA-BUF image into a free V4L2 output buffer.
+ *
+ * @param img Captured DMA-BUF image descriptor to convert.
+ * @return 0 on success, or -1 when buffer selection or DMA-BUF import fails.
+ */
+ int convert(platf::img_t &img) override {
+ int frame_idx = get_free_frame();
+ if (frame_idx < 0) {
+ return -1;
+ }
+ auto &descriptor = (egl::img_descriptor_t &) img;
+
+ if (descriptor.sequence == 0) {
+ // For dummy images, use a blank RGB texture instead of importing a DMA-BUF
+ input_frame = egl::create_blank(img);
+ } else if (descriptor.sequence > sequence) {
+ sequence = descriptor.sequence;
+
+ auto rgb_opt = egl::import_source(display.get(), descriptor.sd);
+ if (!rgb_opt) {
+ return -1;
+ }
+
+ input_frame = std::move(*rgb_opt);
+ }
+
+ convertor.load_vram(descriptor, offset_x, offset_y, (int) input_frame->tex[0], false);
+ convertor.convert_nv12(output_frames[frame_idx]->buf);
+ current_output_frame_idx = frame_idx;
+ return 0;
+ }
+
+ /**
+ * @brief Initialize a DMA-BUF-backed V4L2 encode device and its capture offset.
+ *
+ * @param in_width Input image width in pixels.
+ * @param in_height Input image height in pixels.
+ * @param render_device Open render-device descriptor whose ownership is transferred to this object.
+ * @param offset_x Horizontal offset of the input image within its texture.
+ * @param offset_y Vertical offset of the input image within its texture.
+ * @return 0 on success, or -1 when the common V4L2/EGL initialization fails.
+ */
+ int init(int in_width, int in_height, file_t &&render_device, int offset_x, int offset_y) {
+ if (v4l2_t::init(in_width, in_height, std::move(render_device))) {
+ return -1;
+ }
+
+ sequence = 0;
+
+ this->offset_x = offset_x;
+ this->offset_y = offset_y;
+
+ return 0;
+ }
+
+ private:
+ egl::rgb_t input_frame; ///< EGL import of the current DMA-BUF capture frame.
+ std::uint64_t sequence; ///< Sequence number of the capture frame currently imported into EGL.
+ int offset_x; ///< Horizontal capture offset within the imported texture.
+ int offset_y; ///< Vertical capture offset within the imported texture.
+ };
+
+ std::unique_ptr make_avcodec_encode_device(int width, int height, file_t &&card, int offset_x, int offset_y, bool vram) {
+ if (vram) {
+ auto egl = std::make_unique();
+ if (egl->init(width, height, std::move(card), offset_x, offset_y)) {
+ return nullptr;
+ }
+
+ return egl;
+ }
+
+ else {
+ auto egl = std::make_unique();
+ if (egl->init(width, height, std::move(card))) {
+ return nullptr;
+ }
+
+ return egl;
+ }
+ }
+
+ std::unique_ptr make_avcodec_encode_device(int width, int height, int offset_x, int offset_y, bool vram) {
+ auto render_device = platf::resolve_render_device();
+
+ file_t file = ::open(render_device.c_str(), O_RDWR); // NOSONAR(cpp:S1874): `_sopen_s` not available
+ if (file.el < 0) {
+ char string[1024];
+ BOOST_LOG(error) << "Couldn't open "sv << render_device << ": " << strerror_r(errno, string, sizeof(string));
+
+ return nullptr;
+ }
+
+ return make_avcodec_encode_device(width, height, std::move(file), offset_x, offset_y, vram);
+ }
+
+ std::unique_ptr make_avcodec_encode_device(int width, int height, bool vram) {
+ return make_avcodec_encode_device(width, height, 0, 0, vram);
+ }
+
+} // namespace v4l2
diff --git a/src/platform/linux/v4l2.h b/src/platform/linux/v4l2.h
new file mode 100644
index 00000000000..32a4c4764f2
--- /dev/null
+++ b/src/platform/linux/v4l2.h
@@ -0,0 +1,46 @@
+/**
+ * @file src/platform/linux/v4l2.h
+ * @brief Declarations for V4L2 M2M zero-copy encode devices.
+ */
+#pragma once
+
+// local includes
+#include "misc.h"
+#include "src/platform/common.h"
+
+namespace v4l2 {
+ /**
+ * @brief Create a V4L2 encode device using the automatically selected render device.
+ *
+ * @param width Input image width in pixels.
+ * @param height Input image height in pixels.
+ * @param vram Whether input frames are DMA-BUF-backed VRAM images.
+ * @return Initialized encode device, or nullptr when initialization fails.
+ */
+ std::unique_ptr make_avcodec_encode_device(int width, int height, bool vram);
+
+ /**
+ * @brief Create an offset-aware V4L2 encode device using the automatically selected render device.
+ *
+ * @param width Input image width in pixels.
+ * @param height Input image height in pixels.
+ * @param offset_x Horizontal offset of the input image within its texture.
+ * @param offset_y Vertical offset of the input image within its texture.
+ * @param vram Whether input frames are DMA-BUF-backed VRAM images.
+ * @return Initialized encode device, or nullptr when initialization fails.
+ */
+ std::unique_ptr make_avcodec_encode_device(int width, int height, int offset_x, int offset_y, bool vram);
+
+ /**
+ * @brief Create a V4L2 encode device using an open render-device descriptor.
+ *
+ * @param width Input image width in pixels.
+ * @param height Input image height in pixels.
+ * @param card Render-device file descriptor whose ownership is transferred to the encode device.
+ * @param offset_x Horizontal offset of the input image within its texture.
+ * @param offset_y Vertical offset of the input image within its texture.
+ * @param vram Whether input frames are DMA-BUF-backed VRAM images.
+ * @return Initialized encode device, or nullptr when initialization fails.
+ */
+ std::unique_ptr make_avcodec_encode_device(int width, int height, file_t &&card, int offset_x, int offset_y, bool vram);
+} // namespace v4l2
diff --git a/src/platform/linux/v4l2_wrapper.c b/src/platform/linux/v4l2_wrapper.c
new file mode 100644
index 00000000000..5a2fef784e5
--- /dev/null
+++ b/src/platform/linux/v4l2_wrapper.c
@@ -0,0 +1,61 @@
+/**
+ * @file src/platform/linux/v4l2_wrapper.c
+ * @brief Accessors for the patched FFmpeg V4L2 M2M output-buffer internals.
+ *
+ * FFmpeg's private V4L2 headers are intended for C and cause compatibility
+ * problems when included directly from C++. This translation unit includes
+ * those headers as C and presents the narrow C-compatible interface consumed
+ * by Sunshine's C++ zero-copy V4L2 encode device.
+ */
+
+// standard includes
+#include
+#include
+
+// local includes
+#include "v4l2_wrapper.h"
+
+/** @copydoc v4l2_wrapper_context_init */
+int v4l2_wrapper_context_init(struct v4l2_wrapper_context *ctx, AVCodecContext *avctx) {
+ if (!ctx || !avctx || !avctx->priv_data) {
+ return -1;
+ }
+
+ V4L2m2mContext *v4l2_m2m_ctx = ((V4L2m2mPriv *) avctx->priv_data)->context;
+
+ ctx->avctx = avctx;
+ ctx->output = &v4l2_m2m_ctx->output;
+ ctx->fd = v4l2_m2m_ctx->fd;
+ ctx->format = v4l2_m2m_ctx->output.format;
+ ctx->width = v4l2_m2m_ctx->output.width;
+ ctx->height = v4l2_m2m_ctx->output.height;
+ ctx->num_buffers = v4l2_m2m_ctx->output.num_buffers;
+
+ v4l2_m2m_ctx->output.sunshine_zero_copy = 1;
+ v4l2_m2m_ctx->output.current_buffer_index = -1;
+
+ return 0;
+}
+
+/** @copydoc v4l2_wrapper_getfree_v4l2buf_idx */
+int v4l2_wrapper_getfree_v4l2buf_idx(struct v4l2_wrapper_context *ctx) {
+ struct V4L2Context *output = (struct V4L2Context *) ctx->output;
+
+ // get back as many output buffers as possible
+ while (v4l2_dequeue_v4l2buf(output, 0));
+
+ for (int i = 0; i < ctx->num_buffers; i++) {
+ if (output->buffers[i].status == V4L2BUF_AVAILABLE) {
+ return i;
+ }
+ }
+
+ return -1;
+}
+
+/** @copydoc v4l2_wrapper_set_current_buffer_index */
+void v4l2_wrapper_set_current_buffer_index(struct v4l2_wrapper_context *ctx, int index) {
+ struct V4L2Context *output = (struct V4L2Context *) ctx->output;
+
+ output->current_buffer_index = index;
+}
diff --git a/src/platform/linux/v4l2_wrapper.h b/src/platform/linux/v4l2_wrapper.h
new file mode 100644
index 00000000000..ede9b4231c6
--- /dev/null
+++ b/src/platform/linux/v4l2_wrapper.h
@@ -0,0 +1,68 @@
+/**
+ * @file src/platform/linux/v4l2_wrapper.h
+ * @brief Interface to the patched FFmpeg V4L2 M2M output-buffer internals.
+ *
+ * FFmpeg's private V4L2 headers are implemented for C and cannot be included
+ * safely from Sunshine's C++ V4L2 encode device. This C-compatible interface
+ * keeps those headers behind a small wrapper and exposes only the state and
+ * operations required by the zero-copy path.
+ */
+#pragma once
+
+// standard includes
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include
+
+ /**
+ * @brief State required to access an FFmpeg V4L2 M2M output queue.
+ *
+ * This structure exposes only the FFmpeg-private state needed by Sunshine's
+ * zero-copy V4L2 encode device, keeping those dependencies out of C++ code.
+ */
+ struct v4l2_wrapper_context {
+ AVCodecContext *avctx; ///< FFmpeg codec context that owns the V4L2 M2M context.
+
+ void *output; ///< Opaque pointer to FFmpeg's V4L2 output-queue context.
+ int fd; ///< File descriptor of the V4L2 M2M device.
+ struct v4l2_format format; ///< Negotiated format of the V4L2 output queue.
+ int width; ///< Negotiated output width in pixels.
+ int height; ///< Negotiated output height in pixels.
+ int num_buffers; ///< Number of buffers allocated for the V4L2 output queue.
+ };
+
+ /**
+ * @brief Initialize a wrapper context from an opened FFmpeg V4L2 M2M encoder.
+ *
+ * This also enables Sunshine's zero-copy buffer-selection path in the
+ * patched FFmpeg V4L2 output queue.
+ *
+ * @param ctx Wrapper context to initialize.
+ * @param avctx Opened FFmpeg V4L2 M2M encoder context.
+ * @return 0 on success, or -1 when a required input pointer is null.
+ */
+ int v4l2_wrapper_context_init(struct v4l2_wrapper_context *ctx, AVCodecContext *avctx);
+
+ /**
+ * @brief Dequeue completed output buffers and find one available for reuse.
+ *
+ * @param ctx Initialized wrapper context whose output queue is inspected.
+ * @return Zero-based buffer index on success, or -1 when none is available.
+ */
+ int v4l2_wrapper_getfree_v4l2buf_idx(struct v4l2_wrapper_context *ctx);
+
+ /**
+ * @brief Select the V4L2 output buffer used for the next submitted frame.
+ *
+ * @param ctx Initialized wrapper context whose output queue is updated.
+ * @param index Zero-based index of the output buffer to select.
+ */
+ void v4l2_wrapper_set_current_buffer_index(struct v4l2_wrapper_context *ctx, int index);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/src/platform/linux/wlgrab.cpp b/src/platform/linux/wlgrab.cpp
index 30be46e5fad..a6c52321925 100644
--- a/src/platform/linux/wlgrab.cpp
+++ b/src/platform/linux/wlgrab.cpp
@@ -10,6 +10,7 @@
#include "src/logging.h"
#include "src/platform/common.h"
#include "src/video.h"
+#include "v4l2.h"
#include "vaapi.h"
#include "wayland.h"
@@ -30,6 +31,12 @@ namespace wl {
}
#endif
+#ifdef SUNSHINE_BUILD_V4L2
+ if (hwdevice_type == platf::mem_type_e::v4l2) {
+ return true;
+ }
+#endif
+
return false;
}
@@ -337,6 +344,12 @@ namespace wl {
}
#endif
+#ifdef SUNSHINE_BUILD_V4L2
+ if (mem_type == platf::mem_type_e::v4l2) {
+ return v4l2::make_avcodec_encode_device(width, height, false);
+ }
+#endif
+
return std::make_unique();
}
@@ -473,6 +486,12 @@ namespace wl {
}
#endif
+#ifdef SUNSHINE_BUILD_V4L2
+ if (mem_type == platf::mem_type_e::v4l2) {
+ return v4l2::make_avcodec_encode_device(width, height, 0, 0, true);
+ }
+#endif
+
return std::make_unique();
}
@@ -497,7 +516,7 @@ namespace platf {
* @brief Create a Wayland capture backend for the requested memory type.
*/
std::shared_ptr 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::v4l2) {
BOOST_LOG(error) << "[wlgrab] Could not initialize display with the given hw device type."sv;
return nullptr;
}
diff --git a/src/platform/linux/x11grab.cpp b/src/platform/linux/x11grab.cpp
index a14b3b66d09..b69a46b3940 100644
--- a/src/platform/linux/x11grab.cpp
+++ b/src/platform/linux/x11grab.cpp
@@ -28,6 +28,7 @@
#include "src/platform/common.h"
#include "src/task_pool.h"
#include "src/video.h"
+#include "v4l2.h"
#include "vaapi.h"
#include "x11grab.h"
@@ -661,6 +662,12 @@ namespace platf {
}
#endif
+#ifdef SUNSHINE_BUILD_V4L2
+ if (mem_type == mem_type_e::v4l2) {
+ return v4l2::make_avcodec_encode_device(width, height, false);
+ }
+#endif
+
return std::make_unique();
}
@@ -888,7 +895,7 @@ namespace platf {
* @return X11 display backend, or nullptr when initialization fails.
*/
std::shared_ptr x11_display(platf::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::v4l2) {
BOOST_LOG(error) << "Could not initialize x11 display with the given hw device type"sv;
return nullptr;
}
diff --git a/src/video.cpp b/src/video.cpp
index 843a19a0a58..39b75d2f929 100644
--- a/src/video.cpp
+++ b/src/video.cpp
@@ -437,11 +437,15 @@ namespace video {
* @param avcodec_ctx Open FFmpeg codec context for the selected encoder.
* @param encode_device Platform encode device that supplies frames to FFmpeg.
* @param inject Whether SPS/VPS replacement data should be injected.
+ * @param first_idr_packets_cnt Number of packets needed to form the first IDR frame.
*/
- avcodec_encode_session_t(avcodec_ctx_t &&avcodec_ctx, std::unique_ptr encode_device, int inject):
+ avcodec_encode_session_t(avcodec_ctx_t &&avcodec_ctx, std::unique_ptr encode_device, int inject, uint8_t first_idr_packets_cnt):
avcodec_ctx {std::move(avcodec_ctx)},
device {std::move(encode_device)},
inject {inject} {
+ // If the first packet already contains the IDR frame, then we don't need to reconstruct the packet again.
+ packets_needed_cnt = first_idr_packets_cnt > 1 ? first_idr_packets_cnt : 0;
+ packets_needed.reserve(first_idr_packets_cnt);
}
/**
@@ -478,6 +482,8 @@ namespace video {
vps = std::move(other.vps);
inject = other.inject;
+ packets_needed_cnt = other.packets_needed_cnt;
+ packets_needed = std::move(other.packets_needed);
return *this;
}
@@ -528,6 +534,25 @@ namespace video {
request_idr_frame();
}
+ /**
+ * @brief Flush the remaining frames from the encoder. This function will make avcodec_ctx unusable.
+ *
+ * @return Vector of remaining packets.
+ */
+ std::vector> flush_rest_frames() {
+ std::vector> packets;
+
+ // Flush the encoder by passing nullptr as the frame
+ if (avcodec_send_frame(avcodec_ctx.get(), nullptr) == 0) {
+ do {
+ packets.emplace_back(std::make_unique());
+ } while (avcodec_receive_packet(avcodec_ctx.get(), packets.back()->av_packet) == 0);
+ packets.pop_back(); // Remove the last packet which is invalid
+ }
+
+ return packets;
+ }
+
avcodec_ctx_t avcodec_ctx; ///< FFmpeg codec context owned by the encode session.
std::unique_ptr device; ///< Platform device used by the FFmpeg hardware encoder.
@@ -538,6 +563,9 @@ namespace video {
// inject sps/vps data into idr pictures
int inject; ///< Number of upcoming IDR frames that should receive rewritten parameter sets.
+
+ uint8_t packets_needed_cnt; ///< Number of encoder packets still required to complete the first IDR frame.
+ std::vector> packets_needed; ///< First-IDR packets held until the frame is complete.
};
/**
@@ -1366,6 +1394,57 @@ namespace video {
LIMITED_GOP_SIZE | PARALLEL_ENCODING
};
#endif // SUNSHINE_BUILD_VULKAN
+
+ #ifdef SUNSHINE_BUILD_V4L2
+ /**
+ * @brief FFmpeg V4L2 M2M encoder configuration.
+ */
+ encoder_t v4l2m2m {
+ "v4l2m2m"sv,
+ std::make_unique(
+ AV_HWDEVICE_TYPE_NONE,
+ AV_HWDEVICE_TYPE_NONE,
+ AV_PIX_FMT_NONE,
+ AV_PIX_FMT_NV12,
+ AV_PIX_FMT_P010,
+ AV_PIX_FMT_NONE,
+ AV_PIX_FMT_NONE,
+ nullptr,
+ platf::mem_type_e::v4l2
+ ),
+ {
+ {}, // Common options
+ {}, // SDR-specific options
+ {}, // HDR-specific options
+ {}, // YUV444 SDR-specific options
+ {}, // YUV444 HDR-specific options
+ {}, // Fallback options
+ "av1_v4l2m2m"s,
+ {}, // capabilities
+ },
+ {
+ {}, // Common options
+ {}, // SDR-specific options
+ {}, // HDR-specific options
+ {}, // YUV444 SDR-specific options
+ {}, // YUV444 HDR-specific options
+ {}, // Fallback options
+ "hevc_v4l2m2m"s,
+ {}, // capabilities
+ },
+ {
+ {}, // Common options
+ {}, // SDR-specific options
+ {}, // HDR-specific options
+ {}, // YUV444 SDR-specific options
+ {}, // YUV444 HDR-specific options
+ {}, // Fallback options
+ "h264_v4l2m2m"s,
+ {}, // capabilities
+ },
+ PARALLEL_ENCODING
+ };
+ #endif // SUNSHINE_BUILD_V4L2
#endif // linux
#ifdef __APPLE__
@@ -1460,6 +1539,9 @@ namespace video {
&vulkan,
#endif
&vaapi,
+ #ifdef SUNSHINE_BUILD_V4L2
+ &v4l2m2m,
+ #endif
#endif
#ifdef __APPLE__
&videotoolbox,
@@ -1833,7 +1915,7 @@ namespace video {
auto &vps = session.vps;
// send the frame to the encoder
- auto ret = avcodec_send_frame(ctx.get(), frame);
+ auto ret = session.device->send_frame(ctx.get());
if (ret < 0) {
char err_str[AV_ERROR_MAX_STRING_SIZE] {0};
BOOST_LOG(error) << "Could not send a frame for encoding: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, ret);
@@ -1852,6 +1934,47 @@ namespace video {
return ret;
}
+ if (session.packets_needed_cnt) {
+ session.packets_needed_cnt--;
+
+ if (session.packets_needed_cnt > 0) {
+ BOOST_LOG(debug) << "Stashing packet "sv << session.packets_needed.size() << " for the first IDR frame";
+ session.packets_needed.emplace_back(std::move(packet));
+ continue;
+ }
+
+ BOOST_LOG(debug) << "Releasing stashed packets for the first IDR frame";
+ packet->av_packet = av_packet_alloc();
+ if (!packet->av_packet) {
+ BOOST_LOG(error) << "Failed to allocate AVPacket for stashed packets";
+ return -1;
+ }
+
+ int new_size = av_packet->size;
+ for (const auto &stashed_packet : session.packets_needed) {
+ new_size += stashed_packet->av_packet->size;
+ }
+ if (av_new_packet(packet->av_packet, new_size)) {
+ BOOST_LOG(error) << "Failed to allocate AVPacket data for stashed packets";
+ return -1;
+ }
+
+ int copy_offset = 0;
+ for (const auto &stashed_packet : session.packets_needed) {
+ memcpy(packet->av_packet->data + copy_offset, stashed_packet->av_packet->data, stashed_packet->av_packet->size);
+ copy_offset += stashed_packet->av_packet->size;
+ }
+ memcpy(packet->av_packet->data + copy_offset, av_packet->data, av_packet->size);
+ if (av_packet_copy_props(packet->av_packet, av_packet)) {
+ BOOST_LOG(error) << "Failed to copy AVPacket properties for stashed packets";
+ return -1;
+ }
+
+ av_packet_free(&av_packet);
+ av_packet = packet->av_packet;
+ session.packets_needed.clear();
+ }
+
if (av_packet->flags & AV_PKT_FLAG_KEY) {
BOOST_LOG(debug) << "Frame "sv << frame_nr << ": IDR Keyframe (AV_FRAME_FLAG_KEY)"sv;
}
@@ -2329,7 +2452,7 @@ namespace video {
encode_device_final = std::move(encode_device);
}
- if (encode_device_final->set_frame(frame.release(), ctx->hw_frames_ctx)) {
+ if (encode_device_final->set_frame(frame.release(), ctx->hw_frames_ctx) || encode_device_final->load_opened_context(ctx.get())) {
return nullptr;
}
@@ -2340,7 +2463,8 @@ namespace video {
std::move(encode_device_final),
// 0 ==> don't inject, 1 ==> inject for h264, 2 ==> inject for hevc
- config.videoFormat <= 1 ? (1 - static_cast(video_format[encoder_t::VUI_PARAMETERS])) * (1 + config.videoFormat) : 0
+ config.videoFormat <= 1 ? (1 - static_cast(video_format[encoder_t::VUI_PARAMETERS])) * (1 + config.videoFormat) : 0,
+ video_format.first_idr_packets_cnt
);
return session;
@@ -2996,10 +3120,28 @@ namespace video {
}
/**
- * @brief Enumerates supported validate flag options.
+ * @brief Result of a configuration validation.
*/
- enum validate_flag_e {
- VUI_PARAMS = 0x01, ///< VUI parameters
+ struct validate_result_t {
+ /**
+ * @brief Enumerates supported validate flag options.
+ */
+ enum validate_flag_e {
+ VUI_PARAMS = 0x01, ///< VUI parameters
+ };
+
+ bool success; ///< Whether the encoder accepted and successfully encoded the validation frame.
+ uint8_t flags; ///< Bitwise combination of validate_flag_e capabilities observed during validation.
+ uint8_t first_idr_packets_cnt; ///< Number of packets emitted for the first IDR frame, including separate parameter-set packets.
+
+ /**
+ * @brief Construct a failed validation result with all capabilities cleared.
+ *
+ * @return Failed validation result.
+ */
+ static constexpr validate_result_t error() {
+ return validate_result_t {false};
+ }
};
/**
@@ -3008,58 +3150,97 @@ namespace video {
* @param disp Display connection or display handle.
* @param encoder Encoder configuration or encoder instance.
* @param config Configuration values to apply.
- * @return 0 when the selected encoder/device accepts the configuration; nonzero otherwise.
+ * @return Validation status, detected capabilities, and first-IDR packet count.
*/
- int validate_config(std::shared_ptr disp, const encoder_t &encoder, const config_t &config) {
+ validate_result_t validate_config(std::shared_ptr disp, const encoder_t &encoder, const config_t &config) {
auto encode_device = make_encode_device(*disp, encoder, config);
if (!encode_device) {
- return -1;
+ return validate_result_t::error();
}
auto session = make_encode_session(disp.get(), encoder, config, disp->width, disp->height, std::move(encode_device));
if (!session) {
- return -1;
+ return validate_result_t::error();
}
{
// Image buffers are large, so we use a separate scope to free it immediately after convert()
auto img = disp->alloc_img();
if (!img || disp->dummy_img(img.get()) || session->convert(*img)) {
- return -1;
+ return validate_result_t::error();
}
}
session->request_idr_frame();
- auto packets = mail::man->queue(mail::video_packets);
- while (!packets->peek()) {
- if (encode(1, *session, packets, nullptr, {})) {
- return -1;
+ bool has_vui_params = false;
+ bool is_first_frame_idr = false;
+ uint8_t first_idr_packets_cnt = 0;
+
+ // libavcodec does not require that an IDR frame must be placed in the first packet;
+ // some encoders may output certain packets first (such as H.264 SPS and PPS).
+ if (auto avcodec_session = dynamic_cast(session.get())) {
+ avcodec_session->device->frame->pts = 1;
+ auto ret = avcodec_session->device->send_frame(avcodec_session->avcodec_ctx.get());
+ if (ret < 0) {
+ char err_str[AV_ERROR_MAX_STRING_SIZE] {0};
+ BOOST_LOG(error) << "Could not send a frame for encoding: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, ret);
+ return validate_result_t::error();
+ }
+
+ auto avcodec_packets = avcodec_session->flush_rest_frames();
+ if (avcodec_packets.size() > 5) {
+ BOOST_LOG(error) << "Received >5 packets for the first IDR frame (weird). Marking validation as failed."sv;
+ return validate_result_t::error();
+ }
+ if (avcodec_packets.empty()) {
+ BOOST_LOG(error) << "Received no packets for the first IDR frame. Marking validation as failed."sv;
+ return validate_result_t::error();
+ }
+
+ std::vector avcodec_packets_raw(avcodec_packets.size());
+ std::transform(avcodec_packets.begin(), avcodec_packets.end(), avcodec_packets_raw.begin(), [](auto &packet) {
+ return packet->av_packet;
+ });
+
+ is_first_frame_idr = std::any_of(avcodec_packets.begin(), avcodec_packets.end(), [](auto &packet) {
+ return packet->is_idr();
+ });
+ // This check only applies for H.264 and HEVC
+ has_vui_params = config.videoFormat > 1 || cbs::validate_sps(avcodec_packets_raw, config.videoFormat ? AV_CODEC_ID_H265 : AV_CODEC_ID_H264);
+ first_idr_packets_cnt = static_cast(avcodec_packets.size());
+ } else {
+ auto packets = mail::man->queue(mail::video_packets);
+ while (!packets->peek()) {
+ if (encode(1, *session, packets, nullptr, {})) {
+ return validate_result_t::error();
+ }
}
+
+ auto packet = packets->pop();
+ is_first_frame_idr = packet->is_idr();
+ // Don't check it for non-avcodec encoders.
+ has_vui_params = true;
+ first_idr_packets_cnt = 1;
}
- auto packet = packets->pop();
- if (!packet->is_idr()) {
+ if (!is_first_frame_idr) {
BOOST_LOG(error) << "First packet type is not an IDR frame"sv;
-
- return -1;
+ return validate_result_t::error();
+ }
+ if (!has_vui_params && first_idr_packets_cnt > 1) {
+ BOOST_LOG(error) << "Encoder splitting the first IDR frame into multiple packets without VUI parameters is not supported for now."sv;
+ return validate_result_t::error();
}
- int flag = 0;
+ validate_result_t result {true};
- // This check only applies for H.264 and HEVC
- if (config.videoFormat <= 1) {
- if (auto packet_avcodec = dynamic_cast(packet.get())) {
- if (cbs::validate_sps(packet_avcodec->av_packet, config.videoFormat ? AV_CODEC_ID_H265 : AV_CODEC_ID_H264)) {
- flag |= VUI_PARAMS;
- }
- } else {
- // Don't check it for non-avcodec encoders.
- flag |= VUI_PARAMS;
- }
+ if (has_vui_params) {
+ result.flags |= validate_result_t::VUI_PARAMS;
}
+ result.first_idr_packets_cnt = first_idr_packets_cnt;
- return flag;
+ return result;
}
/**
@@ -3082,8 +3263,8 @@ namespace video {
encoder.av1.capabilities.set();
// First, test encoder viability
- config_t config_max_ref_frames {1920, 1080, 60, 6000, 1000, 1, 1, 1, 0, 0, 0, 0};
- config_t config_autoselect {1920, 1080, 60, 6000, 1000, 1, 0, 1, 0, 0, 0, 0};
+ config_t config_max_ref_frames {1920, 1080, 30, 3000, 1000, 1, 1, 1, 0, 0, 0, 0};
+ config_t config_autoselect {1920, 1080, 30, 3000, 1000, 1, 0, 1, 0, 0, 0, 0};
// If the encoder isn't supported at all (not even H.264), bail early
reset_display(disp, encoder.platform_formats->dev_type, output_name, config_autoselect);
@@ -3098,24 +3279,25 @@ namespace video {
// If we're expecting failure, use the autoselect ref config first since that will always succeed
// if the encoder is available.
- auto max_ref_frames_h264 = expect_failure ? -1 : validate_config(disp, encoder, config_max_ref_frames);
- auto autoselect_h264 = max_ref_frames_h264 >= 0 ? max_ref_frames_h264 : validate_config(disp, encoder, config_autoselect);
- if (autoselect_h264 < 0) {
+ auto max_ref_frames_h264 = expect_failure ? validate_result_t::error() : validate_config(disp, encoder, config_max_ref_frames);
+ auto autoselect_h264 = max_ref_frames_h264.success ? max_ref_frames_h264 : validate_config(disp, encoder, config_autoselect);
+ if (!autoselect_h264.success) {
return false;
} else if (expect_failure) {
// We expected failure, but actually succeeded. Do the max_ref_frames probe we skipped.
max_ref_frames_h264 = validate_config(disp, encoder, config_max_ref_frames);
}
- std::vector> packet_deficiencies {
- {VUI_PARAMS, encoder_t::VUI_PARAMETERS},
+ std::vector> packet_deficiencies {
+ {validate_result_t::VUI_PARAMS, encoder_t::VUI_PARAMETERS},
};
for (auto [validate_flag, encoder_flag] : packet_deficiencies) {
- encoder.h264[encoder_flag] = (max_ref_frames_h264 & validate_flag && autoselect_h264 & validate_flag);
+ encoder.h264[encoder_flag] = (max_ref_frames_h264.flags & validate_flag && autoselect_h264.flags & validate_flag);
}
- encoder.h264[encoder_t::REF_FRAMES_RESTRICT] = max_ref_frames_h264 >= 0;
+ encoder.h264.first_idr_packets_cnt = autoselect_h264.first_idr_packets_cnt;
+ encoder.h264[encoder_t::REF_FRAMES_RESTRICT] = max_ref_frames_h264.success;
encoder.h264[encoder_t::PASSED] = true;
if (test_hevc) {
@@ -3127,16 +3309,17 @@ namespace video {
// If H.264 succeeded with max ref frames specified, assume that we can count on
// HEVC to also succeed with max ref frames specified if HEVC is supported.
- auto autoselect_hevc = (max_ref_frames_hevc >= 0 || max_ref_frames_h264 >= 0) ?
+ auto autoselect_hevc = (max_ref_frames_hevc.success || max_ref_frames_h264.success) ?
max_ref_frames_hevc :
validate_config(disp, encoder, config_autoselect);
for (auto [validate_flag, encoder_flag] : packet_deficiencies) {
- encoder.hevc[encoder_flag] = (max_ref_frames_hevc & validate_flag && autoselect_hevc & validate_flag);
+ encoder.hevc[encoder_flag] = (max_ref_frames_hevc.flags & validate_flag && autoselect_hevc.flags & validate_flag);
}
- encoder.hevc[encoder_t::REF_FRAMES_RESTRICT] = max_ref_frames_hevc >= 0;
- encoder.hevc[encoder_t::PASSED] = max_ref_frames_hevc >= 0 || autoselect_hevc >= 0;
+ encoder.hevc.first_idr_packets_cnt = autoselect_hevc.first_idr_packets_cnt;
+ encoder.hevc[encoder_t::REF_FRAMES_RESTRICT] = max_ref_frames_hevc.success;
+ encoder.hevc[encoder_t::PASSED] = max_ref_frames_hevc.success || autoselect_hevc.success;
} else {
BOOST_LOG(info) << "Encoder ["sv << encoder.hevc.name << "] is not supported on this GPU"sv;
encoder.hevc.capabilities.reset();
@@ -3155,16 +3338,17 @@ namespace video {
// If H.264 succeeded with max ref frames specified, assume that we can count on
// AV1 to also succeed with max ref frames specified if AV1 is supported.
- auto autoselect_av1 = (max_ref_frames_av1 >= 0 || max_ref_frames_h264 >= 0) ?
+ auto autoselect_av1 = (max_ref_frames_av1.success || max_ref_frames_h264.success) ?
max_ref_frames_av1 :
validate_config(disp, encoder, config_autoselect);
for (auto [validate_flag, encoder_flag] : packet_deficiencies) {
- encoder.av1[encoder_flag] = (max_ref_frames_av1 & validate_flag && autoselect_av1 & validate_flag);
+ encoder.av1[encoder_flag] = (max_ref_frames_av1.flags & validate_flag && autoselect_av1.flags & validate_flag);
}
- encoder.av1[encoder_t::REF_FRAMES_RESTRICT] = max_ref_frames_av1 >= 0;
- encoder.av1[encoder_t::PASSED] = max_ref_frames_av1 >= 0 || autoselect_av1 >= 0;
+ encoder.av1.first_idr_packets_cnt = autoselect_av1.first_idr_packets_cnt;
+ encoder.av1[encoder_t::REF_FRAMES_RESTRICT] = max_ref_frames_av1.success;
+ encoder.av1[encoder_t::PASSED] = max_ref_frames_av1.success || autoselect_av1.success;
} else {
BOOST_LOG(info) << "Encoder ["sv << encoder.av1.name << "] is not supported on this GPU"sv;
encoder.av1.capabilities.reset();
@@ -3189,7 +3373,7 @@ namespace video {
auto encoder_codec_name = encoder.codec_from_config(config).name;
- 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).success) {
flag_map[encoder_t::YUV444] = true;
} else {
flag_map[encoder_t::YUV444] = false;
@@ -3209,7 +3393,7 @@ namespace video {
auto encoder_codec_name = encoder.codec_from_config(config).name;
- 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).success) {
flag_map[encoder_t::DYNAMIC_RANGE] = true;
} else {
flag_map[encoder_t::DYNAMIC_RANGE] = false;
@@ -3229,7 +3413,7 @@ namespace video {
auto encoder_codec_name = encoder.codec_from_config(config).name;
- 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).success) {
flag_map[encoder_t::DYNAMIC_RANGE_YUV444] = true;
} else {
flag_map[encoder_t::DYNAMIC_RANGE_YUV444] = false;
diff --git a/src/video.h b/src/video.h
index 63f3cf55cbf..080844dcbe3 100644
--- a/src/video.h
+++ b/src/video.h
@@ -219,6 +219,7 @@ namespace video {
* @param avcodec_pix_fmt_yuv444_8bit AVCodec 8-bit YUV444 pixel format.
* @param avcodec_pix_fmt_yuv444_10bit AVCodec 10-bit YUV444 pixel format.
* @param init_avcodec_hardware_input_buffer_function Hardware input buffer initialization callback.
+ * @param dev_type Explicit platform memory type, or unknown to derive it from avcodec_base_dev_type.
*/
encoder_platform_formats_avcodec(
const AVHWDeviceType &avcodec_base_dev_type,
@@ -228,7 +229,10 @@ namespace video {
const AVPixelFormat &avcodec_pix_fmt_10bit,
const AVPixelFormat &avcodec_pix_fmt_yuv444_8bit,
const AVPixelFormat &avcodec_pix_fmt_yuv444_10bit,
- const init_buffer_function_t &init_avcodec_hardware_input_buffer_function
+ const init_buffer_function_t &init_avcodec_hardware_input_buffer_function,
+ // Some encoders like V4L2 manage input buffers themselves and use AV_HWDEVICE_TYPE_NONE to avoid
+ // Sunshine's hwdevice/hwframes setup, so allow them to override the default device-type mapping.
+ const platf::mem_type_e &dev_type = platf::mem_type_e::unknown
):
avcodec_base_dev_type {avcodec_base_dev_type},
avcodec_derived_dev_type {avcodec_derived_dev_type},
@@ -238,7 +242,7 @@ namespace video {
avcodec_pix_fmt_yuv444_8bit {avcodec_pix_fmt_yuv444_8bit},
avcodec_pix_fmt_yuv444_10bit {avcodec_pix_fmt_yuv444_10bit},
init_avcodec_hardware_input_buffer {init_avcodec_hardware_input_buffer_function} {
- dev_type = map_base_dev_type(avcodec_base_dev_type);
+ this->dev_type = (dev_type == platf::mem_type_e::unknown ? map_base_dev_type(avcodec_base_dev_type) : dev_type);
pix_fmt_8bit = map_pix_fmt(avcodec_pix_fmt_8bit);
pix_fmt_10bit = map_pix_fmt(avcodec_pix_fmt_10bit);
pix_fmt_yuv444_8bit = map_pix_fmt(avcodec_pix_fmt_yuv444_8bit);
@@ -369,6 +373,9 @@ namespace video {
std::string name; ///< Codec name passed to the encoder backend.
std::bitset capabilities; ///< Capability flags supported by this codec on the encoder.
+ // libavcodec does not require that an IDR frame must be placed in the first packet;
+ // some encoders may output certain packets first (such as H.264 SPS and PPS).
+ std::uint8_t first_idr_packets_cnt = 0; ///< Number of encoder packets combined to emit the first complete IDR frame.
/**
* @brief Test whether a codec capability is enabled.
@@ -468,6 +475,10 @@ namespace video {
extern encoder_t vaapi;
#endif
+#ifdef SUNSHINE_BUILD_V4L2
+ extern encoder_t v4l2m2m;
+#endif
+
#ifdef __APPLE__
extern encoder_t videotoolbox;
#endif
diff --git a/src_assets/common/assets/web/configs/tabs/Advanced.vue b/src_assets/common/assets/web/configs/tabs/Advanced.vue
index bad302fddef..35f5abc7177 100644
--- a/src_assets/common/assets/web/configs/tabs/Advanced.vue
+++ b/src_assets/common/assets/web/configs/tabs/Advanced.vue
@@ -105,6 +105,7 @@ const config = ref(props.config)
+
diff --git a/tests/unit/platform/linux/test_wayland.cpp b/tests/unit/platform/linux/test_wayland.cpp
index 98b221a9dde..0ce2ca7f705 100644
--- a/tests/unit/platform/linux/test_wayland.cpp
+++ b/tests/unit/platform/linux/test_wayland.cpp
@@ -92,6 +92,14 @@ TEST(WaylandCaptureTest, UsesVramForVaapi) {
EXPECT_TRUE(wl::use_vram_capture(platf::mem_type_e::vaapi));
}
+TEST(WaylandCaptureTest, UsesVramForV4l2OnlyWhenV4l2SupportIsBuilt) {
+ #ifdef SUNSHINE_BUILD_V4L2
+ EXPECT_TRUE(wl::use_vram_capture(platf::mem_type_e::v4l2));
+ #else
+ EXPECT_FALSE(wl::use_vram_capture(platf::mem_type_e::v4l2));
+ #endif
+}
+
TEST(WaylandCaptureTest, UsesSystemMemoryForSoftwareEncoding) {
EXPECT_FALSE(wl::use_vram_capture(platf::mem_type_e::system));
}
diff --git a/tests/unit/test_video.cpp b/tests/unit/test_video.cpp
index a7f04260848..af417cd074a 100644
--- a/tests/unit/test_video.cpp
+++ b/tests/unit/test_video.cpp
@@ -56,6 +56,9 @@ INSTANTIATE_TEST_SUITE_P(
#if defined(__linux__) || defined(__FreeBSD__)
&video::vaapi,
#endif
+#ifdef SUNSHINE_BUILD_V4L2
+ &video::v4l2m2m,
+#endif
#ifdef __APPLE__
&video::videotoolbox,
#endif
diff --git a/third-party/build-deps b/third-party/build-deps
index a1fe2841cbc..ebf2e2f33b1 160000
--- a/third-party/build-deps
+++ b/third-party/build-deps
@@ -1 +1 @@
-Subproject commit a1fe2841cbc0d8c4501a1006d2d1cb88f219cc8c
+Subproject commit ebf2e2f33b195c029eff8dec88455b8fd5309ad5