/** * @file src/platform/linux/wayland.cpp * @brief Definitions for Wayland capture. */ // standard includes #include // platform includes #include #include #include #include #include #include #include #include // local includes #include "graphics.h" #include "src/logging.h" #include "src/platform/common.h" #include "src/round_robin.h" #include "src/utility.h" #include "wayland.h" extern const wl_interface wl_output_interface; using namespace std::literals; // Disable warning for converting incompatible functions #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wpedantic" #pragma GCC diagnostic ignored "-Wpmf-conversions" namespace wl { // Helper to call C++ method from wayland C callback template static auto classCall(void *data, Params... params) -> decltype(((*reinterpret_cast(data)).*m)(params...)) { return ((*reinterpret_cast(data)).*m)(params...); } #define CLASS_CALL(c, m) classCall // Define buffer params listener static const struct zwp_linux_buffer_params_v1_listener params_listener = { .created = dmabuf_t::buffer_params_created, .failed = dmabuf_t::buffer_params_failed }; int display_t::init(const char *display_name) { if (!display_name) { display_name = std::getenv("WAYLAND_DISPLAY"); } if (!display_name) { BOOST_LOG(error) << "Environment variable WAYLAND_DISPLAY has not been defined"sv; return -1; } display_internal.reset(wl_display_connect(display_name)); if (!display_internal) { BOOST_LOG(error) << "Couldn't connect to Wayland display: "sv << display_name; return -1; } BOOST_LOG(info) << "Found display ["sv << display_name << ']'; return 0; } void display_t::roundtrip() { wl_display_roundtrip(display_internal.get()); } /** * @brief Waits up to the specified timeout to dispatch new events on the wl_display. * @param timeout The timeout in milliseconds. * @return `true` if new events were dispatched or `false` if the timeout expired. */ bool display_t::dispatch(std::chrono::milliseconds timeout) { // Check if any events are queued already. If not, flush // outgoing events, and prepare to wait for readability. if (wl_display_prepare_read(display_internal.get()) == 0) { wl_display_flush(display_internal.get()); // Wait for an event to come in struct pollfd pfd = {}; pfd.fd = wl_display_get_fd(display_internal.get()); pfd.events = POLLIN; if (poll(&pfd, 1, timeout.count()) == 1 && (pfd.revents & POLLIN)) { // Read the new event(s) wl_display_read_events(display_internal.get()); } else { // We timed out, so unlock the queue now wl_display_cancel_read(display_internal.get()); return false; } } // Dispatch any existing or new pending events wl_display_dispatch_pending(display_internal.get()); return true; } wl_registry *display_t::registry() { return wl_display_get_registry(display_internal.get()); } inline monitor_t::monitor_t(wl_output *output): output {output}, wl_listener { &CLASS_CALL(monitor_t, wl_geometry), &CLASS_CALL(monitor_t, wl_mode), &CLASS_CALL(monitor_t, wl_done), &CLASS_CALL(monitor_t, wl_scale), }, xdg_listener { &CLASS_CALL(monitor_t, xdg_position), &CLASS_CALL(monitor_t, xdg_size), &CLASS_CALL(monitor_t, xdg_done), &CLASS_CALL(monitor_t, xdg_name), &CLASS_CALL(monitor_t, xdg_description) } { } inline void monitor_t::xdg_name(zxdg_output_v1 *, const char *name) { this->name = name; BOOST_LOG(info) << "Name: "sv << this->name; } void monitor_t::xdg_description(zxdg_output_v1 *, const char *description) { this->description = description; BOOST_LOG(info) << "Found monitor: "sv << this->description; } void monitor_t::xdg_position(zxdg_output_v1 *, std::int32_t x, std::int32_t y) { viewport.offset_x = x; viewport.offset_y = y; BOOST_LOG(info) << "Offset: "sv << x << 'x' << y; } void monitor_t::xdg_size(zxdg_output_v1 *, std::int32_t width, std::int32_t height) { BOOST_LOG(info) << "Logical size: "sv << width << 'x' << height; } void monitor_t::wl_mode( wl_output *wl_output, std::uint32_t flags, std::int32_t width, std::int32_t height, std::int32_t refresh ) { viewport.width = width; viewport.height = height; BOOST_LOG(info) << "Resolution: "sv << width << 'x' << height; } void monitor_t::listen(zxdg_output_manager_v1 *output_manager) { auto xdg_output = zxdg_output_manager_v1_get_xdg_output(output_manager, output); zxdg_output_v1_add_listener(xdg_output, &xdg_listener, this); wl_output_add_listener(output, &wl_listener, this); } interface_t::interface_t() noexcept : screencopy_manager {nullptr}, dmabuf_interface {nullptr}, output_manager {nullptr}, listener { &CLASS_CALL(interface_t, add_interface), &CLASS_CALL(interface_t, del_interface) } { } void interface_t::listen(wl_registry *registry) { wl_registry_add_listener(registry, &listener, this); } void interface_t::add_interface( wl_registry *registry, std::uint32_t id, const char *interface, std::uint32_t version ) { BOOST_LOG(debug) << "Available interface: "sv << interface << '(' << id << ") version "sv << version; if (!std::strcmp(interface, wl_output_interface.name)) { BOOST_LOG(info) << "Found interface: "sv << interface << '(' << id << ") version "sv << version; monitors.emplace_back( std::make_unique( (wl_output *) wl_registry_bind(registry, id, &wl_output_interface, 2) ) ); } else if (!std::strcmp(interface, zxdg_output_manager_v1_interface.name)) { BOOST_LOG(info) << "Found interface: "sv << interface << '(' << id << ") version "sv << version; output_manager = (zxdg_output_manager_v1 *) wl_registry_bind(registry, id, &zxdg_output_manager_v1_interface, version); this->interface[XDG_OUTPUT] = true; } else if (!std::strcmp(interface, zwlr_screencopy_manager_v1_interface.name)) { BOOST_LOG(info) << "Found interface: "sv << interface << '(' << id << ") version "sv << version; screencopy_manager = (zwlr_screencopy_manager_v1 *) wl_registry_bind(registry, id, &zwlr_screencopy_manager_v1_interface, version); this->interface[WLR_EXPORT_DMABUF] = true; } else if (!std::strcmp(interface, zwp_linux_dmabuf_v1_interface.name)) { BOOST_LOG(info) << "Found interface: "sv << interface << '(' << id << ") version "sv << version; dmabuf_interface = (zwp_linux_dmabuf_v1 *) wl_registry_bind(registry, id, &zwp_linux_dmabuf_v1_interface, version); this->interface[LINUX_DMABUF] = true; } } void interface_t::del_interface(wl_registry *registry, uint32_t id) { BOOST_LOG(info) << "Delete: "sv << id; } // Initialize GBM bool dmabuf_t::init_gbm() { if (gbm_device) { return true; } // Find render node drmDevice *devices[16]; int n = drmGetDevices2(0, devices, 16); if (n <= 0) { BOOST_LOG(error) << "No DRM devices found"sv; return false; } int drm_fd = -1; for (int i = 0; i < n; i++) { if (devices[i]->available_nodes & (1 << DRM_NODE_RENDER)) { drm_fd = open(devices[i]->nodes[DRM_NODE_RENDER], O_RDWR); if (drm_fd >= 0) { break; } } } drmFreeDevices(devices, n); if (drm_fd < 0) { BOOST_LOG(error) << "Failed to open DRM render node"sv; return false; } gbm_device = gbm_create_device(drm_fd); if (!gbm_device) { close(drm_fd); BOOST_LOG(error) << "Failed to create GBM device"sv; return false; } return true; } // Cleanup GBM void dmabuf_t::cleanup_gbm() { if (current_bo) { gbm_bo_destroy(current_bo); current_bo = nullptr; } if (current_wl_buffer) { wl_buffer_destroy(current_wl_buffer); current_wl_buffer = nullptr; } } dmabuf_t::dmabuf_t(): status {READY}, frames {}, current_frame {&frames[0]}, listener { &CLASS_CALL(dmabuf_t, buffer), &CLASS_CALL(dmabuf_t, flags), &CLASS_CALL(dmabuf_t, ready), &CLASS_CALL(dmabuf_t, failed), &CLASS_CALL(dmabuf_t, damage), &CLASS_CALL(dmabuf_t, linux_dmabuf), &CLASS_CALL(dmabuf_t, buffer_done), } { } // Start capture void dmabuf_t::listen( zwlr_screencopy_manager_v1 *screencopy_manager, zwp_linux_dmabuf_v1 *dmabuf_interface, wl_output *output, bool blend_cursor ) { this->dmabuf_interface = dmabuf_interface; // Reset state shm_info.supported = false; dmabuf_info.supported = false; // Create new frame auto frame = zwlr_screencopy_manager_v1_capture_output( screencopy_manager, blend_cursor ? 1 : 0, output ); // Store frame data pointer for callbacks zwlr_screencopy_frame_v1_set_user_data(frame, this); // Add listener zwlr_screencopy_frame_v1_add_listener(frame, &listener, this); status = WAITING; } dmabuf_t::~dmabuf_t() { cleanup_gbm(); for (auto &frame : frames) { frame.destroy(); } if (gbm_device) { // We should close the DRM FD, but it's owned by GBM gbm_device_destroy(gbm_device); gbm_device = nullptr; } } // Buffer format callback void dmabuf_t::buffer( zwlr_screencopy_frame_v1 *frame, uint32_t format, uint32_t width, uint32_t height, uint32_t stride ) { shm_info.supported = true; shm_info.format = format; shm_info.width = width; shm_info.height = height; shm_info.stride = stride; BOOST_LOG(debug) << "Screencopy supports SHM format: "sv << format; } // DMA-BUF format callback void dmabuf_t::linux_dmabuf( zwlr_screencopy_frame_v1 *frame, std::uint32_t format, std::uint32_t width, std::uint32_t height ) { dmabuf_info.supported = true; dmabuf_info.format = format; dmabuf_info.width = width; dmabuf_info.height = height; BOOST_LOG(debug) << "Screencopy supports DMA-BUF format: "sv << format; } // Flags callback void dmabuf_t::flags(zwlr_screencopy_frame_v1 *frame, std::uint32_t flags) { y_invert = flags & ZWLR_SCREENCOPY_FRAME_V1_FLAGS_Y_INVERT; BOOST_LOG(debug) << "Frame flags: "sv << flags << (y_invert ? " (y_invert)" : ""); } // DMA-BUF creation helper void dmabuf_t::create_and_copy_dmabuf(zwlr_screencopy_frame_v1 *frame) { if (!init_gbm()) { BOOST_LOG(error) << "Failed to initialize GBM"sv; zwlr_screencopy_frame_v1_destroy(frame); status = REINIT; return; } // Create GBM buffer current_bo = gbm_bo_create(gbm_device, dmabuf_info.width, dmabuf_info.height, dmabuf_info.format, GBM_BO_USE_RENDERING); if (!current_bo) { BOOST_LOG(error) << "Failed to create GBM buffer"sv; zwlr_screencopy_frame_v1_destroy(frame); status = REINIT; return; } // Get buffer info int fd = gbm_bo_get_fd(current_bo); if (fd < 0) { BOOST_LOG(error) << "Failed to get buffer FD"sv; gbm_bo_destroy(current_bo); current_bo = nullptr; zwlr_screencopy_frame_v1_destroy(frame); status = REINIT; 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); // Add listener for buffer creation zwp_linux_buffer_params_v1_add_listener(params, ¶ms_listener, frame); // Create Wayland buffer (async - callback will handle copy) zwp_linux_buffer_params_v1_create(params, dmabuf_info.width, dmabuf_info.height, dmabuf_info.format, 0); } // Buffer done callback - time to create buffer void dmabuf_t::buffer_done(zwlr_screencopy_frame_v1 *frame) { auto next_frame = get_next_frame(); // Prefer DMA-BUF if supported if (dmabuf_info.supported && dmabuf_interface) { // Store format info first next_frame->sd.fourcc = dmabuf_info.format; next_frame->sd.width = dmabuf_info.width; next_frame->sd.height = dmabuf_info.height; // Create and start copy create_and_copy_dmabuf(frame); } else if (shm_info.supported) { // SHM fallback would go here BOOST_LOG(warning) << "SHM capture not implemented"sv; zwlr_screencopy_frame_v1_destroy(frame); status = REINIT; } else { BOOST_LOG(error) << "No supported buffer types"sv; zwlr_screencopy_frame_v1_destroy(frame); status = REINIT; } } // Buffer params created callback void dmabuf_t::buffer_params_created( void *data, struct zwp_linux_buffer_params_v1 *params, struct wl_buffer *buffer ) { auto frame = static_cast(data); auto self = static_cast(zwlr_screencopy_frame_v1_get_user_data(frame)); // Store for cleanup self->current_wl_buffer = buffer; // Start the actual copy zwlr_screencopy_frame_v1_copy(frame, buffer); } // Buffer params failed callback void dmabuf_t::buffer_params_failed( void *data, struct zwp_linux_buffer_params_v1 *params ) { auto frame = static_cast(data); auto self = static_cast(zwlr_screencopy_frame_v1_get_user_data(frame)); BOOST_LOG(error) << "Failed to create buffer from params"sv; self->cleanup_gbm(); zwlr_screencopy_frame_v1_destroy(frame); self->status = REINIT; } // Ready callback void dmabuf_t::ready( zwlr_screencopy_frame_v1 *frame, std::uint32_t tv_sec_hi, std::uint32_t tv_sec_lo, std::uint32_t tv_nsec ) { BOOST_LOG(debug) << "Frame ready"sv; // Frame is ready for use, GBM buffer now contains screen content current_frame->destroy(); current_frame = get_next_frame(); // Keep the GBM buffer alive but destroy the Wayland objects if (current_wl_buffer) { wl_buffer_destroy(current_wl_buffer); current_wl_buffer = nullptr; } cleanup_gbm(); zwlr_screencopy_frame_v1_destroy(frame); status = READY; } // Failed callback void dmabuf_t::failed(zwlr_screencopy_frame_v1 *frame) { BOOST_LOG(error) << "Frame capture failed"sv; // Clean up resources cleanup_gbm(); auto next_frame = get_next_frame(); next_frame->destroy(); zwlr_screencopy_frame_v1_destroy(frame); status = REINIT; } void dmabuf_t::damage( zwlr_screencopy_frame_v1 *frame, std::uint32_t x, std::uint32_t y, std::uint32_t width, std::uint32_t height ) {}; void frame_t::destroy() { for (auto x = 0; x < 4; ++x) { if (sd.fds[x] >= 0) { close(sd.fds[x]); sd.fds[x] = -1; } } } frame_t::frame_t() { // File descriptors aren't open std::fill_n(sd.fds, 4, -1); }; std::vector> monitors(const char *display_name) { display_t display; if (display.init(display_name)) { return {}; } interface_t interface; interface.listen(display.registry()); display.roundtrip(); if (!interface[interface_t::XDG_OUTPUT]) { BOOST_LOG(error) << "Missing Wayland wire XDG_OUTPUT"sv; return {}; } for (auto &monitor : interface.monitors) { monitor->listen(interface.output_manager); } display.roundtrip(); return std::move(interface.monitors); } static bool validate() { display_t display; return display.init() == 0; } int init() { static bool validated = validate(); return !validated; } } // namespace wl #pragma GCC diagnostic pop