From 6ac36937ee556ce5f4aae99f9a56870b446c769c Mon Sep 17 00:00:00 2001 From: GenshinImpactStarts <12211831@mail.sustech.edu.cn> Date: Sat, 26 Sep 2026 16:26:08 +0000 Subject: [PATCH 1/7] feat(linux): add V4L2 encoder --- src/platform/common.h | 34 ++ src/platform/linux/graphics.cpp | 29 ++ src/platform/linux/graphics.h | 9 + src/platform/linux/v4l2.cpp | 428 +++++++++++++++++++++ src/platform/linux/v4l2.h | 46 +++ src/platform/linux/v4l2_wrapper.c | 61 +++ src/platform/linux/v4l2_wrapper.h | 68 ++++ src/video.cpp | 58 ++- src/video.h | 12 +- tests/unit/platform/linux/test_wayland.cpp | 8 + tests/unit/test_video.cpp | 3 + 11 files changed, 752 insertions(+), 4 deletions(-) create mode 100644 src/platform/linux/v4l2.cpp create mode 100644 src/platform/linux/v4l2.h create mode 100644 src/platform/linux/v4l2_wrapper.c create mode 100644 src/platform/linux/v4l2_wrapper.h 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..9c51f10721b 100644 --- a/src/platform/linux/graphics.cpp +++ b/src/platform/linux/graphics.cpp @@ -1527,6 +1527,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..de65e4652d7 100644 --- a/src/platform/linux/graphics.h +++ b/src/platform/linux/graphics.h @@ -724,6 +724,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/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/video.cpp b/src/video.cpp index 843a19a0a58..9838d365bdf 100644 --- a/src/video.cpp +++ b/src/video.cpp @@ -1366,6 +1366,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 +1511,9 @@ namespace video { &vulkan, #endif &vaapi, + #ifdef SUNSHINE_BUILD_V4L2 + &v4l2m2m, + #endif #endif #ifdef __APPLE__ &videotoolbox, @@ -1833,7 +1887,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); @@ -2329,7 +2383,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; } diff --git a/src/video.h b/src/video.h index 63f3cf55cbf..f151b8c20f1 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); @@ -468,6 +472,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/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 From 4dfed972f3231283c6b5c058f19225c07540b3d6 Mon Sep 17 00:00:00 2001 From: GenshinImpactStarts <12211831@mail.sustech.edu.cn> Date: Sat, 26 Sep 2026 16:26:54 +0000 Subject: [PATCH 2/7] build(linux): enable V4L2 encoder support --- cmake/compile_definitions/linux.cmake | 16 ++++++++++++++++ cmake/prep/options.cmake | 2 ++ 2 files changed, 18 insertions(+) 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 From 76606aa0efadf79c9f96619e317eeb41663f272b Mon Sep 17 00:00:00 2001 From: GenshinImpactStarts <12211831@mail.sustech.edu.cn> Date: Mon, 14 Sep 2026 06:16:00 +0000 Subject: [PATCH 3/7] fix(video): support multi-packet V4L2 validation --- src/cbs.cpp | 35 ++++---- src/cbs.h | 6 +- src/video.cpp | 230 +++++++++++++++++++++++++++++++++++++++----------- src/video.h | 3 + 4 files changed, 204 insertions(+), 70 deletions(-) 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/video.cpp b/src/video.cpp index 9838d365bdf..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. }; /** @@ -1906,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; } @@ -2394,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; @@ -3050,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}; + } }; /** @@ -3062,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; } /** @@ -3136,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); @@ -3152,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) { @@ -3181,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(); @@ -3209,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(); @@ -3243,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; @@ -3263,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; @@ -3283,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 f151b8c20f1..080844dcbe3 100644 --- a/src/video.h +++ b/src/video.h @@ -373,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. From 04dbab3067c52e91560dc43075ee3f6f291a4337 Mon Sep 17 00:00:00 2001 From: GenshinImpactStarts <12211831@mail.sustech.edu.cn> Date: Sat, 26 Sep 2026 16:34:45 +0000 Subject: [PATCH 4/7] fix(egl): support KHR image import --- src/platform/linux/graphics.cpp | 107 +++++++++++++++++++++++++++----- src/platform/linux/graphics.h | 43 ++++++++++--- 2 files changed, 126 insertions(+), 24 deletions(-) diff --git a/src/platform/linux/graphics.cpp b/src/platform/linux/graphics.cpp index 9c51f10721b..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) diff --git a/src/platform/linux/graphics.h b/src/platform/linux/graphics.h index de65e4652d7..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); } }); From ff082e3f7752faec6c668fda214bffacaab8688b Mon Sep 17 00:00:00 2001 From: GenshinImpactStarts <12211831@mail.sustech.edu.cn> Date: Mon, 14 Sep 2026 06:22:08 +0000 Subject: [PATCH 5/7] feat(linux): integrate V4L2 with display backends --- src/platform/linux/kmsgrab.cpp | 25 +++++++++++++++++++++++-- src/platform/linux/pipewire.cpp | 11 ++++++++++- src/platform/linux/wlgrab.cpp | 21 ++++++++++++++++++++- src/platform/linux/x11grab.cpp | 9 ++++++++- 4 files changed, 61 insertions(+), 5 deletions(-) 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/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; } From c9c252b054e965233ed8c5ab7381dc8bcb0c431b Mon Sep 17 00:00:00 2001 From: GenshinImpactStarts <12211831@mail.sustech.edu.cn> Date: Mon, 14 Sep 2026 06:22:23 +0000 Subject: [PATCH 6/7] feat(web): add V4L2 encoder option --- docs/configuration.md | 7 ++++++- src_assets/common/assets/web/configs/tabs/Advanced.vue | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) 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_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) +