summaryrefslogtreecommitdiff
path: root/utils
diff options
context:
space:
mode:
Diffstat (limited to 'utils')
-rw-r--r--utils/fbtest.cpp61
-rw-r--r--utils/kmsblank.cpp103
-rw-r--r--utils/kmscapture.cpp419
-rw-r--r--utils/kmsprint.cpp525
-rw-r--r--utils/kmstest.cpp1210
-rw-r--r--utils/kmstouch.cpp293
-rw-r--r--utils/kmsview.cpp121
-rw-r--r--utils/meson.build26
8 files changed, 2758 insertions, 0 deletions
diff --git a/utils/fbtest.cpp b/utils/fbtest.cpp
new file mode 100644
index 0000000..4992895
--- /dev/null
+++ b/utils/fbtest.cpp
@@ -0,0 +1,61 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+#include <sys/ioctl.h>
+#include <sys/stat.h>
+#include <sys/mman.h>
+
+#include <linux/fb.h>
+
+#include <kms++util/kms++util.h>
+
+using namespace kms;
+
+int main(int argc, char** argv)
+{
+ const char* fbdev = "/dev/fb0";
+ int r;
+
+ int fd = open(fbdev, O_RDWR);
+ FAIL_IF(fd < 0, "open %s failed\n", fbdev);
+
+ struct fb_var_screeninfo var;
+
+ r = ioctl(fd, FBIOGET_VSCREENINFO, &var);
+ FAIL_IF(r, "FBIOGET_VSCREENINFO failed");
+
+ struct fb_fix_screeninfo fix;
+
+ r = ioctl(fd, FBIOGET_FSCREENINFO, &fix);
+ FAIL_IF(r, "FBIOGET_FSCREENINFO failed");
+
+ uint8_t* ptr = static_cast<uint8_t*>(mmap(NULL,
+ var.yres_virtual * fix.line_length,
+ PROT_WRITE | PROT_READ,
+ MAP_SHARED, fd, 0));
+
+ FAIL_IF(ptr == MAP_FAILED, "mmap failed");
+
+ PixelFormat fmt = var.bits_per_pixel == 16 ? PixelFormat::RGB565 : PixelFormat::XRGB8888;
+
+ ExtCPUFramebuffer buf(var.xres, var.yres, fmt,
+ ptr, var.yres_virtual * fix.line_length, fix.line_length, 0);
+
+ printf("%s: res %dx%d, virtual %dx%d, line_len %d\n",
+ fbdev,
+ var.xres, var.yres,
+ var.xres_virtual, var.yres_virtual,
+ fix.line_length);
+
+ draw_test_pattern(buf);
+ // XXX this may draw over the edge for narrow displays
+ draw_text(buf, buf.width() / 2, 0, fbdev, RGB(255, 255, 255));
+
+ close(fd);
+
+ return 0;
+}
diff --git a/utils/kmsblank.cpp b/utils/kmsblank.cpp
new file mode 100644
index 0000000..43bd2a2
--- /dev/null
+++ b/utils/kmsblank.cpp
@@ -0,0 +1,103 @@
+#include <stdio.h>
+#include <unistd.h>
+#include <algorithm>
+
+#include <kms++/kms++.h>
+#include <kms++util/kms++util.h>
+
+using namespace std;
+using namespace kms;
+
+static const char* usage_str =
+ "Usage: kmsblank [OPTION]...\n\n"
+ "Blank screen(s)\n\n"
+ "Options:\n"
+ " --device=DEVICE DEVICE is the path to DRM card to open\n"
+ " -C, --card=NUM open /dev/dri/card<NUM>\n"
+ " -c, --connector=CONN CONN is <connector>\n"
+ " -t, --time=TIME blank/unblank in TIME intervals\n"
+ "\n"
+ "<connector> can be given by index (<idx>) or id (@<id>).\n"
+ "<connector> can also be given by name.\n";
+
+static void usage()
+{
+ puts(usage_str);
+}
+
+int main(int argc, char** argv)
+{
+ string dev_path;
+
+ vector<string> conn_strs;
+ uint32_t time = 0;
+
+ OptionSet optionset = {
+ Option("|device=", [&dev_path](string s) {
+ dev_path = s;
+ }),
+ Option("C|card=", [&dev_path](string s) {
+ dev_path = "/dev/dri/card" + s;
+ }),
+ Option("c|connector=", [&conn_strs](string str) {
+ conn_strs.push_back(str);
+ }),
+ Option("t|time=", [&time](string str) {
+ time = stoul(str);
+ }),
+ Option("h|help", []() {
+ usage();
+ exit(-1);
+ }),
+ };
+
+ optionset.parse(argc, argv);
+
+ if (optionset.params().size() > 0) {
+ usage();
+ exit(-1);
+ }
+
+ Card card(dev_path);
+ ResourceManager resman(card);
+
+ vector<Connector*> conns;
+
+ if (conn_strs.size() > 0) {
+ for (string s : conn_strs) {
+ auto c = resman.reserve_connector(s);
+ if (!c)
+ EXIT("Failed to resolve connector '%s'", s.c_str());
+ conns.push_back(c);
+ }
+ } else {
+ conns = card.get_connectors();
+ }
+
+ bool blank = true;
+
+ while (true) {
+ for (Connector* conn : conns) {
+ if (!conn->connected()) {
+ printf("Connector %u not connected\n", conn->idx());
+ continue;
+ }
+
+ printf("Connector %u: %sblank\n", conn->idx(), blank ? "" : "un");
+ int r = conn->set_prop_value("DPMS", blank ? 3 : 0);
+ if (r)
+ EXIT("Failed to set DPMS: %d", r);
+ }
+
+ if (time == 0)
+ break;
+
+ usleep(1000 * time);
+
+ blank = !blank;
+ }
+
+ printf("press enter to exit\n");
+
+ getchar();
+}
diff --git a/utils/kmscapture.cpp b/utils/kmscapture.cpp
new file mode 100644
index 0000000..251cc3b
--- /dev/null
+++ b/utils/kmscapture.cpp
@@ -0,0 +1,419 @@
+#include <linux/videodev2.h>
+#include <cstdio>
+#include <string.h>
+#include <poll.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <fstream>
+#include <sys/ioctl.h>
+#include <glob.h>
+
+#include <kms++/kms++.h>
+#include <kms++util/kms++util.h>
+
+#define CAMERA_BUF_QUEUE_SIZE 3
+#define MAX_CAMERA 9
+
+using namespace std;
+using namespace kms;
+
+enum class BufferProvider {
+ DRM,
+ V4L2,
+};
+
+class CameraPipeline
+{
+public:
+ CameraPipeline(int cam_fd, Card& card, Crtc* crtc, Plane* plane, uint32_t x, uint32_t y,
+ uint32_t iw, uint32_t ih, PixelFormat pixfmt,
+ BufferProvider buffer_provider);
+ ~CameraPipeline();
+
+ CameraPipeline(const CameraPipeline& other) = delete;
+ CameraPipeline& operator=(const CameraPipeline& other) = delete;
+
+ void show_next_frame(AtomicReq& req);
+ int fd() const { return m_fd; }
+ void start_streaming();
+
+private:
+ DmabufFramebuffer* GetDmabufFrameBuffer(Card& card, uint32_t i, PixelFormat pixfmt);
+ int m_fd; /* camera file descriptor */
+ Crtc* m_crtc;
+ Plane* m_plane;
+ BufferProvider m_buffer_provider;
+ vector<Framebuffer*> m_fb;
+ int m_prev_fb_index;
+ uint32_t m_in_width, m_in_height; /* camera capture resolution */
+ /* image properties for display */
+ uint32_t m_out_width, m_out_height;
+ uint32_t m_out_x, m_out_y;
+};
+
+static int buffer_export(int v4lfd, enum v4l2_buf_type bt, uint32_t index, int* dmafd)
+{
+ struct v4l2_exportbuffer expbuf;
+
+ memset(&expbuf, 0, sizeof(expbuf));
+ expbuf.type = bt;
+ expbuf.index = index;
+ if (ioctl(v4lfd, VIDIOC_EXPBUF, &expbuf) == -1) {
+ perror("VIDIOC_EXPBUF");
+ return -1;
+ }
+
+ *dmafd = expbuf.fd;
+
+ return 0;
+}
+
+DmabufFramebuffer* CameraPipeline::GetDmabufFrameBuffer(Card& card, uint32_t i, PixelFormat pixfmt)
+{
+ int r, dmafd;
+
+ r = buffer_export(m_fd, V4L2_BUF_TYPE_VIDEO_CAPTURE, i, &dmafd);
+ ASSERT(r == 0);
+
+ const PixelFormatInfo& format_info = get_pixel_format_info(pixfmt);
+ ASSERT(format_info.num_planes == 1);
+
+ vector<int> fds{ dmafd };
+ vector<uint32_t> pitches{ format_info.stride(m_in_width, 0) };
+ vector<uint32_t> offsets{ 0 };
+
+ return new DmabufFramebuffer(card, m_in_width, m_in_height, pixfmt,
+ fds, pitches, offsets);
+}
+
+bool inline better_size(struct v4l2_frmsize_discrete* v4ldisc,
+ uint32_t iw, uint32_t ih,
+ uint32_t best_w, uint32_t best_h)
+{
+ if (v4ldisc->width <= iw && v4ldisc->height <= ih &&
+ (v4ldisc->width >= best_w || v4ldisc->height >= best_h))
+ return true;
+
+ return false;
+}
+
+CameraPipeline::CameraPipeline(int cam_fd, Card& card, Crtc* crtc, Plane* plane, uint32_t x, uint32_t y,
+ uint32_t iw, uint32_t ih, PixelFormat pixfmt,
+ BufferProvider buffer_provider)
+ : m_fd(cam_fd), m_crtc(crtc), m_buffer_provider(buffer_provider), m_prev_fb_index(-1)
+{
+ int r;
+ uint32_t best_w = 320;
+ uint32_t best_h = 240;
+
+ struct v4l2_frmsizeenum v4lfrms = {};
+ v4lfrms.pixel_format = pixel_format_to_fourcc(pixfmt);
+ while (ioctl(m_fd, VIDIOC_ENUM_FRAMESIZES, &v4lfrms) == 0) {
+ if (v4lfrms.type != V4L2_FRMSIZE_TYPE_DISCRETE) {
+ v4lfrms.index++;
+ continue;
+ }
+
+ if (v4lfrms.discrete.width > iw || v4lfrms.discrete.height > ih) {
+ //skip
+ } else if (v4lfrms.discrete.width == iw && v4lfrms.discrete.height == ih) {
+ // Exact match
+ best_w = v4lfrms.discrete.width;
+ best_h = v4lfrms.discrete.height;
+ break;
+ } else if (v4lfrms.discrete.width >= best_w || v4lfrms.discrete.height >= ih) {
+ best_w = v4lfrms.discrete.width;
+ best_h = v4lfrms.discrete.height;
+ }
+
+ v4lfrms.index++;
+ };
+
+ m_out_width = m_in_width = best_w;
+ m_out_height = m_in_height = best_h;
+ /* Move it to the middle of the requested area */
+ m_out_x = x + iw / 2 - m_out_width / 2;
+ m_out_y = y + ih / 2 - m_out_height / 2;
+
+ printf("Capture: %ux%u\n", best_w, best_h);
+
+ struct v4l2_format v4lfmt = {};
+ v4lfmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+ r = ioctl(m_fd, VIDIOC_G_FMT, &v4lfmt);
+ ASSERT(r == 0);
+
+ v4lfmt.fmt.pix.pixelformat = pixel_format_to_fourcc(pixfmt);
+ v4lfmt.fmt.pix.width = m_in_width;
+ v4lfmt.fmt.pix.height = m_in_height;
+
+ r = ioctl(m_fd, VIDIOC_S_FMT, &v4lfmt);
+ ASSERT(r == 0);
+
+ uint32_t v4l_mem;
+
+ if (m_buffer_provider == BufferProvider::V4L2)
+ v4l_mem = V4L2_MEMORY_MMAP;
+ else
+ v4l_mem = V4L2_MEMORY_DMABUF;
+
+ struct v4l2_requestbuffers v4lreqbuf = {};
+ v4lreqbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+ v4lreqbuf.memory = v4l_mem;
+ v4lreqbuf.count = CAMERA_BUF_QUEUE_SIZE;
+ r = ioctl(m_fd, VIDIOC_REQBUFS, &v4lreqbuf);
+ ASSERT(r == 0);
+ ASSERT(v4lreqbuf.count == CAMERA_BUF_QUEUE_SIZE);
+
+ struct v4l2_buffer v4lbuf = {};
+ v4lbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+ v4lbuf.memory = v4l_mem;
+
+ for (unsigned i = 0; i < CAMERA_BUF_QUEUE_SIZE; i++) {
+ Framebuffer* fb;
+
+ if (m_buffer_provider == BufferProvider::V4L2)
+ fb = GetDmabufFrameBuffer(card, i, pixfmt);
+ else
+ fb = new DumbFramebuffer(card, m_in_width,
+ m_in_height, pixfmt);
+
+ v4lbuf.index = i;
+ if (m_buffer_provider == BufferProvider::DRM)
+ v4lbuf.m.fd = fb->prime_fd(0);
+ r = ioctl(m_fd, VIDIOC_QBUF, &v4lbuf);
+ ASSERT(r == 0);
+
+ m_fb.push_back(fb);
+ }
+
+ m_plane = plane;
+
+ // Do initial plane setup with first fb, so that we only need to
+ // set the FB when page flipping
+ AtomicReq req(card);
+
+ Framebuffer* fb = m_fb[0];
+
+ req.add(m_plane, "CRTC_ID", m_crtc->id());
+ req.add(m_plane, "FB_ID", fb->id());
+
+ req.add(m_plane, "CRTC_X", m_out_x);
+ req.add(m_plane, "CRTC_Y", m_out_y);
+ req.add(m_plane, "CRTC_W", m_out_width);
+ req.add(m_plane, "CRTC_H", m_out_height);
+
+ req.add(m_plane, "SRC_X", 0);
+ req.add(m_plane, "SRC_Y", 0);
+ req.add(m_plane, "SRC_W", m_in_width << 16);
+ req.add(m_plane, "SRC_H", m_in_height << 16);
+
+ r = req.commit_sync();
+ FAIL_IF(r, "initial plane setup failed");
+}
+
+CameraPipeline::~CameraPipeline()
+{
+ for (unsigned i = 0; i < m_fb.size(); i++)
+ delete m_fb[i];
+
+ ::close(m_fd);
+}
+
+void CameraPipeline::start_streaming()
+{
+ enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+
+ int r = ioctl(m_fd, VIDIOC_STREAMON, &type);
+ FAIL_IF(r, "Failed to enable camera stream: %d", r);
+}
+
+void CameraPipeline::show_next_frame(AtomicReq& req)
+{
+ int r;
+ uint32_t v4l_mem;
+
+ if (m_buffer_provider == BufferProvider::V4L2)
+ v4l_mem = V4L2_MEMORY_MMAP;
+ else
+ v4l_mem = V4L2_MEMORY_DMABUF;
+
+ struct v4l2_buffer v4l2buf = {};
+ v4l2buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+ v4l2buf.memory = v4l_mem;
+ r = ioctl(m_fd, VIDIOC_DQBUF, &v4l2buf);
+ if (r != 0) {
+ printf("VIDIOC_DQBUF ioctl failed with %d\n", errno);
+ return;
+ }
+
+ unsigned fb_index = v4l2buf.index;
+
+ Framebuffer* fb = m_fb[fb_index];
+
+ req.add(m_plane, "FB_ID", fb->id());
+
+ if (m_prev_fb_index >= 0) {
+ memset(&v4l2buf, 0, sizeof(v4l2buf));
+ v4l2buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+ v4l2buf.memory = v4l_mem;
+ v4l2buf.index = m_prev_fb_index;
+ if (m_buffer_provider == BufferProvider::DRM)
+ v4l2buf.m.fd = m_fb[m_prev_fb_index]->prime_fd(0);
+ r = ioctl(m_fd, VIDIOC_QBUF, &v4l2buf);
+ ASSERT(r == 0);
+ }
+
+ m_prev_fb_index = fb_index;
+}
+
+static bool is_capture_dev(int fd)
+{
+ struct v4l2_capability cap = {};
+ int r = ioctl(fd, VIDIOC_QUERYCAP, &cap);
+ ASSERT(r == 0);
+ return cap.capabilities & V4L2_CAP_VIDEO_CAPTURE;
+}
+
+std::vector<std::string> glob(const std::string& pat)
+{
+ glob_t glob_result;
+ glob(pat.c_str(), 0, NULL, &glob_result);
+ vector<string> ret;
+ for (unsigned i = 0; i < glob_result.gl_pathc; ++i)
+ ret.push_back(string(glob_result.gl_pathv[i]));
+ globfree(&glob_result);
+ return ret;
+}
+
+static const char* usage_str =
+ "Usage: kmscapture [OPTIONS]\n\n"
+ "Options:\n"
+ " -s, --single Single camera mode. Open only /dev/video0\n"
+ " --buffer-type=<drm|v4l> Use DRM or V4L provided buffers. Default: DRM\n"
+ " -h, --help Print this help\n";
+
+int main(int argc, char** argv)
+{
+ BufferProvider buffer_provider = BufferProvider::DRM;
+ bool single_cam = false;
+
+ OptionSet optionset = {
+ Option("s|single", [&]() {
+ single_cam = true;
+ }),
+ Option("|buffer-type=", [&](string s) {
+ if (s == "v4l")
+ buffer_provider = BufferProvider::V4L2;
+ else if (s == "drm")
+ buffer_provider = BufferProvider::DRM;
+ else
+ FAIL("Invalid buffer provider: %s", s.c_str());
+ }),
+ Option("h|help", [&]() {
+ puts(usage_str);
+ exit(-1);
+ }),
+ };
+
+ optionset.parse(argc, argv);
+
+ if (optionset.params().size() > 0) {
+ puts(usage_str);
+ exit(-1);
+ }
+
+ auto pixfmt = PixelFormat::YUYV;
+
+ Card card;
+
+ auto conn = card.get_first_connected_connector();
+ auto crtc = conn->get_current_crtc();
+ printf("Display: %dx%d\n", crtc->width(), crtc->height());
+ printf("Buffer provider: %s\n", buffer_provider == BufferProvider::V4L2 ? "V4L" : "DRM");
+
+ vector<int> camera_fds;
+
+ for (const string& vidpath : glob("/dev/video*")) {
+ int fd = ::open(vidpath.c_str(), O_RDWR | O_NONBLOCK);
+
+ if (fd < 0)
+ continue;
+
+ if (!is_capture_dev(fd)) {
+ close(fd);
+ continue;
+ }
+
+ camera_fds.push_back(fd);
+ printf("Using %s\n", vidpath.c_str());
+
+ if (single_cam)
+ break;
+ }
+
+ FAIL_IF(camera_fds.size() == 0, "No cameras found");
+
+ vector<Plane*> available_planes;
+ for (Plane* p : crtc->get_possible_planes()) {
+ if (p->plane_type() != PlaneType::Overlay)
+ continue;
+
+ if (!p->supports_format(pixfmt))
+ continue;
+
+ available_planes.push_back(p);
+ }
+
+ FAIL_IF(available_planes.size() < camera_fds.size(), "Not enough video planes for cameras");
+
+ uint32_t plane_w = crtc->width() / camera_fds.size();
+ vector<CameraPipeline*> cameras;
+
+ for (unsigned i = 0; i < camera_fds.size(); ++i) {
+ int cam_fd = camera_fds[i];
+ Plane* plane = available_planes[i];
+
+ auto cam = new CameraPipeline(cam_fd, card, crtc, plane, i * plane_w, 0,
+ plane_w, crtc->height(), pixfmt, buffer_provider);
+ cameras.push_back(cam);
+ }
+
+ unsigned nr_cameras = cameras.size();
+
+ vector<pollfd> fds(nr_cameras + 1);
+
+ for (unsigned i = 0; i < nr_cameras; i++) {
+ fds[i].fd = cameras[i]->fd();
+ fds[i].events = POLLIN;
+ }
+ fds[nr_cameras].fd = 0;
+ fds[nr_cameras].events = POLLIN;
+
+ for (auto cam : cameras)
+ cam->start_streaming();
+
+ while (true) {
+ int r = poll(fds.data(), nr_cameras + 1, -1);
+ ASSERT(r > 0);
+
+ if (fds[nr_cameras].revents != 0)
+ break;
+
+ AtomicReq req(card);
+
+ for (unsigned i = 0; i < nr_cameras; i++) {
+ if (!fds[i].revents)
+ continue;
+ cameras[i]->show_next_frame(req);
+ fds[i].revents = 0;
+ }
+
+ r = req.test();
+ FAIL_IF(r, "Atomic commit failed: %d", r);
+
+ req.commit_sync();
+ }
+
+ for (auto cam : cameras)
+ delete cam;
+}
diff --git a/utils/kmsprint.cpp b/utils/kmsprint.cpp
new file mode 100644
index 0000000..849c05c
--- /dev/null
+++ b/utils/kmsprint.cpp
@@ -0,0 +1,525 @@
+#include <algorithm>
+#include <cinttypes>
+#include <cstdio>
+#include <iostream>
+#include <string>
+#include <unistd.h>
+#include <kms++/format.h>
+
+#include <kms++/kms++.h>
+#include <kms++util/kms++util.h>
+
+using namespace std;
+using namespace kms;
+
+static struct {
+ bool print_props;
+ bool print_modes;
+ bool print_list;
+ bool x_modeline;
+} s_opts;
+
+static string format_mode(const Videomode& m, unsigned idx)
+{
+ string str;
+
+ str = fmt::format(" {:2} ", idx);
+
+ if (s_opts.x_modeline) {
+ str += fmt::format("{:12} {:6} {:4} {:4} {:4} {:4} {:4} {:4} {:4} {:4} {:3} {:#x} {:#x}",
+ m.name,
+ m.clock,
+ m.hdisplay, m.hsync_start, m.hsync_end, m.htotal,
+ m.vdisplay, m.vsync_start, m.vsync_end, m.vtotal,
+ m.vrefresh,
+ m.flags,
+ m.type);
+ } else {
+ str += m.to_string_long_padded();
+ }
+
+ return str;
+}
+
+static string format_mode_short(const Videomode& m)
+{
+ return m.to_string_long();
+}
+
+static string format_connector(Connector& c)
+{
+ string str;
+
+ str = fmt::format("Connector {} ({}) {}",
+ c.idx(), c.id(), c.fullname());
+
+ switch (c.connector_status()) {
+ case ConnectorStatus::Connected:
+ str += " (connected)";
+ break;
+ case ConnectorStatus::Disconnected:
+ str += " (disconnected)";
+ break;
+ default:
+ str += " (unknown)";
+ break;
+ }
+
+ return str;
+}
+
+static string format_encoder(Encoder& e)
+{
+ return fmt::format("Encoder {} ({}) {}",
+ e.idx(), e.id(), e.get_encoder_type());
+}
+
+static string format_crtc(Crtc& c)
+{
+ string str;
+
+ str = fmt::format("Crtc {} ({})", c.idx(), c.id());
+
+ if (c.mode_valid())
+ str += " " + format_mode_short(c.mode());
+
+ return str;
+}
+
+static string format_plane(Plane& p)
+{
+ string str;
+
+ str = fmt::format("Plane {} ({})", p.idx(), p.id());
+
+ if (p.fb_id())
+ str += fmt::format(" fb-id: {}", p.fb_id());
+
+ string crtcs = join<Crtc*>(p.get_possible_crtcs(), " ", [](Crtc* crtc) { return to_string(crtc->idx()); });
+
+ str += fmt::format(" (crtcs: {})", crtcs);
+
+ if (p.card().has_atomic()) {
+ str += fmt::format(" {},{} {}x{} -> {},{} {}x{}",
+ (uint32_t)p.get_prop_value("SRC_X") >> 16,
+ (uint32_t)p.get_prop_value("SRC_Y") >> 16,
+ (uint32_t)p.get_prop_value("SRC_W") >> 16,
+ (uint32_t)p.get_prop_value("SRC_H") >> 16,
+ (int32_t)p.get_prop_value("CRTC_X"),
+ (int32_t)p.get_prop_value("CRTC_Y"),
+ (uint32_t)p.get_prop_value("CRTC_W"),
+ (uint32_t)p.get_prop_value("CRTC_H"));
+ }
+
+ string fmts = join<uint32_t>(p.get_fourccs(), " ", [](uint32_t fourcc) { return fourcc_to_str(fourcc); });
+
+ str += fmt::format(" ({})", fmts);
+
+ return str;
+}
+
+static string format_fb(Framebuffer& fb)
+{
+ return fmt::format("FB {} {}x{} {}",
+ fb.id(), fb.width(), fb.height(),
+ fourcc_to_str(fb.fourcc()));
+}
+
+static string format_property(const Property* prop, uint64_t val)
+{
+ string ret = fmt::format("{} ({}) = ", prop->name(), prop->id());
+
+ switch (prop->type()) {
+ case PropertyType::Bitmask: {
+ vector<string> v, vall;
+
+ for (auto kvp : prop->get_enums()) {
+ if (val & (1 << kvp.first))
+ v.push_back(kvp.second);
+ vall.push_back(fmt::format("{}={:#x}", kvp.second, 1 << kvp.first));
+ }
+
+ // XXX
+ ret += fmt::format("{:#x} ({}) [{}]", val, join(v, "|"), join(vall, "|"));
+
+ break;
+ }
+
+ case PropertyType::Blob: {
+ uint32_t blob_id = (uint32_t)val;
+
+ if (blob_id) {
+ Blob blob(prop->card(), blob_id);
+ auto data = blob.data();
+
+ ret += fmt::format("blob-id {} len {}", blob_id, data.size());
+ } else {
+ ret += fmt::format("blob-id {}", blob_id);
+ }
+
+ break;
+ }
+
+ case PropertyType::Enum: {
+ string cur;
+ vector<string> vall;
+
+ for (auto kvp : prop->get_enums()) {
+ if (val == kvp.first)
+ cur = kvp.second;
+ vall.push_back(fmt::format("{}={}", kvp.second, kvp.first));
+ }
+
+ ret += fmt::format("{} ({}) [{}]", val, cur, join(vall, "|"));
+
+ break;
+ }
+
+ case PropertyType::Object: {
+ ret += fmt::format("object id {}", val);
+ break;
+ }
+
+ case PropertyType::Range: {
+ auto values = prop->get_values();
+
+ ret += fmt::format("{} [{} - {}]",
+ val, values[0], values[1]);
+
+ break;
+ }
+
+ case PropertyType::SignedRange: {
+ auto values = prop->get_values();
+
+ ret += fmt::format("{} [{} - {}]",
+ (int64_t)val, (int64_t)values[0], (int64_t)values[1]);
+
+ break;
+ }
+ }
+
+ if (prop->is_pending())
+ ret += " (pending)";
+ if (prop->is_immutable())
+ ret += " (immutable)";
+
+ return ret;
+}
+
+static vector<string> format_props(DrmPropObject* o)
+{
+ vector<string> lines;
+
+ auto pmap = o->get_prop_map();
+ for (auto pp : pmap) {
+ const Property* p = o->card().get_prop(pp.first);
+ lines.push_back(format_property(p, pp.second));
+ }
+
+ return lines;
+}
+
+static string format_ob(DrmObject* ob)
+{
+ if (auto o = dynamic_cast<Connector*>(ob))
+ return format_connector(*o);
+ else if (auto o = dynamic_cast<Encoder*>(ob))
+ return format_encoder(*o);
+ else if (auto o = dynamic_cast<Crtc*>(ob))
+ return format_crtc(*o);
+ else if (auto o = dynamic_cast<Plane*>(ob))
+ return format_plane(*o);
+ else if (auto o = dynamic_cast<Framebuffer*>(ob))
+ return format_fb(*o);
+ else
+ EXIT("Unkown DRM Object type\n");
+}
+
+template<class T>
+vector<T> filter(const vector<T>& sequence, function<bool(T)> predicate)
+{
+ vector<T> result;
+
+ for (auto it = sequence.begin(); it != sequence.end(); ++it)
+ if (predicate(*it))
+ result.push_back(*it);
+
+ return result;
+}
+
+struct Entry {
+ string title;
+ vector<string> lines;
+ vector<Entry> children;
+};
+
+static Entry& add_entry(vector<Entry>& entries)
+{
+ entries.emplace_back();
+ return entries.back();
+}
+/*
+static bool on_tty()
+{
+ return isatty(STDOUT_FILENO) > 0;
+}
+*/
+enum class TreeGlyphMode {
+ None,
+ ASCII,
+ UTF8,
+};
+
+static TreeGlyphMode s_glyph_mode = TreeGlyphMode::None;
+
+enum class TreeGlyph {
+ Vertical,
+ Branch,
+ Right,
+ Space,
+};
+
+static const map<TreeGlyph, string> glyphs_utf8 = {
+ { TreeGlyph::Vertical, "│ " },
+ { TreeGlyph::Branch, "├─" },
+ { TreeGlyph::Right, "└─" },
+ { TreeGlyph::Space, " " },
+
+};
+
+static const map<TreeGlyph, string> glyphs_ascii = {
+ { TreeGlyph::Vertical, "| " },
+ { TreeGlyph::Branch, "|-" },
+ { TreeGlyph::Right, "`-" },
+ { TreeGlyph::Space, " " },
+
+};
+
+const string& get_glyph(TreeGlyph glyph)
+{
+ static const string s_empty = " ";
+
+ if (s_glyph_mode == TreeGlyphMode::None)
+ return s_empty;
+
+ const map<TreeGlyph, string>& glyphs = s_glyph_mode == TreeGlyphMode::UTF8 ? glyphs_utf8 : glyphs_ascii;
+
+ return glyphs.at(glyph);
+}
+
+static void print_entry(const Entry& e, const string& prefix, bool is_child, bool is_last)
+{
+ string prefix1;
+ string prefix2;
+
+ if (is_child) {
+ prefix1 = prefix + (is_last ? get_glyph(TreeGlyph::Right) : get_glyph(TreeGlyph::Branch));
+ prefix2 = prefix + (is_last ? get_glyph(TreeGlyph::Space) : get_glyph(TreeGlyph::Vertical));
+ }
+
+ fmt::print("{}{}\n", prefix1, e.title);
+
+ bool has_children = e.children.size() > 0;
+
+ string data_prefix = prefix2 + (has_children ? get_glyph(TreeGlyph::Vertical) : get_glyph(TreeGlyph::Space));
+
+ for (const string& str : e.lines) {
+ string p = data_prefix + get_glyph(TreeGlyph::Space);
+ fmt::print("{}{}\n", p, str);
+ }
+
+ for (const Entry& child : e.children) {
+ bool child_is_last = &child == &e.children.back();
+
+ print_entry(child, prefix2, true, child_is_last);
+ }
+}
+
+static void print_entries(const vector<Entry>& entries, const string& prefix)
+{
+ for (const Entry& e : entries) {
+ print_entry(e, "", false, false);
+ }
+}
+
+template<class T>
+static void append(vector<DrmObject*>& dst, const vector<T*>& src)
+{
+ dst.insert(dst.end(), src.begin(), src.end());
+}
+
+static void print_as_list(Card& card)
+{
+ vector<DrmPropObject*> obs;
+ vector<Framebuffer*> fbs;
+
+ for (Connector* conn : card.get_connectors()) {
+ obs.push_back(conn);
+ }
+
+ for (Encoder* enc : card.get_encoders()) {
+ obs.push_back(enc);
+ }
+
+ for (Crtc* crtc : card.get_crtcs()) {
+ obs.push_back(crtc);
+ if (crtc->buffer_id() && !card.has_universal_planes()) {
+ Framebuffer* fb = new Framebuffer(card, crtc->buffer_id());
+ fbs.push_back(fb);
+ }
+ }
+
+ for (Plane* plane : card.get_planes()) {
+ obs.push_back(plane);
+ if (plane->fb_id()) {
+ Framebuffer* fb = new Framebuffer(card, plane->fb_id());
+ fbs.push_back(fb);
+ }
+ }
+
+ for (DrmPropObject* ob : obs) {
+ fmt::print("{}\n", format_ob(ob));
+
+ if (s_opts.print_props) {
+ for (string str : format_props(ob))
+ fmt::print(" {}\n", str);
+ }
+ }
+
+ for (Framebuffer* fb : fbs) {
+ fmt::print("{}\n", format_ob(fb));
+ }
+}
+
+static void print_as_tree(Card& card)
+{
+ vector<Entry> entries;
+
+ for (Connector* conn : card.get_connectors()) {
+ Entry& e1 = add_entry(entries);
+ e1.title = format_ob(conn);
+ if (s_opts.print_props)
+ e1.lines = format_props(conn);
+
+ for (Encoder* enc : conn->get_encoders()) {
+ Entry& e2 = add_entry(e1.children);
+ e2.title = format_ob(enc);
+ if (s_opts.print_props)
+ e2.lines = format_props(enc);
+
+ if (Crtc* crtc = enc->get_crtc()) {
+ Entry& e3 = add_entry(e2.children);
+ e3.title = format_ob(crtc);
+ if (s_opts.print_props)
+ e3.lines = format_props(crtc);
+
+ if (crtc->buffer_id() && !card.has_universal_planes()) {
+ Framebuffer fb(card, crtc->buffer_id());
+ Entry& e5 = add_entry(e3.children);
+
+ e5.title = format_ob(&fb);
+ }
+
+ for (Plane* plane : card.get_planes()) {
+ if (plane->crtc_id() != crtc->id())
+ continue;
+
+ Entry& e4 = add_entry(e3.children);
+ e4.title = format_ob(plane);
+ if (s_opts.print_props)
+ e4.lines = format_props(plane);
+
+ uint32_t fb_id = plane->fb_id();
+ if (fb_id) {
+ Framebuffer fb(card, fb_id);
+
+ Entry& e5 = add_entry(e4.children);
+
+ e5.title = format_ob(&fb);
+ }
+ }
+ }
+ }
+ }
+
+ print_entries(entries, "");
+}
+
+static void print_modes(Card& card)
+{
+ for (Connector* conn : card.get_connectors()) {
+ if (!conn->connected())
+ continue;
+
+ fmt::print("{}\n", format_ob(conn));
+
+ auto modes = conn->get_modes();
+ for (unsigned i = 0; i < modes.size(); ++i)
+ fmt::print("{}\n", format_mode(modes[i], i));
+ }
+}
+
+static const char* usage_str =
+ "Usage: kmsprint [OPTIONS]\n\n"
+ "Options:\n"
+ " --device=DEVICE DEVICE is the path to DRM card to open\n"
+ " -C, --card=NUM open /dev/dri/card<NUM>\n"
+ " -l, --list Print list instead of tree\n"
+ " -m, --modes Print modes\n"
+ " --xmode Print modes using X modeline\n"
+ " -p, --props Print properties\n";
+
+static void usage()
+{
+ puts(usage_str);
+}
+
+int main(int argc, char** argv)
+{
+ string dev_path;
+
+ OptionSet optionset = {
+ Option("|device=", [&dev_path](string s) {
+ dev_path = s;
+ }),
+ Option("C|card=", [&dev_path](string s) {
+ dev_path = "/dev/dri/card" + s;
+ }),
+ Option("l|list", []() {
+ s_opts.print_list = true;
+ }),
+ Option("m|modes", []() {
+ s_opts.print_modes = true;
+ }),
+ Option("p|props", []() {
+ s_opts.print_props = true;
+ }),
+ Option("|xmode", []() {
+ s_opts.x_modeline = true;
+ }),
+ Option("h|help", []() {
+ usage();
+ exit(-1);
+ }),
+ };
+
+ optionset.parse(argc, argv);
+
+ if (optionset.params().size() > 0) {
+ usage();
+ exit(-1);
+ }
+
+ Card card(dev_path);
+
+ if (s_opts.print_modes) {
+ print_modes(card);
+ return 0;
+ }
+
+ if (s_opts.print_list)
+ print_as_list(card);
+ else
+ print_as_tree(card);
+}
diff --git a/utils/kmstest.cpp b/utils/kmstest.cpp
new file mode 100644
index 0000000..cc21b92
--- /dev/null
+++ b/utils/kmstest.cpp
@@ -0,0 +1,1210 @@
+#include <cstdio>
+#include <cstring>
+#include <algorithm>
+#include <regex>
+#include <set>
+#include <chrono>
+#include <cstdint>
+#include <cinttypes>
+#include <thread>
+
+#include <sys/select.h>
+
+#include <kms++/format.h>
+
+#include <kms++/kms++.h>
+#include <kms++/modedb.h>
+#include <kms++/mode_cvt.h>
+
+#include <kms++util/kms++util.h>
+
+using namespace std;
+using namespace kms;
+
+struct PropInfo {
+ PropInfo(string n, uint64_t v)
+ : prop(NULL), name(n), val(v) {}
+
+ Property* prop;
+ string name;
+ uint64_t val;
+};
+
+struct PlaneInfo {
+ Plane* plane;
+
+ signed x;
+ signed y;
+ unsigned w;
+ unsigned h;
+
+ unsigned view_x;
+ unsigned view_y;
+ unsigned view_w;
+ unsigned view_h;
+
+ vector<Framebuffer*> fbs;
+
+ vector<PropInfo> props;
+};
+
+struct OutputInfo {
+ Connector* connector;
+
+ Crtc* crtc;
+ Videomode mode;
+ vector<Framebuffer*> legacy_fbs;
+
+ vector<PlaneInfo> planes;
+
+ vector<PropInfo> conn_props;
+ vector<PropInfo> crtc_props;
+};
+
+static bool s_use_dmt;
+static bool s_use_cea;
+static unsigned s_num_buffers = 1;
+static bool s_flip_mode;
+static bool s_flip_sync;
+static bool s_cvt;
+static bool s_cvt_v2;
+static bool s_cvt_vid_opt;
+static unsigned s_max_flips;
+static bool s_print_crc;
+static unsigned s_run_time;
+static TestPatternOptions s_pattern_options;
+
+__attribute__((unused)) static void print_regex_match(smatch sm)
+{
+ for (unsigned i = 0; i < sm.size(); ++i) {
+ string str = sm[i].str();
+ fmt::print("{}: {}\n", i, str);
+ }
+}
+
+static void get_connector(ResourceManager& resman, OutputInfo& output, const string& str = "")
+{
+ Connector* conn = resman.reserve_connector(str);
+
+ if (!conn)
+ EXIT("No connector '%s'", str.c_str());
+
+ output.connector = conn;
+ output.mode = output.connector->get_default_mode();
+}
+
+static void get_default_crtc(ResourceManager& resman, OutputInfo& output)
+{
+ output.crtc = resman.reserve_crtc(output.connector);
+
+ if (!output.crtc)
+ EXIT("Could not find available crtc");
+}
+
+static PlaneInfo* add_default_planeinfo(OutputInfo* output)
+{
+ output->planes.push_back(PlaneInfo{});
+ PlaneInfo* ret = &output->planes.back();
+ ret->w = output->mode.hdisplay;
+ ret->h = output->mode.vdisplay;
+ return ret;
+}
+
+static void parse_crtc(ResourceManager& resman, Card& card, const string& crtc_str, OutputInfo& output)
+{
+ // @12:1920x1200i@60
+ // @12:33000000,800/210/30/16/-,480/22/13/10/-,i
+
+ const regex modename_re("(?:(@?)(\\d+):)?" // @12:
+ "(?:(\\d+)x(\\d+)(i)?)" // 1920x1200i
+ "(?:@([\\d\\.]+))?"); // @60
+
+ const regex modeline_re("(?:(@?)(\\d+):)?" // @12:
+ "(\\d+)," // 33000000,
+ "(\\d+)/(\\d+)/(\\d+)/(\\d+)/([+-\\?])," // 800/210/30/16/-,
+ "(\\d+)/(\\d+)/(\\d+)/(\\d+)/([+-\\?])" // 480/22/13/10/-
+ "(?:,([i]+))?" // ,i
+ );
+
+ smatch sm;
+ if (regex_match(crtc_str, sm, modename_re)) {
+ if (sm[2].matched) {
+ bool use_id = sm[1].length() == 1;
+ unsigned num = stoul(sm[2].str());
+
+ if (use_id) {
+ Crtc* c = card.get_crtc(num);
+ if (!c)
+ EXIT("Bad crtc id '%u'", num);
+
+ output.crtc = c;
+ } else {
+ auto crtcs = card.get_crtcs();
+
+ if (num >= crtcs.size())
+ EXIT("Bad crtc number '%u'", num);
+
+ output.crtc = crtcs[num];
+ }
+ } else {
+ output.crtc = output.connector->get_current_crtc();
+ }
+
+ unsigned w = stoul(sm[3]);
+ unsigned h = stoul(sm[4]);
+ bool ilace = sm[5].matched ? true : false;
+ float refresh = sm[6].matched ? stof(sm[6]) : 0;
+
+ if (s_cvt) {
+ output.mode = videomode_from_cvt(w, h, refresh, ilace, s_cvt_v2, s_cvt_vid_opt);
+ } else if (s_use_dmt) {
+ try {
+ output.mode = find_dmt(w, h, refresh, ilace);
+ } catch (exception& e) {
+ EXIT("Mode not found from DMT tables\n");
+ }
+ } else if (s_use_cea) {
+ try {
+ output.mode = find_cea(w, h, refresh, ilace);
+ } catch (exception& e) {
+ EXIT("Mode not found from CEA tables\n");
+ }
+ } else {
+ try {
+ output.mode = output.connector->get_mode(w, h, refresh, ilace);
+ } catch (exception& e) {
+ EXIT("Mode not found from the connector\n");
+ }
+ }
+ } else if (regex_match(crtc_str, sm, modeline_re)) {
+ if (sm[2].matched) {
+ bool use_id = sm[1].length() == 1;
+ unsigned num = stoul(sm[2].str());
+
+ if (use_id) {
+ Crtc* c = card.get_crtc(num);
+ if (!c)
+ EXIT("Bad crtc id '%u'", num);
+
+ output.crtc = c;
+ } else {
+ auto crtcs = card.get_crtcs();
+
+ if (num >= crtcs.size())
+ EXIT("Bad crtc number '%u'", num);
+
+ output.crtc = crtcs[num];
+ }
+ } else {
+ output.crtc = output.connector->get_current_crtc();
+ }
+
+ unsigned clock = stoul(sm[3]);
+
+ unsigned hact = stoul(sm[4]);
+ unsigned hfp = stoul(sm[5]);
+ unsigned hsw = stoul(sm[6]);
+ unsigned hbp = stoul(sm[7]);
+
+ SyncPolarity h_sync;
+ switch (sm[8].str()[0]) {
+ case '+': h_sync = SyncPolarity::Positive; break;
+ case '-': h_sync = SyncPolarity::Negative; break;
+ default: h_sync = SyncPolarity::Undefined; break;
+ }
+
+ unsigned vact = stoul(sm[9]);
+ unsigned vfp = stoul(sm[10]);
+ unsigned vsw = stoul(sm[11]);
+ unsigned vbp = stoul(sm[12]);
+
+ SyncPolarity v_sync;
+ switch (sm[13].str()[0]) {
+ case '+': v_sync = SyncPolarity::Positive; break;
+ case '-': v_sync = SyncPolarity::Negative; break;
+ default: v_sync = SyncPolarity::Undefined; break;
+ }
+
+ output.mode = videomode_from_timings(clock / 1000, hact, hfp, hsw, hbp, vact, vfp, vsw, vbp);
+ output.mode.set_hsync(h_sync);
+ output.mode.set_vsync(v_sync);
+
+ if (sm[14].matched) {
+ for (int i = 0; i < sm[14].length(); ++i) {
+ char f = string(sm[14])[i];
+
+ switch (f) {
+ case 'i':
+ output.mode.set_interlace(true);
+ break;
+ default:
+ EXIT("Bad mode flag %c", f);
+ }
+ }
+ }
+ } else {
+ EXIT("Failed to parse crtc option '%s'", crtc_str.c_str());
+ }
+
+ if (output.crtc)
+ output.crtc = resman.reserve_crtc(output.crtc);
+ else
+ output.crtc = resman.reserve_crtc(output.connector);
+
+ if (!output.crtc)
+ EXIT("Could not find available crtc");
+}
+
+static void parse_plane(ResourceManager& resman, Card& card, const string& plane_str, const OutputInfo& output, PlaneInfo& pinfo)
+{
+ // 3:400,400-400x400
+ const regex plane_re("(?:(@?)(\\d+):)?" // 3:
+ "(?:(-?\\d+),(-?\\d+)-)?" // 400,400-
+ "(\\d+)x(\\d+)"); // 400x400
+
+ smatch sm;
+ if (!regex_match(plane_str, sm, plane_re))
+ EXIT("Failed to parse plane option '%s'", plane_str.c_str());
+
+ if (sm[2].matched) {
+ bool use_id = sm[1].length() == 1;
+ unsigned num = stoul(sm[2].str());
+
+ if (use_id) {
+ Plane* p = card.get_plane(num);
+ if (!p)
+ EXIT("Bad plane id '%u'", num);
+
+ pinfo.plane = p;
+ } else {
+ auto planes = card.get_planes();
+
+ if (num >= planes.size())
+ EXIT("Bad plane number '%u'", num);
+
+ pinfo.plane = planes[num];
+ }
+
+ auto plane = resman.reserve_plane(pinfo.plane);
+ if (!plane)
+ EXIT("Plane id %u is not available", pinfo.plane->id());
+ }
+
+ pinfo.w = stoul(sm[5]);
+ pinfo.h = stoul(sm[6]);
+
+ if (sm[3].matched)
+ pinfo.x = stol(sm[3]);
+ else
+ pinfo.x = output.mode.hdisplay / 2 - pinfo.w / 2;
+
+ if (sm[4].matched)
+ pinfo.y = stol(sm[4]);
+ else
+ pinfo.y = output.mode.vdisplay / 2 - pinfo.h / 2;
+}
+
+static void parse_prop(const string& prop_str, vector<PropInfo>& props)
+{
+ string name, val;
+
+ size_t split = prop_str.find("=");
+
+ if (split == string::npos)
+ EXIT("Equal sign ('=') not found in %s", prop_str.c_str());
+
+ name = prop_str.substr(0, split);
+ val = prop_str.substr(split + 1);
+
+ props.push_back(PropInfo(name, stoull(val, 0, 0)));
+}
+
+static void get_props(Card& card, vector<PropInfo>& props, const DrmPropObject* propobj)
+{
+ for (auto& pi : props)
+ pi.prop = propobj->get_prop(pi.name);
+}
+
+static vector<Framebuffer*> get_default_fb(Card& card, unsigned width, unsigned height)
+{
+ vector<Framebuffer*> v;
+
+ for (unsigned i = 0; i < s_num_buffers; ++i)
+ v.push_back(new DumbFramebuffer(card, width, height, PixelFormat::XRGB8888));
+
+ return v;
+}
+
+static void parse_fb(Card& card, const string& fb_str, OutputInfo* output, PlaneInfo* pinfo)
+{
+ unsigned w, h;
+ PixelFormat format = PixelFormat::XRGB8888;
+
+ if (pinfo) {
+ w = pinfo->w;
+ h = pinfo->h;
+ } else {
+ w = output->mode.hdisplay;
+ h = output->mode.vdisplay;
+ }
+
+ if (!fb_str.empty()) {
+ // XXX the regexp is not quite correct
+ // 400x400-NV12
+ const regex fb_re("(?:(\\d+)x(\\d+))?" // 400x400
+ "(?:-)?" // -
+ "(\\w+)?"); // NV12
+
+ smatch sm;
+ if (!regex_match(fb_str, sm, fb_re))
+ EXIT("Failed to parse fb option '%s'", fb_str.c_str());
+
+ if (sm[1].matched)
+ w = stoul(sm[1]);
+ if (sm[2].matched)
+ h = stoul(sm[2]);
+ if (sm[3].matched) {
+ try {
+ format = find_pixel_format_by_name(sm[3]);
+ } catch (const invalid_argument& e) {
+ format = fourcc_str_to_pixel_format(sm[3]);
+ }
+ }
+ }
+
+ vector<Framebuffer*> v;
+
+ for (unsigned i = 0; i < s_num_buffers; ++i)
+ v.push_back(new DumbFramebuffer(card, w, h, format));
+
+ if (pinfo)
+ pinfo->fbs = v;
+ else
+ output->legacy_fbs = v;
+}
+
+static void parse_view(const string& view_str, PlaneInfo& pinfo)
+{
+ const regex view_re("(\\d+),(\\d+)-(\\d+)x(\\d+)"); // 400,400-400x400
+
+ smatch sm;
+ if (!regex_match(view_str, sm, view_re))
+ EXIT("Failed to parse view option '%s'", view_str.c_str());
+
+ pinfo.view_x = stoul(sm[1]);
+ pinfo.view_y = stoul(sm[2]);
+ pinfo.view_w = stoul(sm[3]);
+ pinfo.view_h = stoul(sm[4]);
+}
+
+static const char* usage_str =
+ "Usage: kmstest [OPTION]...\n\n"
+ "Show a test pattern on a display or plane\n\n"
+ "Options:\n"
+ " --device=DEVICE DEVICE is the path to DRM card to open\n"
+ " -C, --card=NUM open /dev/dri/card<NUM>\n"
+ " -c, --connector=CONN CONN is <connector>\n"
+ " -r, --crtc=CRTC CRTC is [<crtc>:]<w>x<h>[@<Hz>]\n"
+ " or\n"
+ " [<crtc>:]<pclk>,<hact>/<hfp>/<hsw>/<hbp>/<hsp>,<vact>/<vfp>/<vsw>/<vbp>/<vsp>[,i]\n"
+ " -p, --plane=PLANE PLANE is [<plane>:][<x>,<y>-]<w>x<h>\n"
+ " -f, --fb=FB FB is [<w>x<h>][-][<fmtname>|<4cc>]\n"
+ " -v, --view=VIEW VIEW is <x>,<y>-<w>x<h>\n"
+ " -P, --property=PROP=VAL Set PROP to VAL in the previous DRM object\n"
+ " --dmt Search for the given mode from DMT tables\n"
+ " --cea Search for the given mode from CEA tables\n"
+ " --cvt=CVT Create videomode with CVT. CVT is 'v1', 'v2' or 'v2o'\n"
+ " --flip[=max] Do page flipping for each output with an optional maximum flips count\n"
+ " --sync Synchronize page flipping\n"
+ " --crc Print CRC16 for framebuffer contents\n"
+ " -t, --time=SECONDS Run for SECONDS and exit (no stdin read)\n"
+ " -T, --pattern=PAT test, white, black, red, green, blue, smpte\n"
+ " --rec=REC bt601, bt709, bt2020\n"
+ " --range=RANGE limited, full\n"
+ "\n"
+ "<connector>, <crtc> and <plane> can be given by index (<idx>) or id (@<id>).\n"
+ "<connector> can also be given by name.\n"
+ "\n"
+ "Options can be given multiple times to set up multiple displays or planes.\n"
+ "Options may apply to previous options, e.g. a plane will be set on a crtc set in\n"
+ "an earlier option.\n"
+ "If you omit parameters, kmstest tries to guess what you mean\n"
+ "\n"
+ "Examples:\n"
+ "\n"
+ "Set eDP-1 mode to 1920x1080@60, show XR24 framebuffer on the crtc, and a 400x400 XB24 plane:\n"
+ " kmstest -c eDP-1 -r 1920x1080@60 -f XR24 -p 400x400 -f XB24\n\n"
+ "XR24 framebuffer on first connected connector in the default mode:\n"
+ " kmstest -f XR24\n\n"
+ "XR24 framebuffer on a 400x400 plane on the first connected connector in the default mode:\n"
+ " kmstest -p 400x400 -f XR24\n\n"
+ "Test pattern on the second connector with default mode:\n"
+ " kmstest -c 1\n"
+ "\n"
+ "Environmental variables:\n"
+ " KMSXX_DISABLE_UNIVERSAL_PLANES Don't enable universal planes even if available\n"
+ " KMSXX_DISABLE_ATOMIC Don't enable atomic modesetting even if available\n";
+
+static void usage()
+{
+ puts(usage_str);
+}
+
+enum class ArgType {
+ Connector,
+ Crtc,
+ Plane,
+ Framebuffer,
+ View,
+ Property,
+};
+
+struct Arg {
+ ArgType type;
+ string arg;
+};
+
+static string s_device_path;
+
+static vector<Arg> parse_cmdline(int argc, char** argv)
+{
+ vector<Arg> args;
+
+ OptionSet optionset = {
+ Option("|device=",
+ [&](string s) {
+ s_device_path = s;
+ }),
+ Option("C|card=",
+ [&](string s) {
+ s_device_path = "/dev/dri/card" + s;
+ }),
+ Option("c|connector=",
+ [&](string s) {
+ args.push_back(Arg{ ArgType::Connector, s });
+ }),
+ Option("r|crtc=", [&](string s) {
+ args.push_back(Arg{ ArgType::Crtc, s });
+ }),
+ Option("p|plane=", [&](string s) {
+ args.push_back(Arg{ ArgType::Plane, s });
+ }),
+ Option("f|fb=", [&](string s) {
+ args.push_back(Arg{ ArgType::Framebuffer, s });
+ }),
+ Option("v|view=", [&](string s) {
+ args.push_back(Arg{ ArgType::View, s });
+ }),
+ Option("P|property=", [&](string s) {
+ args.push_back(Arg{ ArgType::Property, s });
+ }),
+ Option("|dmt", []() {
+ s_use_dmt = true;
+ }),
+ Option("|cea", []() {
+ s_use_cea = true;
+ }),
+ Option("|flip?", [&](string s) {
+ s_flip_mode = true;
+ s_num_buffers = 2;
+ if (!s.empty())
+ s_max_flips = stoi(s);
+ }),
+ Option("|sync", []() {
+ s_flip_sync = true;
+ }),
+ Option("|cvt=", [&](string s) {
+ if (s == "v1")
+ s_cvt = true;
+ else if (s == "v2")
+ s_cvt = s_cvt_v2 = true;
+ else if (s == "v2o")
+ s_cvt = s_cvt_v2 = s_cvt_vid_opt = true;
+ else {
+ usage();
+ exit(-1);
+ }
+ }),
+ Option("|crc", []() {
+ s_print_crc = true;
+ }),
+ Option("t|time=", [&](string s) {
+ s_run_time = stoul(s);
+ }),
+ Option("T|pattern=", [&](string s) {
+ s_pattern_options.pattern = s;
+ }),
+ Option("|rec=", [&](string s) {
+ s = to_lower(s);
+
+ if (s == "bt601")
+ s_pattern_options.rec = RecStandard::BT601;
+ else if (s == "bt709")
+ s_pattern_options.rec = RecStandard::BT709;
+ else if (s == "bt2020")
+ s_pattern_options.rec = RecStandard::BT2020;
+ else {
+ usage();
+ exit(-1);
+ }
+ }),
+ Option("|range=", [&](string s) {
+ s = to_lower(s);
+
+ if (s == "limited")
+ s_pattern_options.range = ColorRange::Limited;
+ else if (s == "full")
+ s_pattern_options.range = ColorRange::Full;
+ else {
+ usage();
+ exit(-1);
+ }
+ }),
+ Option("h|help", [&]() {
+ usage();
+ exit(-1);
+ }),
+ };
+
+ optionset.parse(argc, argv);
+
+ if (optionset.params().size() > 0) {
+ usage();
+ exit(-1);
+ }
+
+ return args;
+}
+
+static vector<OutputInfo> setups_to_outputs(Card& card, ResourceManager& resman, const vector<Arg>& output_args)
+{
+ vector<OutputInfo> outputs;
+
+ OutputInfo* current_output = 0;
+ PlaneInfo* current_plane = 0;
+
+ for (const auto& arg : output_args) {
+ switch (arg.type) {
+ case ArgType::Connector: {
+ outputs.push_back(OutputInfo{});
+ current_output = &outputs.back();
+
+ get_connector(resman, *current_output, arg.arg);
+ current_plane = 0;
+
+ break;
+ }
+
+ case ArgType::Crtc: {
+ if (!current_output) {
+ outputs.push_back(OutputInfo{});
+ current_output = &outputs.back();
+ }
+
+ if (!current_output->connector)
+ get_connector(resman, *current_output);
+
+ parse_crtc(resman, card, arg.arg, *current_output);
+
+ current_plane = 0;
+
+ break;
+ }
+
+ case ArgType::Plane: {
+ if (!current_output) {
+ outputs.push_back(OutputInfo{});
+ current_output = &outputs.back();
+ }
+
+ if (!current_output->connector)
+ get_connector(resman, *current_output);
+
+ if (!current_output->crtc)
+ get_default_crtc(resman, *current_output);
+
+ current_plane = add_default_planeinfo(current_output);
+
+ parse_plane(resman, card, arg.arg, *current_output, *current_plane);
+
+ break;
+ }
+
+ case ArgType::Framebuffer: {
+ if (!current_output) {
+ outputs.push_back(OutputInfo{});
+ current_output = &outputs.back();
+ }
+
+ if (!current_output->connector)
+ get_connector(resman, *current_output);
+
+ if (!current_output->crtc)
+ get_default_crtc(resman, *current_output);
+
+ if (!current_plane && card.has_atomic())
+ current_plane = add_default_planeinfo(current_output);
+
+ parse_fb(card, arg.arg, current_output, current_plane);
+
+ break;
+ }
+
+ case ArgType::View: {
+ if (!current_plane || current_plane->fbs.empty())
+ EXIT("'view' parameter requires a plane and a fb");
+
+ parse_view(arg.arg, *current_plane);
+ break;
+ }
+
+ case ArgType::Property: {
+ if (!current_output)
+ EXIT("No object to which set the property");
+
+ if (current_plane)
+ parse_prop(arg.arg, current_plane->props);
+ else if (current_output->crtc)
+ parse_prop(arg.arg, current_output->crtc_props);
+ else if (current_output->connector)
+ parse_prop(arg.arg, current_output->conn_props);
+ else
+ EXIT("no object");
+
+ break;
+ }
+ }
+ }
+
+ if (outputs.empty()) {
+ // no outputs defined, show a pattern on all connected screens
+ for (Connector* conn : card.get_connectors()) {
+ if (!conn->connected())
+ continue;
+
+ OutputInfo output = {};
+ output.connector = resman.reserve_connector(conn);
+ EXIT_IF(!output.connector, "Failed to reserve connector %s", conn->fullname().c_str());
+ output.crtc = resman.reserve_crtc(conn);
+ EXIT_IF(!output.crtc, "Failed to reserve crtc for %s", conn->fullname().c_str());
+ output.mode = output.connector->get_default_mode();
+
+ outputs.push_back(output);
+ }
+ }
+
+ for (OutputInfo& o : outputs) {
+ get_props(card, o.conn_props, o.connector);
+
+ if (!o.crtc)
+ get_default_crtc(resman, o);
+
+ get_props(card, o.crtc_props, o.crtc);
+
+ if (!o.mode.valid())
+ EXIT("Mode not valid for %s", o.connector->fullname().c_str());
+
+ if (card.has_atomic()) {
+ if (o.planes.empty())
+ add_default_planeinfo(&o);
+ } else {
+ if (o.legacy_fbs.empty())
+ o.legacy_fbs = get_default_fb(card, o.mode.hdisplay, o.mode.vdisplay);
+ }
+
+ for (PlaneInfo& p : o.planes) {
+ if (p.fbs.empty())
+ p.fbs = get_default_fb(card, p.w, p.h);
+ }
+
+ for (PlaneInfo& p : o.planes) {
+ if (!p.plane) {
+ if (card.has_atomic())
+ p.plane = resman.reserve_generic_plane(o.crtc, p.fbs[0]->format());
+ else
+ p.plane = resman.reserve_overlay_plane(o.crtc, p.fbs[0]->format());
+
+ if (!p.plane)
+ EXIT("Failed to find available plane");
+ }
+ get_props(card, p.props, p.plane);
+ }
+ }
+
+ return outputs;
+}
+
+static uint16_t crc16(uint16_t crc, uint8_t data)
+{
+ const uint16_t CRC16_IBM = 0x8005;
+
+ for (uint8_t i = 0; i < 8; i++) {
+ if (((crc & 0x8000) >> 8) ^ (data & 0x80))
+ crc = (crc << 1) ^ CRC16_IBM;
+ else
+ crc = (crc << 1);
+
+ data <<= 1;
+ }
+
+ return crc;
+}
+
+static string fb_crc(IFramebuffer* fb)
+{
+ uint8_t* p = fb->map(0);
+ uint16_t r, g, b;
+
+ r = g = b = 0;
+
+ for (unsigned y = 0; y < fb->height(); ++y) {
+ for (unsigned x = 0; x < fb->width(); ++x) {
+ uint32_t* p32 = reinterpret_cast<uint32_t*>(p + fb->stride(0) * y + x * 4);
+ RGB rgb(*p32);
+
+ r = crc16(r, rgb.r);
+ r = crc16(r, 0);
+
+ g = crc16(g, rgb.g);
+ g = crc16(g, 0);
+
+ b = crc16(b, rgb.b);
+ b = crc16(b, 0);
+ }
+ }
+
+ return fmt::format("{:#06x} {:#06x} {:#06x}", r, g, b);
+}
+
+static void print_outputs(const vector<OutputInfo>& outputs)
+{
+ for (unsigned i = 0; i < outputs.size(); ++i) {
+ const OutputInfo& o = outputs[i];
+
+ fmt::print("Connector {}/@{}: {}", o.connector->idx(), o.connector->id(),
+ o.connector->fullname());
+
+ for (const PropInfo& prop : o.conn_props)
+ fmt::print(" {}={}", prop.prop->name(), prop.val);
+
+ fmt::print("\n Crtc {}/@{}", o.crtc->idx(), o.crtc->id());
+
+ for (const PropInfo& prop : o.crtc_props)
+ fmt::print(" {}={}", prop.prop->name(), prop.val);
+
+ fmt::print(": {}\n", o.mode.to_string_long());
+
+ if (!o.legacy_fbs.empty()) {
+ auto fb = o.legacy_fbs[0];
+ fmt::print(" Fb {} {}x{}-{}\n", fb->id(), fb->width(), fb->height(), pixel_format_to_fourcc_str(fb->format()));
+ }
+
+ for (unsigned j = 0; j < o.planes.size(); ++j) {
+ const PlaneInfo& p = o.planes[j];
+ auto fb = p.fbs[0];
+ fmt::print(" Plane {}/@{}: {},{}-{}x{}", p.plane->idx(), p.plane->id(),
+ p.x, p.y, p.w, p.h);
+ for (const PropInfo& prop : p.props)
+ fmt::print(" {}={}", prop.prop->name(), prop.val);
+ fmt::print("\n");
+
+ fmt::print(" Fb {} {}x{}-{}\n", fb->id(), fb->width(), fb->height(),
+ pixel_format_to_fourcc_str(fb->format()));
+ if (s_print_crc)
+ fmt::print(" CRC16 {}\n", fb_crc(fb).c_str());
+ }
+ }
+}
+
+static void draw_test_patterns(const vector<OutputInfo>& outputs)
+{
+ for (const OutputInfo& o : outputs) {
+ for (auto fb : o.legacy_fbs)
+ draw_test_pattern(*fb, s_pattern_options);
+
+ for (const PlaneInfo& p : o.planes)
+ for (auto fb : p.fbs)
+ draw_test_pattern(*fb, s_pattern_options);
+ }
+}
+
+static void set_crtcs_n_planes_legacy(Card& card, const vector<OutputInfo>& outputs)
+{
+ // Disable unused crtcs
+ for (Crtc* crtc : card.get_crtcs()) {
+ if (find_if(outputs.begin(), outputs.end(), [crtc](const OutputInfo& o) { return o.crtc == crtc; }) != outputs.end())
+ continue;
+
+ crtc->disable_mode();
+ }
+
+ for (const OutputInfo& o : outputs) {
+ int r;
+ auto conn = o.connector;
+ auto crtc = o.crtc;
+
+ for (const PropInfo& prop : o.conn_props) {
+ r = conn->set_prop_value(prop.prop, prop.val);
+ EXIT_IF(r, "failed to set connector property %s\n", prop.name.c_str());
+ }
+
+ for (const PropInfo& prop : o.crtc_props) {
+ r = crtc->set_prop_value(prop.prop, prop.val);
+ EXIT_IF(r, "failed to set crtc property %s\n", prop.name.c_str());
+ }
+
+ if (!o.legacy_fbs.empty()) {
+ auto fb = o.legacy_fbs[0];
+ r = crtc->set_mode(conn, *fb, o.mode);
+ if (r)
+ fmt::print(stderr, "crtc->set_mode() failed for crtc {}: {}\n",
+ crtc->id(), strerror(-r));
+ }
+
+ for (const PlaneInfo& p : o.planes) {
+ for (const PropInfo& prop : p.props) {
+ r = p.plane->set_prop_value(prop.prop, prop.val);
+ EXIT_IF(r, "failed to set plane property %s\n", prop.name.c_str());
+ }
+
+ auto fb = p.fbs[0];
+ r = crtc->set_plane(p.plane, *fb,
+ p.x, p.y, p.w, p.h,
+ 0, 0, fb->width(), fb->height());
+ if (r)
+ fmt::print(stderr, "crtc->set_plane() failed for plane {}: {}\n",
+ p.plane->id(), strerror(-r));
+ }
+ }
+}
+
+static void set_crtcs_n_planes_atomic(Card& card, const vector<OutputInfo>& outputs)
+{
+ int r;
+
+ // XXX DRM framework doesn't allow moving an active plane from one crtc to another.
+ // See drm_atomic.c::plane_switching_crtc().
+ // For the time being, try and disable all crtcs and planes here.
+ // Do not check the return value as some simple displays don't support the crtc being
+ // enabled but the primary plane being disabled.
+
+ AtomicReq disable_req(card);
+
+ // Disable unused crtcs
+ for (Crtc* crtc : card.get_crtcs()) {
+ //if (find_if(outputs.begin(), outputs.end(), [crtc](const OutputInfo& o) { return o.crtc == crtc; }) != outputs.end())
+ // continue;
+
+ disable_req.add(crtc, {
+ { "ACTIVE", 0 },
+ });
+ }
+
+ // Disable unused planes
+ for (Plane* plane : card.get_planes())
+ disable_req.add(plane, {
+ { "FB_ID", 0 },
+ { "CRTC_ID", 0 },
+ });
+
+ disable_req.commit_sync(true);
+
+ // Keep blobs here so that we keep ref to them until we have committed the req
+ vector<unique_ptr<Blob>> blobs;
+
+ AtomicReq req(card);
+
+ for (const OutputInfo& o : outputs) {
+ auto conn = o.connector;
+ auto crtc = o.crtc;
+
+ blobs.emplace_back(o.mode.to_blob(card));
+ Blob* mode_blob = blobs.back().get();
+
+ req.add(conn, {
+ { "CRTC_ID", crtc->id() },
+ });
+
+ for (const PropInfo& prop : o.conn_props)
+ req.add(conn, prop.prop, prop.val);
+
+ req.add(crtc, {
+ { "ACTIVE", 1 },
+ { "MODE_ID", mode_blob->id() },
+ });
+
+ for (const PropInfo& prop : o.crtc_props)
+ req.add(crtc, prop.prop, prop.val);
+
+ for (const PlaneInfo& p : o.planes) {
+ auto fb = p.fbs[0];
+
+ req.add(p.plane, {
+ { "FB_ID", fb->id() },
+ { "CRTC_ID", crtc->id() },
+ { "SRC_X", (p.view_x ?: 0) << 16 },
+ { "SRC_Y", (p.view_y ?: 0) << 16 },
+ { "SRC_W", (p.view_w ?: fb->width()) << 16 },
+ { "SRC_H", (p.view_h ?: fb->height()) << 16 },
+ { "CRTC_X", p.x },
+ { "CRTC_Y", p.y },
+ { "CRTC_W", p.w },
+ { "CRTC_H", p.h },
+ });
+
+ for (const PropInfo& prop : p.props)
+ req.add(p.plane, prop.prop, prop.val);
+ }
+ }
+
+ r = req.test(true);
+ if (r)
+ EXIT("Atomic test failed: %d\n", r);
+
+ r = req.commit_sync(true);
+ if (r)
+ EXIT("Atomic commit failed: %d\n", r);
+}
+
+static void set_crtcs_n_planes(Card& card, const vector<OutputInfo>& outputs)
+{
+ if (card.has_atomic())
+ set_crtcs_n_planes_atomic(card, outputs);
+ else
+ set_crtcs_n_planes_legacy(card, outputs);
+}
+
+static bool max_flips_reached;
+
+class FlipState : private PageFlipHandlerBase
+{
+public:
+ FlipState(Card& card, const string& name, const vector<const OutputInfo*>& outputs)
+ : m_card(card), m_name(name), m_outputs(outputs), m_frame_num(0), m_flip_count(0)
+ {
+ }
+
+ void start_flipping()
+ {
+ m_prev_frame = m_prev_print = std::chrono::steady_clock::now();
+ m_slowest_frame = std::chrono::duration<float>::min();
+ m_frame_num = 0;
+ queue_next();
+ }
+
+private:
+ void handle_page_flip(uint32_t frame, double time)
+ {
+ /*
+ * We get flip event for each crtc in this flipstate. We can commit the next frames
+ * only after we've gotten the flip event for all crtcs
+ */
+ if (++m_flip_count < m_outputs.size())
+ return;
+
+ m_frame_num++;
+ if (s_max_flips && m_frame_num >= s_max_flips)
+ max_flips_reached = true;
+
+ auto now = std::chrono::steady_clock::now();
+
+ std::chrono::duration<float> diff = now - m_prev_frame;
+ if (diff > m_slowest_frame)
+ m_slowest_frame = diff;
+
+ if (m_frame_num % 100 == 0) {
+ std::chrono::duration<float> fsec = now - m_prev_print;
+ fmt::print("Connector {}: fps {:.2f}, slowest {:.2f} ms\n",
+ m_name.c_str(),
+ 100.0 / fsec.count(),
+ m_slowest_frame.count() * 1000);
+ m_prev_print = now;
+ m_slowest_frame = std::chrono::duration<float>::min();
+ }
+
+ m_prev_frame = now;
+
+ queue_next();
+ }
+
+ static unsigned get_bar_pos(Framebuffer* fb, unsigned frame_num)
+ {
+ return (frame_num * bar_speed) % (fb->width() - bar_width + 1);
+ }
+
+ static void draw_bar(Framebuffer* fb, unsigned frame_num)
+ {
+ int old_xpos = frame_num < s_num_buffers ? -1 : get_bar_pos(fb, frame_num - s_num_buffers);
+ int new_xpos = get_bar_pos(fb, frame_num);
+
+ draw_color_bar(*fb, old_xpos, new_xpos, bar_width);
+ draw_text(*fb, fb->width() / 2, 0, to_string(frame_num), RGB(255, 255, 255));
+ }
+
+ static void do_flip_output(AtomicReq& req, unsigned frame_num, const OutputInfo& o)
+ {
+ unsigned cur = frame_num % s_num_buffers;
+
+ for (const PlaneInfo& p : o.planes) {
+ auto fb = p.fbs[cur];
+
+ draw_bar(fb, frame_num);
+
+ req.add(p.plane, {
+ { "FB_ID", fb->id() },
+ });
+ }
+ }
+
+ void do_flip_output_legacy(unsigned frame_num, const OutputInfo& o)
+ {
+ unsigned cur = frame_num % s_num_buffers;
+
+ if (!o.legacy_fbs.empty()) {
+ auto fb = o.legacy_fbs[cur];
+
+ draw_bar(fb, frame_num);
+
+ int r = o.crtc->page_flip(*fb, this);
+ ASSERT(r == 0);
+ }
+
+ for (const PlaneInfo& p : o.planes) {
+ auto fb = p.fbs[cur];
+
+ draw_bar(fb, frame_num);
+
+ int r = o.crtc->set_plane(p.plane, *fb,
+ p.x, p.y, p.w, p.h,
+ 0, 0, fb->width(), fb->height());
+ ASSERT(r == 0);
+ }
+ }
+
+ void queue_next()
+ {
+ m_flip_count = 0;
+
+ if (m_card.has_atomic()) {
+ AtomicReq req(m_card);
+
+ for (auto o : m_outputs)
+ do_flip_output(req, m_frame_num, *o);
+
+ int r = req.commit(this);
+ if (r)
+ EXIT("Flip commit failed: %d\n", r);
+ } else {
+ ASSERT(m_outputs.size() == 1);
+ do_flip_output_legacy(m_frame_num, *m_outputs[0]);
+ }
+ }
+
+ Card& m_card;
+ string m_name;
+ vector<const OutputInfo*> m_outputs;
+ unsigned m_frame_num;
+ unsigned m_flip_count;
+
+ chrono::steady_clock::time_point m_prev_print;
+ chrono::steady_clock::time_point m_prev_frame;
+ chrono::duration<float> m_slowest_frame;
+
+ static const unsigned bar_width = 20;
+ static const unsigned bar_speed = 8;
+};
+
+static void main_flip(Card& card, const vector<OutputInfo>& outputs)
+{
+// clang-tidy does not seem to handle FD_xxx macros
+#ifndef __clang_analyzer__
+ fd_set fds;
+
+ FD_ZERO(&fds);
+
+ int fd = card.fd();
+
+ vector<unique_ptr<FlipState>> flipstates;
+
+ if (!s_flip_sync) {
+ for (const OutputInfo& o : outputs) {
+ auto fs = unique_ptr<FlipState>(new FlipState(card, to_string(o.connector->idx()), { &o }));
+ flipstates.push_back(std::move(fs));
+ }
+ } else {
+ vector<const OutputInfo*> ois;
+
+ string name;
+ for (const OutputInfo& o : outputs) {
+ name += to_string(o.connector->idx()) + ",";
+ ois.push_back(&o);
+ }
+
+ auto fs = unique_ptr<FlipState>(new FlipState(card, name, ois));
+ flipstates.push_back(std::move(fs));
+ }
+
+ for (unique_ptr<FlipState>& fs : flipstates)
+ fs->start_flipping();
+
+ auto deadline = chrono::steady_clock::now() + chrono::seconds(s_run_time);
+
+ while (!max_flips_reached) {
+ int r;
+
+ if (s_run_time == 0)
+ FD_SET(0, &fds);
+ FD_SET(fd, &fds);
+
+ r = select(fd + 1, &fds, NULL, NULL, NULL);
+ if (r < 0) {
+ fmt::print(stderr, "select() failed with {}: {}\n", errno, strerror(errno));
+ break;
+ } else if (FD_ISSET(0, &fds)) {
+ fmt::print(stderr, "Exit due to user-input\n");
+ break;
+ } else if (FD_ISSET(fd, &fds)) {
+ card.call_page_flip_handlers();
+ }
+
+ if (s_run_time > 0 && chrono::steady_clock::now() >= deadline)
+ break;
+ }
+#endif
+}
+
+int main(int argc, char** argv)
+{
+ vector<Arg> output_args = parse_cmdline(argc, argv);
+
+ Card card(s_device_path);
+
+ if (!card.is_master())
+ EXIT("Could not get DRM master permission. Card already in use?");
+
+ if (!card.has_atomic() && s_flip_sync)
+ EXIT("Synchronized flipping requires atomic modesetting");
+
+ ResourceManager resman(card);
+
+ vector<OutputInfo> outputs = setups_to_outputs(card, resman, output_args);
+
+ if (!s_flip_mode)
+ draw_test_patterns(outputs);
+
+ print_outputs(outputs);
+
+ set_crtcs_n_planes(card, outputs);
+
+ if (s_run_time > 0)
+ fmt::print("running for {} seconds\n", s_run_time);
+ else
+ fmt::print("press enter to exit\n");
+
+ if (s_flip_mode)
+ main_flip(card, outputs);
+ else if (s_run_time > 0)
+ std::this_thread::sleep_for(std::chrono::seconds(s_run_time));
+ else
+ getchar();
+}
diff --git a/utils/kmstouch.cpp b/utils/kmstouch.cpp
new file mode 100644
index 0000000..f4d4c2a
--- /dev/null
+++ b/utils/kmstouch.cpp
@@ -0,0 +1,293 @@
+#include <cstdio>
+#include <unistd.h>
+#include <algorithm>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <cstring>
+
+#include <libevdev/libevdev.h>
+#include <libevdev/libevdev-uinput.h>
+
+#include <kms++/kms++.h>
+#include <kms++util/kms++util.h>
+
+using namespace std;
+using namespace kms;
+
+static const char* usage_str =
+ "Usage: kmstouch [OPTION]...\n\n"
+ "Simple touchscreen tester\n\n"
+ "Options:\n"
+ " --input=DEVICE DEVICE is the path to input device to open\n"
+ " --device=DEVICE DEVICE is the path to DRM card to open\n"
+ " -c, --connector=CONN CONN is <connector>\n"
+ "\n";
+
+static void usage()
+{
+ puts(usage_str);
+}
+
+static bool s_print_ev = false;
+
+static vector<pair<int32_t, int32_t>> s_coords;
+
+// axis -> min,max
+static map<int, pair<int32_t, int32_t>> s_abs_map;
+// axis -> value
+static map<int, int32_t> s_abs_vals;
+
+static void print_abs_bits(struct libevdev* dev, int axis)
+{
+ const struct input_absinfo* abs;
+
+ if (!libevdev_has_event_code(dev, EV_ABS, axis))
+ return;
+
+ abs = libevdev_get_abs_info(dev, axis);
+
+ printf(" Value %6d\n", abs->value);
+ printf(" Min %6d\n", abs->minimum);
+ printf(" Max %6d\n", abs->maximum);
+ if (abs->fuzz)
+ printf(" Fuzz %6d\n", abs->fuzz);
+ if (abs->flat)
+ printf(" Flat %6d\n", abs->flat);
+ if (abs->resolution)
+ printf(" Resolution %6d\n", abs->resolution);
+}
+
+static void print_code_bits(struct libevdev* dev, unsigned int type, unsigned int max)
+{
+ for (uint32_t i = 0; i <= max; i++) {
+ if (!libevdev_has_event_code(dev, type, i))
+ continue;
+
+ printf(" Event code %u (%s)\n", i, libevdev_event_code_get_name(type, i));
+ if (type == EV_ABS)
+ print_abs_bits(dev, i);
+ }
+}
+
+static void print_bits(struct libevdev* dev)
+{
+ printf("Supported events:\n");
+
+ for (uint32_t i = 0; i <= EV_MAX; i++) {
+ if (!libevdev_has_event_type(dev, i))
+ continue;
+
+ printf(" Event type %u (%s)\n", i, libevdev_event_type_get_name(i));
+
+ switch (i) {
+ case EV_KEY:
+ print_code_bits(dev, EV_KEY, KEY_MAX);
+ break;
+ case EV_REL:
+ print_code_bits(dev, EV_REL, REL_MAX);
+ break;
+ case EV_ABS:
+ print_code_bits(dev, EV_ABS, ABS_MAX);
+ break;
+ case EV_LED:
+ print_code_bits(dev, EV_LED, LED_MAX);
+ break;
+ }
+ }
+}
+
+static void collect_current(struct libevdev* dev)
+{
+ for (uint32_t i = 0; i <= ABS_MAX; i++) {
+ if (!libevdev_has_event_code(dev, EV_ABS, i))
+ continue;
+
+ const struct input_absinfo* abs;
+
+ abs = libevdev_get_abs_info(dev, i);
+
+ s_abs_vals[i] = abs->value;
+ s_abs_map[i] = make_pair(abs->minimum, abs->maximum);
+ }
+}
+
+static void print_props(struct libevdev* dev)
+{
+ printf("Properties:\n");
+
+ for (uint32_t i = 0; i <= INPUT_PROP_MAX; i++) {
+ if (!libevdev_has_property(dev, i))
+ continue;
+
+ printf(" Property type %u (%s)\n", i, libevdev_property_get_name(i));
+ }
+}
+
+static void handle_event(struct input_event& ev, DumbFramebuffer* fb)
+{
+ static vector<pair<uint16_t, int32_t>> s_event_vec;
+
+ if (s_print_ev)
+ printf("%-6s %20s %6d\n",
+ libevdev_event_type_get_name(ev.type),
+ libevdev_event_code_get_name(ev.type, ev.code),
+ ev.value);
+
+ switch (ev.type) {
+ case EV_ABS:
+ s_event_vec.emplace_back(ev.code, ev.value);
+ break;
+
+ case EV_KEY:
+ s_event_vec.emplace_back(ev.code, ev.value);
+ break;
+
+ case EV_SYN:
+ switch (ev.code) {
+ case SYN_REPORT: {
+ int32_t min_x = s_abs_map[ABS_X].first;
+ int32_t max_x = s_abs_map[ABS_X].second;
+
+ int32_t min_y = s_abs_map[ABS_Y].first;
+ int32_t max_y = s_abs_map[ABS_Y].second;
+
+ for (const auto& p : s_event_vec) {
+ switch (p.first) {
+ case ABS_X:
+ case ABS_Y:
+ s_abs_vals[p.first] = p.second;
+ break;
+ default:
+ break;
+ }
+ }
+
+ int32_t abs_x = s_abs_vals[ABS_X];
+ int32_t abs_y = s_abs_vals[ABS_Y];
+
+ int32_t x = (abs_x - min_x) * (fb->width() - 1) / (max_x - min_x);
+ int32_t y = (abs_y - min_y) * (fb->height() - 1) / (max_y - min_y);
+
+ printf("%d, %d -> %d, %d\n", abs_x, abs_y, x, y);
+
+ draw_rgb_pixel(*fb, x, y, RGB(255, 255, 255));
+
+ s_event_vec.clear();
+
+ if (s_print_ev)
+ printf("----\n");
+ break;
+ }
+
+ default:
+ EXIT("Unhandled syn event code %u\n", ev.code);
+ break;
+ }
+
+ break;
+
+ default:
+ EXIT("Unhandled event type %u\n", ev.type);
+ break;
+ }
+}
+
+int main(int argc, char** argv)
+{
+ string drm_dev_path = "/dev/dri/card0";
+ string input_dev_path = "/dev/input/event0";
+ string conn_name;
+
+ OptionSet optionset = {
+ Option("i|input=", [&input_dev_path](string s) {
+ input_dev_path = s;
+ }),
+ Option("|device=", [&drm_dev_path](string s) {
+ drm_dev_path = s;
+ }),
+ Option("c|connector=", [&conn_name](string s) {
+ conn_name = s;
+ }),
+ Option("h|help", []() {
+ usage();
+ exit(-1);
+ }),
+ };
+
+ optionset.parse(argc, argv);
+
+ if (optionset.params().size() > 0) {
+ usage();
+ exit(-1);
+ }
+
+ struct libevdev* dev = nullptr;
+
+ int fd = open(input_dev_path.c_str(), O_RDONLY | O_NONBLOCK);
+ FAIL_IF(fd < 0, "Failed to open input device %s: %s\n", input_dev_path.c_str(), strerror(errno));
+ int rc = libevdev_new_from_fd(fd, &dev);
+ FAIL_IF(rc < 0, "Failed to init libevdev (%s)\n", strerror(-rc));
+
+ printf("Input device name: \"%s\"\n", libevdev_get_name(dev));
+ printf("Input device ID: bus %#x vendor %#x product %#x\n",
+ libevdev_get_id_bustype(dev),
+ libevdev_get_id_vendor(dev),
+ libevdev_get_id_product(dev));
+
+ if (!libevdev_has_event_type(dev, EV_ABS) ||
+ !libevdev_has_event_code(dev, EV_KEY, BTN_TOUCH)) {
+ printf("This device does not look like a mouse\n");
+ exit(1);
+ }
+
+ print_bits(dev);
+ print_props(dev);
+
+ collect_current(dev);
+
+ Card card(drm_dev_path);
+ ResourceManager resman(card);
+
+ auto pixfmt = PixelFormat::XRGB8888;
+
+ Connector* conn = resman.reserve_connector(conn_name);
+ Crtc* crtc = resman.reserve_crtc(conn);
+ Plane* plane = resman.reserve_overlay_plane(crtc, pixfmt);
+
+ Videomode mode = conn->get_default_mode();
+
+ uint32_t w = mode.hdisplay;
+ uint32_t h = mode.vdisplay;
+
+ auto fb = new DumbFramebuffer(card, w, h, pixfmt);
+
+ AtomicReq req(card);
+
+ req.add(plane, "CRTC_ID", crtc->id());
+ req.add(plane, "FB_ID", fb->id());
+
+ req.add(plane, "CRTC_X", 0);
+ req.add(plane, "CRTC_Y", 0);
+ req.add(plane, "CRTC_W", w);
+ req.add(plane, "CRTC_H", h);
+
+ req.add(plane, "SRC_X", 0);
+ req.add(plane, "SRC_Y", 0);
+ req.add(plane, "SRC_W", w << 16);
+ req.add(plane, "SRC_H", h << 16);
+
+ int r = req.commit_sync();
+ FAIL_IF(r, "initial plane setup failed");
+
+ do {
+ struct input_event ev {
+ };
+ rc = libevdev_next_event(dev, LIBEVDEV_READ_FLAG_NORMAL, &ev);
+ if (rc == 0)
+ handle_event(ev, fb);
+
+ } while (rc == 1 || rc == 0 || rc == -EAGAIN);
+
+ delete fb;
+}
diff --git a/utils/kmsview.cpp b/utils/kmsview.cpp
new file mode 100644
index 0000000..7cee024
--- /dev/null
+++ b/utils/kmsview.cpp
@@ -0,0 +1,121 @@
+#include <cstdio>
+#include <fstream>
+#include <unistd.h>
+#include <cassert>
+
+#include <kms++/kms++.h>
+#include <kms++util/kms++util.h>
+
+using namespace std;
+using namespace kms;
+
+static void read_frame(ifstream& is, DumbFramebuffer* fb, Crtc* crtc, Plane* plane)
+{
+ for (unsigned i = 0; i < fb->num_planes(); ++i)
+ is.read(reinterpret_cast<char*>(fb->map(i)), fb->size(i));
+
+ unsigned w = min(crtc->width(), fb->width());
+ unsigned h = min(crtc->height(), fb->height());
+
+ int r = crtc->set_plane(plane, *fb,
+ 0, 0, w, h,
+ 0, 0, fb->width(), fb->height());
+
+ ASSERT(r == 0);
+}
+
+static const char* usage_str =
+ "Usage: kmsview [options] <file> <width> <height> <fourcc>\n\n"
+ "Options:\n"
+ " -c, --connector <name> Output connector\n"
+ " -t, --time <ms> Milliseconds to sleep between frames\n";
+
+static void usage()
+{
+ puts(usage_str);
+}
+
+int main(int argc, char** argv)
+{
+ uint32_t time = 0;
+ string dev_path;
+ string conn_name;
+
+ OptionSet optionset = {
+ Option("c|connector=", [&conn_name](string s) {
+ conn_name = s;
+ }),
+ Option("|device=", [&dev_path](string s) {
+ dev_path = s;
+ }),
+ Option("t|time=", [&time](const string& str) {
+ time = stoul(str);
+ }),
+ Option("h|help", []() {
+ usage();
+ exit(-1);
+ }),
+ };
+
+ optionset.parse(argc, argv);
+
+ vector<string> params = optionset.params();
+
+ if (params.size() != 4) {
+ usage();
+ exit(-1);
+ }
+
+ string filename = params[0];
+ uint32_t w = stoi(params[1]);
+ uint32_t h = stoi(params[2]);
+ string modestr = params[3];
+
+ auto pixfmt = fourcc_str_to_pixel_format(modestr);
+
+ ifstream is(filename, ifstream::binary);
+
+ is.seekg(0, std::ios::end);
+ unsigned fsize = is.tellg();
+ is.seekg(0);
+
+ Card card(dev_path);
+ ResourceManager res(card);
+
+ auto conn = res.reserve_connector(conn_name);
+ auto crtc = res.reserve_crtc(conn);
+ auto plane = res.reserve_overlay_plane(crtc, pixfmt);
+ FAIL_IF(!plane, "available plane not found");
+
+ auto fb = new DumbFramebuffer(card, w, h, pixfmt);
+
+ unsigned frame_size = 0;
+ for (unsigned i = 0; i < fb->num_planes(); ++i)
+ frame_size += fb->size(i);
+
+ assert(frame_size);
+
+ unsigned num_frames = fsize / frame_size;
+ printf("file size %u, frame size %u, frames %u\n", fsize, frame_size, num_frames);
+
+ for (unsigned i = 0; i < num_frames; ++i) {
+ printf("frame %u", i);
+ fflush(stdout);
+ read_frame(is, fb, crtc, plane);
+ if (!time) {
+ getchar();
+ } else {
+ usleep(time * 1000);
+ printf("\n");
+ }
+ }
+
+ is.close();
+
+ if (time) {
+ printf("press enter to exit\n");
+ getchar();
+ }
+
+ delete fb;
+}
diff --git a/utils/meson.build b/utils/meson.build
new file mode 100644
index 0000000..54021d3
--- /dev/null
+++ b/utils/meson.build
@@ -0,0 +1,26 @@
+if not get_option('utils')
+ utils_enabled = false
+ subdir_done()
+endif
+
+if not get_option('libutils')
+ utils_enabled = false
+ subdir_done()
+endif
+
+utils_enabled = true
+
+common_deps = [ libkmsxx_dep, libkmsxxutil_dep ]
+
+libevdev_dep = dependency('libevdev', required : false)
+
+executable('kmstest', 'kmstest.cpp', dependencies : [ common_deps ], install : true)
+executable('kmsview', 'kmsview.cpp', dependencies : [ common_deps ], install : false)
+executable('kmsprint', 'kmsprint.cpp', dependencies : [ common_deps ], install : true)
+executable('fbtest', 'fbtest.cpp', dependencies : [ common_deps ], install : true)
+executable('kmscapture', 'kmscapture.cpp', dependencies : [ common_deps ], install : false)
+executable('kmsblank', 'kmsblank.cpp', dependencies : [ common_deps ], install : true)
+
+if libevdev_dep.found()
+ executable('kmstouch', 'kmstouch.cpp', dependencies : [ common_deps, libevdev_dep ], install : false)
+endif