diff --git a/src/platform/linux/wayland.cpp b/src/platform/linux/wayland.cpp index ca17eb070..ee3f2c3ed 100644 --- a/src/platform/linux/wayland.cpp +++ b/src/platform/linux/wayland.cpp @@ -34,6 +34,16 @@ using namespace std::literals; namespace wl { + namespace { + const gbm_bo_accessors_t gbm_bo_accessors { + .get_plane_count = gbm_bo_get_plane_count, + .get_fd_for_plane = gbm_bo_get_fd_for_plane, + .get_stride_for_plane = gbm_bo_get_stride_for_plane, + .get_offset = gbm_bo_get_offset, + .get_modifier = gbm_bo_get_modifier, + }; + } // namespace + // Helper to call C++ method from wayland C callback template static auto classCall(void *data, Params... params) -> decltype(((*reinterpret_cast(data)).*m)(params...)) { @@ -202,8 +212,10 @@ namespace wl { } void interface_t::dmabuf_modifier(zwp_linux_dmabuf_v1 *zwp_linux_dmabuf, uint32_t format, uint32_t modifier_hi, uint32_t modifier_lo) { - uint64_t modifier = ((uint64_t) modifier_hi << 32) | modifier_lo; - supported_modifiers[format].push_back(modifier); + const auto modifier = (static_cast(modifier_hi) << 32) | modifier_lo; + if (modifier != DRM_FORMAT_MOD_INVALID) { + supported_modifiers[format].push_back(modifier); + } } void interface_t::add_interface( @@ -373,6 +385,31 @@ namespace wl { BOOST_LOG(verbose) << "Frame flags: "sv << flags << (y_invert ? " (y_invert)" : ""); } + std::optional export_gbm_bo_planes(gbm_bo *bo, frame_t &frame, const gbm_bo_accessors_t &accessors) { + frame.destroy(); + + const auto plane_count = accessors.get_plane_count(bo); + if (plane_count <= 0 || plane_count > static_cast(std::size(frame.sd.fds))) { + BOOST_LOG(error) << "[wayland] GBM buffer has unsupported plane count ["sv << plane_count << ']'; + return std::nullopt; + } + + frame.sd.modifier = accessors.get_modifier(bo); + for (auto plane = 0; plane < plane_count; ++plane) { + frame.sd.fds[plane] = accessors.get_fd_for_plane(bo, plane); + if (frame.sd.fds[plane] < 0) { + BOOST_LOG(error) << "[wayland] Failed to export DMA-BUF plane ["sv << plane << ']'; + frame.destroy(); + return std::nullopt; + } + + frame.sd.pitches[plane] = accessors.get_stride_for_plane(bo, plane); + frame.sd.offsets[plane] = accessors.get_offset(bo, plane); + } + + return static_cast(plane_count); + } + // DMA-BUF creation helper void dmabuf_t::create_and_copy_dmabuf(zwlr_screencopy_frame_v1 *frame) { if (!init_gbm()) { @@ -386,7 +423,7 @@ namespace wl { if (supported_modifiers) { auto it = supported_modifiers->find(dmabuf_info.format); if (it != supported_modifiers->end() && !it->second.empty()) { - current_bo = gbm_bo_create_with_modifiers(gbm_device, dmabuf_info.width, dmabuf_info.height, dmabuf_info.format, it->second.data(), it->second.size()); + current_bo = gbm_bo_create_with_modifiers2(gbm_device, dmabuf_info.width, dmabuf_info.height, dmabuf_info.format, it->second.data(), it->second.size(), GBM_BO_USE_RENDERING); } } @@ -401,10 +438,10 @@ namespace wl { return; } - // Get buffer info - int fd = gbm_bo_get_fd(current_bo); - if (fd < 0) { - BOOST_LOG(error) << "Failed to get buffer FD"sv; + // Export every memory plane into the surface descriptor + auto next_frame = get_next_frame(); + const auto plane_count = export_gbm_bo_planes(current_bo, *next_frame, gbm_bo_accessors); + if (!plane_count) { gbm_bo_destroy(current_bo); current_bo = nullptr; zwlr_screencopy_frame_v1_destroy(frame); @@ -412,19 +449,19 @@ namespace wl { return; } - uint32_t stride = gbm_bo_get_stride(current_bo); - uint64_t modifier = gbm_bo_get_modifier(current_bo); - - // Store in surface descriptor for later use - auto next_frame = get_next_frame(); - next_frame->sd.fds[0] = fd; - next_frame->sd.pitches[0] = stride; - next_frame->sd.offsets[0] = 0; - next_frame->sd.modifier = modifier; - // Create linux-dmabuf buffer auto params = zwp_linux_dmabuf_v1_create_params(dmabuf_interface); - zwp_linux_buffer_params_v1_add(params, fd, 0, 0, stride, modifier >> 32, modifier & 0xffffffff); + for (std::uint32_t plane = 0; plane < *plane_count; ++plane) { + zwp_linux_buffer_params_v1_add( + params, + next_frame->sd.fds[plane], + plane, + next_frame->sd.offsets[plane], + next_frame->sd.pitches[plane], + next_frame->sd.modifier >> 32, + next_frame->sd.modifier & 0xffffffff + ); + } // Add listener for buffer creation zwp_linux_buffer_params_v1_add_listener(params, ¶ms_listener, frame); @@ -485,6 +522,7 @@ namespace wl { BOOST_LOG(error) << "[wayland] Failed to create buffer from params"sv; self->cleanup_gbm(); + self->get_next_frame()->destroy(); zwp_linux_buffer_params_v1_destroy(params); zwlr_screencopy_frame_v1_destroy(frame); diff --git a/src/platform/linux/wayland.h b/src/platform/linux/wayland.h index 1514b1925..5cd24bf0b 100644 --- a/src/platform/linux/wayland.h +++ b/src/platform/linux/wayland.h @@ -5,9 +5,11 @@ #pragma once // standard includes +#include #include #include #include +#include #include #ifdef SUNSHINE_BUILD_WAYLAND @@ -25,6 +27,8 @@ */ #ifdef SUNSHINE_BUILD_WAYLAND +struct gbm_bo; + namespace wl { /** * @brief Determine whether wlroots capture should keep frames in VRAM for the requested memory type. @@ -39,6 +43,17 @@ namespace wl { */ using display_internal_t = util::safe_ptr; + /** + * @brief GBM buffer accessors used to export DMA-BUF plane metadata. + */ + struct gbm_bo_accessors_t { + int (*get_plane_count)(gbm_bo *bo); ///< Return the number of memory planes in the buffer. + int (*get_fd_for_plane)(gbm_bo *bo, int plane); ///< Duplicate the DMA-BUF descriptor for one plane. + std::uint32_t (*get_stride_for_plane)(gbm_bo *bo, int plane); ///< Return the row stride for one plane. + std::uint32_t (*get_offset)(gbm_bo *bo, int plane); ///< Return the byte offset for one plane. + std::uint64_t (*get_modifier)(gbm_bo *bo); ///< Return the DRM format modifier shared by the planes. + }; + /** * @brief Captured Wayland frame metadata and DMA-BUF surface state. */ @@ -54,6 +69,16 @@ namespace wl { std::optional frame_timestamp; ///< Capture timestamp associated with the frame. }; + /** + * @brief Export every GBM buffer plane into a captured frame descriptor. + * + * @param bo GBM buffer whose DMA-BUF planes will be exported. + * @param frame Frame that takes ownership of the exported file descriptors. + * @param accessors GBM accessors used to query the buffer. + * @return Number of exported planes, or no value when the buffer cannot be exported. + */ + std::optional export_gbm_bo_planes(gbm_bo *bo, frame_t &frame, const gbm_bo_accessors_t &accessors); + /** * @brief Listener state for Wayland screencopy frames backed by DMA-BUFs. */ @@ -358,12 +383,13 @@ namespace wl { */ void dmabuf_format(zwp_linux_dmabuf_v1 *zwp_linux_dmabuf, uint32_t format); /** - * @brief Record a DMA-BUF format modifier advertised by the compositor. + * @brief Record an explicit DMA-BUF format modifier advertised by the compositor. * * @param zwp_linux_dmabuf DMA-BUF interface that emitted the modifier event. * @param format DRM format associated with the modifier. * @param modifier_hi High 32 bits of the DRM format modifier. * @param modifier_lo Low 32 bits of the DRM format modifier. + * @note The combined DRM_FORMAT_MOD_INVALID value is omitted because implicit allocation uses the fallback path. */ void dmabuf_modifier(zwp_linux_dmabuf_v1 *zwp_linux_dmabuf, uint32_t format, uint32_t modifier_hi, uint32_t modifier_lo); diff --git a/tests/unit/platform/linux/test_wayland.cpp b/tests/unit/platform/linux/test_wayland.cpp index ce851bff3..98b221a9d 100644 --- a/tests/unit/platform/linux/test_wayland.cpp +++ b/tests/unit/platform/linux/test_wayland.cpp @@ -3,12 +3,70 @@ * @brief Test Wayland output mode selection. */ #ifdef SUNSHINE_BUILD_WAYLAND + // standard includes + #include + #include + + // system includes + #include + #include + #include + // test includes #include "../../../tests_common.h" // local includes #include +namespace { + struct fake_gbm_bo_t { + int plane_count {}; + int failed_plane {-1}; + std::uint64_t modifier {}; + std::array strides {}; + std::array offsets {}; + std::array exported_fds {-1, -1, -1, -1}; + }; + + fake_gbm_bo_t &fake_bo(gbm_bo *bo) { + return *static_cast(static_cast(bo)); + } + + int get_plane_count(gbm_bo *bo) { + return fake_bo(bo).plane_count; + } + + int get_fd_for_plane(gbm_bo *bo, int plane) { + auto &fake = fake_bo(bo); + if (plane == fake.failed_plane) { + return -1; + } + + fake.exported_fds[plane] = open("/dev/null", O_RDONLY | O_CLOEXEC); + return fake.exported_fds[plane]; + } + + std::uint32_t get_stride_for_plane(gbm_bo *bo, int plane) { + return fake_bo(bo).strides[plane]; + } + + std::uint32_t get_offset(gbm_bo *bo, int plane) { + return fake_bo(bo).offsets[plane]; + } + + std::uint64_t get_modifier(gbm_bo *bo) { + return fake_bo(bo).modifier; + } + + const wl::gbm_bo_accessors_t fake_accessors { + .get_plane_count = get_plane_count, + .get_fd_for_plane = get_fd_for_plane, + .get_stride_for_plane = get_stride_for_plane, + .get_offset = get_offset, + .get_modifier = get_modifier, + }; +} // namespace + TEST(WaylandMonitorTest, IgnoresNonCurrentModesAroundCurrentMode) { wl::monitor_t monitor {nullptr}; @@ -45,4 +103,69 @@ TEST(WaylandCaptureTest, UsesVramForCudaOnlyWhenCudaSupportIsBuilt) { EXPECT_FALSE(wl::use_vram_capture(platf::mem_type_e::cuda)); #endif } + +TEST(WaylandInterfaceTest, RecordsOnlyExplicitDmabufModifiers) { + constexpr std::uint32_t format = DRM_FORMAT_XRGB8888; + constexpr std::uint64_t explicit_modifier = 0x100000000000004; + wl::interface_t interface; + + interface.dmabuf_modifier(nullptr, format, DRM_FORMAT_MOD_INVALID >> 32, DRM_FORMAT_MOD_INVALID & 0xffffffff); + interface.dmabuf_modifier(nullptr, format, explicit_modifier >> 32, explicit_modifier & 0xffffffff); + + const auto modifiers = interface.supported_modifiers.find(format); + ASSERT_NE(modifiers, interface.supported_modifiers.end()); + ASSERT_EQ(modifiers->second.size(), 1); + EXPECT_EQ(modifiers->second.front(), explicit_modifier); +} + +TEST(WaylandCaptureTest, ExportsEveryGbmBufferPlane) { + fake_gbm_bo_t bo { + .plane_count = 2, + .modifier = 0x100000000000004, + .strides = {10240, 256}, + .offsets = {0, 0x800000}, + }; + wl::frame_t frame; + + const auto plane_count = wl::export_gbm_bo_planes(static_cast(static_cast(&bo)), frame, fake_accessors); + + ASSERT_EQ(plane_count, 2); + EXPECT_EQ(frame.sd.modifier, bo.modifier); + for (std::size_t plane = 0; plane < *plane_count; ++plane) { + EXPECT_EQ(frame.sd.fds[plane], bo.exported_fds[plane]); + EXPECT_EQ(frame.sd.pitches[plane], bo.strides[plane]); + EXPECT_EQ(frame.sd.offsets[plane], bo.offsets[plane]); + } + EXPECT_EQ(frame.sd.fds[2], -1); + EXPECT_EQ(frame.sd.fds[3], -1); + + frame.destroy(); +} + +TEST(WaylandCaptureTest, RejectsUnsupportedGbmPlaneCounts) { + wl::frame_t frame; + fake_gbm_bo_t empty_bo {}; + fake_gbm_bo_t oversized_bo { + .plane_count = 5, + }; + + EXPECT_FALSE(wl::export_gbm_bo_planes(static_cast(static_cast(&empty_bo)), frame, fake_accessors)); + EXPECT_FALSE(wl::export_gbm_bo_planes(static_cast(static_cast(&oversized_bo)), frame, fake_accessors)); +} + +TEST(WaylandCaptureTest, ClosesExportedPlanesAfterLaterPlaneFails) { + fake_gbm_bo_t bo { + .plane_count = 2, + .failed_plane = 1, + }; + wl::frame_t frame; + + EXPECT_FALSE(wl::export_gbm_bo_planes(static_cast(static_cast(&bo)), frame, fake_accessors)); + ASSERT_GE(bo.exported_fds[0], 0); + const auto fd_status = fcntl(bo.exported_fds[0], F_GETFD); + const auto fd_error = errno; + EXPECT_EQ(fd_status, -1); + EXPECT_EQ(fd_error, EBADF); + EXPECT_EQ(frame.sd.fds[0], -1); +} #endif