#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef __linux__ #include #endif #include "ext-image-capture-source-v1-client-protocol.h" #include "ext-image-copy-capture-v1-client-protocol.h" #include "linux-dmabuf-v1-client-protocol.h" namespace { constexpr bool iequals(std::string_view a, std::string_view b) { return a.size() == b.size() && std::equal(a.begin(), a.end(), b.begin(), [](char ac, char bc) { return std::tolower(static_cast(ac)) == std::tolower(static_cast(bc)); }); } constexpr auto CAPTURE_FPS = 60; constexpr auto CAPTURE_SECONDS = 5; } // namespace namespace wayland { struct State { struct wl_display *display{}; struct wl_registry *registry{}; struct ext_image_copy_capture_manager_v1 *copy_capture_manager{}; struct ext_image_copy_capture_session_v1 *copy_capture_session{}; struct ext_image_capture_source_v1 *capture_source{}; struct ext_output_image_capture_source_manager_v1 *capture_source_manager{}; struct zwp_linux_dmabuf_v1 *linux_dmabuf{}; struct OutputInfo { std::string name; std::string description; }; std::unordered_map outputs; // We do not have owership over _dmabuf_devices. struct wl_array *_dmabuf_devices{}; std::span dmabuf_devices; struct DmaBufFormat { uint32_t format{0}; // We do not have owership over _modifiers. struct wl_array *_modifiers{}; std::span modifiers; } dmabuf_format; uint32_t width{0}; uint32_t height{0}; uint32_t stride{0}; bool session_ready{false}; bool frame_ready{false}; std::vector frame_data; std::error_code error; struct DmabufState { struct zwp_linux_buffer_params_v1 *params{}; struct wl_buffer *buffer{}; int fd{-1}; void *data{}; size_t size{0}; uint32_t stride{0}; bool buffer_done{false}; bool buffer_failed{false}; void *map_data{}; } dmabuf_state; int drm_fd{-1}; struct gbm_device *gbm_device{}; struct gbm_bo *gbm_bufferobject{}; explicit State() = default; State(const State &) = delete; State &operator=(const State &) = delete; State(State &&) = delete; State &operator=(State &&) = delete; ~State() { if (copy_capture_manager != nullptr) { ext_image_copy_capture_manager_v1_destroy(copy_capture_manager); } if (copy_capture_session != nullptr) { ext_image_copy_capture_session_v1_destroy(copy_capture_session); } if (capture_source != nullptr) { ext_image_capture_source_v1_destroy(capture_source); } if (capture_source_manager != nullptr) { ext_output_image_capture_source_manager_v1_destroy(capture_source_manager); } if (linux_dmabuf != nullptr) { zwp_linux_dmabuf_v1_destroy(linux_dmabuf); } for (const auto &[output, _] : outputs) { if (output != nullptr) { wl_output_destroy(output); } } if (registry != nullptr) { wl_registry_destroy(registry); } if (display != nullptr) { wl_display_disconnect(display); } if (gbm_bufferobject != nullptr) { gbm_bo_destroy(gbm_bufferobject); } if (gbm_device != nullptr) { gbm_device_destroy(gbm_device); } } }; namespace error { enum class Errc : uint8_t { ListenerAddFailed = 1, DmabufBufferFailed, }; class ErrorCategory : public std::error_category { public: [[nodiscard]] const char *name() const noexcept override { return "Wayland"; } [[nodiscard]] std::string message(int ev) const override { switch (static_cast(ev)) { case Errc::ListenerAddFailed: return "failed to add listener to Wayland global"; case Errc::DmabufBufferFailed: return "failed to create wl_buffer from dmabuf"; default: // unreachable std::terminate(); } } }; namespace { inline std::error_code make_error_code(Errc e) { return {static_cast(e), ErrorCategory()}; } } // namespace } // namespace error namespace output { constexpr struct wl_output_listener listener = { .geometry = [](void * /*data*/, struct wl_output * /*output*/, int32_t /*x*/, int32_t /*y*/, int32_t /*physical_width*/, int32_t /*physical_height*/, int32_t /*subpixel*/, const char * /*make*/, const char * /*model*/, int32_t /*transform*/) {}, .mode = [](void * /*data*/, struct wl_output * /*output*/, uint32_t /*flags*/, int32_t /*width*/, int32_t /*height*/, int32_t /*refresh*/) {}, .done = [](void * /*data*/, struct wl_output * /*output*/) {}, .scale = [](void * /*data*/, struct wl_output * /*output*/, int32_t /*factor*/) {}, .name = [](void *data, struct wl_output *output, const char *name) { auto *state = static_cast(data); try { state->outputs.at(output).name = name; } catch (const std::out_of_range &) { state->error = std::make_error_code(std::errc::result_out_of_range); } }, .description = [](void *data, struct wl_output *output, const char *description) { auto *state = static_cast(data); try { state->outputs.at(output).description = description; } catch (const std::out_of_range &err) { state->error = std::make_error_code(std::errc::result_out_of_range); } }, }; } // namespace output namespace registry { constexpr struct wl_registry_listener listener = { .global = [](void *data, struct wl_registry *registry, uint32_t name, const char *interface, uint32_t /*version*/) { using error::Errc; auto *state = static_cast(data); if (strcmp(interface, "wl_output") == 0) { auto global = static_cast( wl_registry_bind(registry, name, &wl_output_interface, 4)); state->outputs.insert({global, State::OutputInfo{}}); const int r = wl_output_add_listener(global, &output::listener, state); if (r < 0) { state->error = error::make_error_code(Errc::ListenerAddFailed); } } else if (strcmp(interface, "ext_image_copy_capture_manager_v1") == 0) { state->copy_capture_manager = static_cast( wl_registry_bind(registry, name, &ext_image_copy_capture_manager_v1_interface, 1)); } else if (strcmp(interface, "ext_output_image_capture_source_manager_v1") == 0) { state->capture_source_manager = static_cast(wl_registry_bind( registry, name, &ext_output_image_capture_source_manager_v1_interface, 1)); } else if (strcmp(interface, "zwp_linux_dmabuf_v1") == 0) { state->linux_dmabuf = static_cast( wl_registry_bind(registry, name, &zwp_linux_dmabuf_v1_interface, 1)); } }, .global_remove = [](void * /*data*/, struct wl_registry * /*registry*/, uint32_t /*name*/) { // If we're already bound to a global that has been removed, the // object will remain valid until we destroy it, meaning that any // actions we attempt to take will effectively fail, and hence be // caught by other error checking logic. }, }; } // namespace registry namespace session { constexpr struct ext_image_copy_capture_session_v1_listener listener = { .buffer_size = // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) [](void *data, struct ext_image_copy_capture_session_v1 * /*session*/, uint32_t width, uint32_t height) { auto *state = static_cast(data); state->width = width; state->height = height; state->stride = width * 4; // RGBA8888 }, .shm_format = [](void * /*data*/, struct ext_image_copy_capture_session_v1 * /*session*/, uint32_t /*format*/) {}, .dmabuf_device = [](void *data, struct ext_image_copy_capture_session_v1 * /*session*/, struct wl_array *device) { auto *state = static_cast(data); state->_dmabuf_devices = device; state->dmabuf_devices = std::span(static_cast(device->data), device->size / sizeof(dev_t)); }, .dmabuf_format = [](void *data, struct ext_image_copy_capture_session_v1 * /*session*/, uint32_t format, struct wl_array *modifiers) { auto *state = static_cast(data); state->dmabuf_format = State::DmaBufFormat{ .format = format, ._modifiers = modifiers, .modifiers = std::span(static_cast(modifiers->data), modifiers->size / sizeof(uint64_t)), }; }, .done = [](void *data, struct ext_image_copy_capture_session_v1 * /*session*/) { auto *state = static_cast(data); state->session_ready = true; }, .stopped = [](void * /*data*/, struct ext_image_copy_capture_session_v1 * /*session*/) { /* TODO: destroy session. */ }, }; } // namespace session namespace linux_dmabuf { constexpr struct zwp_linux_buffer_params_v1_listener params_listener = { .created = [](void *data, struct zwp_linux_buffer_params_v1 *params, struct wl_buffer *buffer) { auto *state = static_cast(data); state->dmabuf_state.buffer = buffer; state->dmabuf_state.buffer_done = true; // TODO: Do we need to actually destroy the params here? zwp_linux_buffer_params_v1_destroy(params); }, .failed = [](void *data, struct zwp_linux_buffer_params_v1 *params) { auto *state = static_cast(data); state->dmabuf_state.buffer_failed = true; state->error = error::make_error_code(error::Errc::DmabufBufferFailed); // TODO: Do we need to actually destroy the params here? zwp_linux_buffer_params_v1_destroy(params); }}; } // namespace linux_dmabuf namespace frame { constexpr struct ext_image_copy_capture_frame_v1_listener listener = { .transform = [](void *, struct ext_image_copy_capture_frame_v1 *, uint32_t) {}, .damage = [](void *, struct ext_image_copy_capture_frame_v1 *, int32_t, int32_t, int32_t, int32_t) {}, .presentation_time = [](void *, struct ext_image_copy_capture_frame_v1 *, uint32_t, uint32_t, uint32_t) {}, .ready = [](void *data, struct ext_image_copy_capture_frame_v1 * /*frame*/) { auto state = static_cast(data); state->frame_ready = true; }, .failed = [](void *data, struct ext_image_copy_capture_frame_v1 * /*frame*/, uint32_t /*reason*/) { // TODO: actually handle failure. std::println(std::cerr, "Debug: frame listener failed"); auto state = static_cast(data); state->frame_ready = true; }}; } // namespace frame } // namespace wayland namespace std { template <> struct is_error_code_enum : true_type {}; } // namespace std int main() { using std::cerr, std::print, std::println; using namespace wayland; namespace fs = std::filesystem; namespace ranges = std::ranges; int r{0}; State state; state.display = wl_display_connect(nullptr); if (state.display == nullptr) { println(cerr, "Failed to connect to Wayland display: {}.", strerror(errno)); return EXIT_FAILURE; } state.registry = wl_display_get_registry(state.display); if (state.registry == nullptr) { println(cerr, "Failed to get Wayland registry: {}.", strerror(errno)); return EXIT_FAILURE; } r = wl_registry_add_listener(state.registry, ®istry::listener, &state); if (r < 0) { println(cerr, "Failed to add listener to registry."); return EXIT_FAILURE; } r = wl_display_roundtrip(state.display); if (r < 0) { println(cerr, "Failed to roundtrip Wayland display."); return EXIT_FAILURE; } // Registry has been processed by now. if (state.error) { println(cerr, "Error: {}.", state.error.message()); return EXIT_FAILURE; } if (state.outputs.empty()) { println(cerr, "Failed to find any outputs."); return EXIT_FAILURE; } r = wl_display_dispatch(state.display); if (r < 0) { println(cerr, "Failed to dispatch Wayland display."); return EXIT_FAILURE; } // All callbacks for globals have been processed by now. if (state.error) { println(cerr, "Error: {}.", state.error.message()); return EXIT_FAILURE; } if (state.copy_capture_manager == nullptr) { println(cerr, "Compositor doesn't support ext-image-copy-capture-v1."); return EXIT_FAILURE; } if (state.capture_source_manager == nullptr) { println(cerr, "Compositor doesn't support ext-image-capture-source-v1."); return EXIT_FAILURE; } if (state.linux_dmabuf == nullptr) { println(cerr, "Compositor doesn't support linux-dmabuf-v1."); return EXIT_FAILURE; } println("Outputs:"); for (const auto &[_, info] : state.outputs) { println(" {}: {}", info.name, info.description); } print("Enter output to capture: "); std::string input; std::getline(std::cin, input); const auto it = ranges::find_if(state.outputs.begin(), state.outputs.end(), [input](const auto &pair) { return iequals(input, pair.second.name) || iequals(input, pair.second.description); }); if (it == state.outputs.end()) { println(cerr, "Output `{}` does not exist.", input); return EXIT_FAILURE; } auto output = it->first; state.capture_source = ext_output_image_capture_source_manager_v1_create_source(state.capture_source_manager, output); state.copy_capture_session = ext_image_copy_capture_manager_v1_create_session(state.copy_capture_manager, state.capture_source, 0); r = ext_image_copy_capture_session_v1_add_listener(state.copy_capture_session, &session::listener, &state); if (r < 0) { println(cerr, "Failed to add listener to capture session."); return EXIT_FAILURE; } while (!state.session_ready) { r = wl_display_dispatch(state.display); if (r < 0) { println(cerr, "Failed to dispatch Wayland display."); return EXIT_FAILURE; } } println("Width: {}", state.width); println("Height: {}", state.height); println("Stride: {}", state.stride); println("dmabuf devices:"); for (const auto &device : state.dmabuf_devices) { println(" {}", device); } println("dmabuf format: {}", state.dmabuf_format.format); println("dmabuf modifiers:"); for (const auto &modifier : state.dmabuf_format.modifiers) { println(" {}", modifier); } constexpr uint64_t LAYOUT_MODIFIER = 0; // linear if (ranges::find(state.dmabuf_format.modifiers, LAYOUT_MODIFIER) == state.dmabuf_format.modifiers.end()) { println(cerr, "Error: Compositor doesn't support linear layout modifier."); return EXIT_FAILURE; } const dev_t drm_dev = state.dmabuf_devices.front(); const fs::path dri_dir{"/dev/dri"}; std::string render_node; if (!fs::exists(dri_dir) || !fs::is_directory(dri_dir)) { std::println(std::cerr, "Directory {} does not exist", dri_dir.string()); return EXIT_FAILURE; } bool found = false; for (const auto &entry : fs::directory_iterator(dri_dir)) { if (!entry.is_character_file()) { continue; } const auto &path = entry.path(); if (!path.filename().string().starts_with("renderD")) { continue; } struct stat st{}; if (::stat(path.string().c_str(), &st) == 0 && st.st_rdev == drm_dev) { render_node = path.string(); found = true; break; } } if (!found) { std::println(std::cerr, "Error: No matching DRM render node found for dev_t `{}`.", drm_dev); return EXIT_FAILURE; } constexpr struct open_how how{.flags = O_RDWR | O_CLOEXEC, .mode = 0, .resolve = 0}; // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) state.drm_fd = static_cast(syscall(SYS_openat2, AT_FDCWD, render_node.c_str(), &how, sizeof(how))); if (state.drm_fd < 0) { println(cerr, "Error: failed to open DRI render node: {}.", strerror(errno)); return EXIT_FAILURE; } state.gbm_device = gbm_create_device(state.drm_fd); if (state.gbm_device == nullptr) { println(cerr, "Error: failed to create GBM device: {}.", strerror(errno)); return EXIT_FAILURE; } const uint64_t *modifiers = state.dmabuf_format.modifiers.data(); size_t modifier_count = state.dmabuf_format.modifiers.size(); println("modifier count: {}", modifier_count); println("modifiers:"); for (const auto &modifier : state.dmabuf_format.modifiers) { println(" {}", modifier); } state.gbm_bufferobject = gbm_bo_create_with_modifiers( state.gbm_device, state.width, state.height, state.dmabuf_format.format, modifiers, modifier_count); if (state.gbm_bufferobject == nullptr) { println(cerr, "Error: failed to create GBM buffer {}.", strerror(errno)); return EXIT_FAILURE; } state.dmabuf_state.fd = gbm_bo_get_fd(state.gbm_bufferobject); if (state.dmabuf_state.fd < 0) { println(cerr, "Error: failed to export GBM buffer as DMA-BUF: {}.", strerror(errno)); gbm_bo_destroy(state.gbm_bufferobject); return EXIT_FAILURE; } state.dmabuf_state.data = gbm_bo_map(state.gbm_bufferobject, 0, 0, state.width, state.height, GBM_BO_TRANSFER_READ_WRITE, &state.dmabuf_state.stride, &state.dmabuf_state.map_data); if (state.dmabuf_state.data == nullptr) { println(cerr, "Error: failed to map GBM buffer object: {}.", strerror(errno)); return EXIT_FAILURE; } state.dmabuf_state.params = zwp_linux_dmabuf_v1_create_params(state.linux_dmabuf); if (state.dmabuf_state.params == nullptr) { println(cerr, "Error: failed to create linux-dmabuf-v1 parameters."); return EXIT_FAILURE; } zwp_linux_buffer_params_v1_add(state.dmabuf_state.params, state.dmabuf_state.fd, 0, 0, state.dmabuf_state.stride, LAYOUT_MODIFIER, 0); r = zwp_linux_buffer_params_v1_add_listener(state.dmabuf_state.params, &linux_dmabuf::params_listener, &state); if (r < 0) { println(cerr, "Failed to add listener buffer parameters."); return EXIT_FAILURE; } // TODO: why do we pass a signed integer here? zwp_linux_buffer_params_v1_create(state.dmabuf_state.params, static_cast(state.width), static_cast(state.height), state.dmabuf_format.format, 0); while (!state.dmabuf_state.buffer_done && !state.dmabuf_state.buffer_failed) { r = wl_display_roundtrip(state.display); if (r < 0) { println(cerr, "Failed to roundtrip Wayland display."); return EXIT_FAILURE; } } if (state.dmabuf_state.buffer == nullptr || state.dmabuf_state.buffer_failed) { println(cerr, "Error: {}.", state.error.message()); return EXIT_FAILURE; } std::ofstream capture("capture.raw", std::ios::binary); if (!capture) { println(cerr, "Error: could not open `capture.raw` for writing: {}.", strerror(errno)); return EXIT_FAILURE; } for (auto _ : std::views::iota(0, CAPTURE_FPS * CAPTURE_SECONDS)) { state.frame_ready = false; auto frame = ext_image_copy_capture_session_v1_create_frame(state.copy_capture_session); ext_image_copy_capture_frame_v1_add_listener(frame, &frame::listener, &state); ext_image_copy_capture_frame_v1_attach_buffer(frame, state.dmabuf_state.buffer); // I'm not sure why this takes a signed integer considering that all the other APIs take a // signed integer, but casting from an uint32_t to an int32_t is the best we can do. ext_image_copy_capture_frame_v1_damage_buffer(frame, 0, 0, static_cast(state.width), static_cast(state.height)); ext_image_copy_capture_frame_v1_capture(frame); while (!state.frame_ready) { r = wl_display_dispatch(state.display); if (r < 0) { println(cerr, "Failed to dispatch Wayland display."); return EXIT_FAILURE; } } capture.write(static_cast(state.dmabuf_state.data), state.dmabuf_state.stride * state.height); // TODO: are we responsible for destroying the frame? ext_image_copy_capture_frame_v1_destroy(frame); } return EXIT_SUCCESS; }