c++c++17x11shared-memoryxcb

How do I get access to the shared mem with xcb


I'm trying to grab a screenshot of the portion of a window using xcb_shm_get_image_unchecked. I've created the shared memory using the following code:

#include <cstdlib>
#include <memory>

#include <sys/shm.h>
#include <xcb/shm.h>
#include <xcb/xcb.h>
#include <xcb/xcb_image.h>
#include <xcb/xcb_pixel.h>
#include <xcb/xproto.h>

#include <spdlog/spdlog.h>

const auto IMAGE_WIDTH  = 640;
const auto IMAGE_HEIGHT = 480;
const auto SHM_SIZE     = 4 * 1024 * 1024;

auto main() -> int
{
  std::unique_ptr<xcb_connection_t, decltype(&xcb_disconnect)> c(
    xcb_connect(nullptr, nullptr), &xcb_disconnect);

  if (!c) {
    spdlog::error("failed to connect to X server");
    return EXIT_FAILURE;
  }

  auto roots_iter = xcb_setup_roots_iterator(xcb_get_setup(c.get()));
  if (roots_iter.rem == 0) {
    spdlog::error("no screen found");
    return EXIT_FAILURE;
  }

  auto          root   = roots_iter.data->root;
  xcb_shm_seg_t shmseg = xcb_generate_id(c.get());
  // xcb_shm_get_im
  auto shm_reply = xcb_shm_create_segment_reply(
    c.get(), xcb_shm_create_segment(c.get(), shmseg, SHM_SIZE, 0), nullptr);

  if (!shm_reply) {
    spdlog::error("failed to create shared memory segment");
    return EXIT_FAILURE;
  }

  auto fds = xcb_shm_create_segment_reply_fds(c.get(), shm_reply);
  spdlog::info("found {} fds", shm_reply->nfd);
  for (int i = 0; i < shm_reply->nfd; i++) {
    auto err = xcb_request_check(
      c.get(), xcb_shm_attach_fd(c.get(), shmseg, fds[i], true));
    if (err) {
      spdlog::error("failed to attach fd: {}", fds[i]);
      delete err;
    }
  }

  for (int i = 0; i < shm_reply->nfd; i++) {
      close(fds[i]);
  }
  xcb_shm_detach(c.get(), shmseg);
  return 0;
}

First question: have I done this right in terms of freeing up resources?

2nd: How do I get access to the shared memory segment?

Finally, how would I grab a 640x480 screenshot of the root window? Am I on the right path with this:

auto image = xcb_shm_get_image_reply(
  c.get(),
  xcb_shm_get_image_unchecked(c.get(),
                              root,
                              0,
                              0,
                              IMAGE_WIDTH,
                              IMAGE_HEIGHT,
                              XCB_GC_PLANE_MASK,
                              XCB_IMAGE_FORMAT_Z_PIXMAP,
                              shmseg,
                              0),
  nullptr);

if (!image) {
  spdlog::error("failed to get image");
  xcb_shm_detach(c.get(), shmseg);
  return EXIT_FAILURE;
}

spdlog::info("image: size {}", image->size);
delete image;

Solution

  • You have to make a call to mmap to get access to the shared memory. From the docs for xcb_shm_create_segment_unchecked:

    Asks the server to allocate a shared memory segment. The server’s reply will include a file descriptor for the client to pass to mmap().

    And how do you get access?

    You have to make a call to mmap including the file descriptor to tell mmap where to find the region of memory which was allocated by X server.

    You can do it using something like this:

    auto *shmem = mmap(nullptr, SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fds[0], 0);
    
    if (shmem == MAP_FAILED) {
      ... // cleanup
    }
    
    close(fds[0]);
    

    The arguments should be pretty self-explanatory once you read the docs for mmap. The last 0 just means that we want the pointer to refer to the start of the memory region.

    Of course, don't forget to munmap when you're finished:

    munmap(p, SHM_SIZE);
    

    Finally, there is no need for xcb_shm_attach_fd(c.get(), shmseg, fds[i], true));. This is only needed if you had created the mmap file yourself, and passing it to the X server so that it can have access.

    First question: have I done this right in terms of freeing up resources?

    Yes you have

    how would I grab a 640x480 screenshot of the root window?

    The code you have for grabbing the image is almost right. Just change the plane_mask argument to ~0. You can see it's similar to how cairo does it, and also how obs does it.

    To actually convert the bytes to an image, you can choose to convert it to a lowest common denominator image format like PPM. Here is what that code could look like:

    if (auto ppmFile = std::ofstream("/path/to/screenshot.ppm", std::ios::binary); ppmFile) {
      ppmFile << "P6\n" << IMAGE_WIDTH << " " << IMAGE_HEIGHT << "\n255\n";
      const auto *const imageData = shmem;
      for (uint32_t rgb = 0; rgb + 4 <= image->size; rgb += 4) {
        auto b = imageData[rgb];
        auto g = imageData[rgb + 1];
        auto r = imageData[rgb + 2];
        if (setup->image_byte_order == XCB_IMAGE_ORDER_MSB_FIRST) {
          std::swap(b, r);
        }
        ppmFile << r << g << b;
      }
    }
    

    The end result should be an image in ppm format located at /path/to/screenshot.ppm. If you don't have a way of viewing it, imagemagick is able to convert ppm to png, so you can view it that way.