├── .gitignore ├── Makefile ├── README.md ├── autoformat.sh ├── backend_flicker.c ├── backend_opencv.cpp ├── backend_v4l.c ├── backend_xcb.c ├── common.c ├── frontend_fb.c ├── frontend_qt.cpp ├── frontend_term.c ├── frontend_wayland.c ├── frontend_wayland_gbm.c ├── frontend_wayland_gl.c ├── frontend_xcb.c └── interface.h /.gitignore: -------------------------------------------------------------------------------- 1 | obj 2 | latency_cv_wayland 3 | latency_cv_qt 4 | latency_cv_xcb 5 | latency_cv_fb 6 | latency_cv_term 7 | latency_xcb_term 8 | latency_v4l_wayland 9 | latency_v4l_wayland_gl 10 | latency_v4l_wayland_gbm 11 | latency_v4l_xcb 12 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # yes, overkill for setup 2 | 3 | # Note: $(shell pkg-config --libs opencv4) gives *ALL* the libraries 4 | cv_libs := -lopencv_core -lopencv_imgproc -lopencv_videoio 5 | cv_cflags := $(shell pkg-config --cflags opencv4) 6 | qt_libs := $(shell pkg-config --libs Qt5Widgets) 7 | qt_cflags := $(shell pkg-config --cflags Qt5Widgets) 8 | xcb_libs := $(shell pkg-config --libs xcb) 9 | xcb_cflags := $(shell pkg-config --cflags xcb) 10 | gl_libs := $(shell pkg-config --libs opengl egl wayland-egl) 11 | gl_cflags := $(shell pkg-config --cflags opengl egl wayland-egl) 12 | gbm_libs := $(shell pkg-config --libs gbm) 13 | gbm_cflags := $(shell pkg-config --cflags gbm) 14 | way_libs := $(shell pkg-config --libs wayland-client) -lrt 15 | way_cflags := $(shell pkg-config --cflags wayland-client) 16 | wayproto_dir := $(shell pkg-config --variable=pkgdatadir wayland-protocols) 17 | 18 | flags=-O3 -ggdb3 -D_DEFAULT_SOURCE 19 | 20 | all: latency_cv_xcb latency_cv_wayland latency_v4l_wayland_gl latency_v4l_wayland_gbm latency_v4l_wayland latency_v4l_xcb latency_cv_qt latency_cv_fb latency_cv_term latency_xcb_term 21 | 22 | latency_cv_xcb: obj/frontend_xcb.o obj/backend_cv.o obj/common.o 23 | g++ $(flags) $(cv_libs) $(xcb_libs) -o latency_cv_xcb obj/frontend_xcb.o obj/backend_cv.o obj/common.o 24 | 25 | latency_cv_wayland: obj/frontend_wayland.o obj/backend_cv.o obj/xdg-shell-stable-protocol.o obj/common.o 26 | g++ $(flags) $(cv_libs) $(way_libs) -o latency_cv_wayland obj/frontend_wayland.o obj/xdg-shell-stable-protocol.o obj/backend_cv.o obj/common.o 27 | 28 | latency_cv_qt: obj/frontend_qt.o obj/backend_cv.o obj/common.o 29 | g++ $(flags) $(cv_libs) $(qt_libs) -o latency_cv_qt obj/frontend_qt.o obj/backend_cv.o obj/common.o 30 | 31 | latency_cv_fb: obj/frontend_fb.o obj/backend_cv.o obj/common.o 32 | g++ $(flags) $(cv_libs) -o latency_cv_fb obj/frontend_fb.o obj/backend_cv.o obj/common.o 33 | 34 | latency_cv_term: obj/frontend_term.o obj/backend_cv.o obj/common.o 35 | g++ $(flags) $(cv_libs) -o latency_cv_term obj/frontend_term.o obj/backend_cv.o obj/common.o 36 | 37 | latency_v4l_wayland: obj/frontend_wayland.o obj/backend_v4l.o obj/xdg-shell-stable-protocol.o obj/common.o 38 | g++ $(flags) $(way_libs) -o latency_v4l_wayland obj/frontend_wayland.o obj/xdg-shell-stable-protocol.o obj/backend_v4l.o obj/common.o 39 | 40 | latency_v4l_wayland_gl: obj/frontend_wayland_gl.o obj/backend_v4l.o obj/xdg-shell-stable-protocol.o obj/common.o 41 | g++ $(flags) $(way_libs) $(gl_libs) -o latency_v4l_wayland_gl obj/frontend_wayland_gl.o obj/xdg-shell-stable-protocol.o obj/backend_v4l.o obj/common.o 42 | 43 | latency_v4l_wayland_gbm: obj/frontend_wayland_gbm.o obj/backend_v4l.o obj/xdg-shell-stable-protocol.o obj/linux-dmabuf-unstable-v1-protocol.o obj/common.o 44 | g++ $(flags) $(way_libs) $(gbm_libs) -o latency_v4l_wayland_gbm obj/frontend_wayland_gbm.o obj/xdg-shell-stable-protocol.o obj/linux-dmabuf-unstable-v1-protocol.o obj/backend_v4l.o obj/common.o 45 | 46 | latency_v4l_xcb: obj/frontend_xcb.o obj/backend_v4l.o obj/xdg-shell-stable-protocol.o obj/common.o 47 | g++ $(flags) $(xcb_libs) -o latency_v4l_xcb obj/frontend_xcb.o obj/backend_v4l.o obj/common.o 48 | 49 | latency_flicker_term: obj/frontend_term.o obj/backend_flicker.o 50 | g++ $(flags) -o latency_flicker_term obj/frontend_term.o obj/backend_flicker.o 51 | 52 | latency_xcb_term: obj/frontend_term.o obj/backend_xcb.o 53 | g++ $(flags) $(xcb_libs) -o latency_xcb_term obj/frontend_term.o obj/backend_xcb.o 54 | 55 | # Object files, in C (or C++ as libraries require) 56 | obj/backend_cv.o: obj/.sentinel backend_opencv.cpp 57 | g++ $(flags) -c -fPIC $(cv_cflags) -o obj/backend_cv.o backend_opencv.cpp 58 | obj/backend_flicker.o: obj/.sentinel backend_flicker.c 59 | gcc $(flags) -c -fPIC -o obj/backend_flicker.o backend_flicker.c 60 | obj/backend_xcb.o: obj/.sentinel backend_xcb.c 61 | gcc $(flags) -c -fPIC $(xcb_cflags) -o obj/backend_xcb.o backend_xcb.c 62 | obj/backend_v4l.o: obj/.sentinel backend_v4l.c 63 | gcc $(flags) -c -fPIC -o obj/backend_v4l.o backend_v4l.c 64 | 65 | obj/frontend_qt.o: obj/.sentinel frontend_qt.cpp obj/frontend_qt.moc 66 | g++ $(flags) -c -fPIC $(qt_cflags) -o obj/frontend_qt.o frontend_qt.cpp 67 | obj/frontend_qt.moc: obj/.sentinel frontend_qt.cpp 68 | moc $(qt_cflags) frontend_qt.cpp -o $@ 69 | 70 | obj/frontend_xcb.o: obj/.sentinel frontend_xcb.c 71 | gcc $(flags) -c -fPIC $(xcb_cflags) -o obj/frontend_xcb.o frontend_xcb.c 72 | 73 | obj/frontend_wayland.o: obj/.sentinel frontend_wayland.c obj/xdg-shell-stable-client-protocol.h 74 | gcc $(flags) -c -fPIC $(way_cflags) -o obj/frontend_wayland.o frontend_wayland.c 75 | obj/frontend_wayland_gl.o: obj/.sentinel frontend_wayland_gl.c obj/xdg-shell-stable-client-protocol.h 76 | gcc $(flags) -c -fPIC $(way_cflags) $(gl_cflags) -o obj/frontend_wayland_gl.o frontend_wayland_gl.c 77 | obj/frontend_wayland_gbm.o: obj/.sentinel frontend_wayland_gbm.c obj/xdg-shell-stable-client-protocol.h obj/linux-dmabuf-unstable-v1-client-protocol.h 78 | gcc $(flags) -c -fPIC $(way_cflags) $(gbm_cflags) -o obj/frontend_wayland_gbm.o frontend_wayland_gbm.c 79 | obj/xdg-shell-stable-client-protocol.h: obj/.sentinel 80 | wayland-scanner client-header $(wayproto_dir)/stable/xdg-shell/xdg-shell.xml obj/xdg-shell-stable-client-protocol.h 81 | obj/xdg-shell-stable-protocol.c: obj/.sentinel 82 | wayland-scanner private-code $(wayproto_dir)/stable/xdg-shell/xdg-shell.xml obj/xdg-shell-stable-protocol.c 83 | obj/xdg-shell-stable-protocol.o: obj/.sentinel obj/xdg-shell-stable-protocol.c 84 | gcc $(flags) -c -fPIC $(way_cflags) -o obj/xdg-shell-stable-protocol.o obj/xdg-shell-stable-protocol.c 85 | obj/linux-dmabuf-unstable-v1-client-protocol.h: obj/.sentinel 86 | wayland-scanner client-header $(wayproto_dir)/unstable/linux-dmabuf/linux-dmabuf-unstable-v1.xml obj/linux-dmabuf-unstable-v1-client-protocol.h 87 | obj/linux-dmabuf-unstable-v1-protocol.c: obj/.sentinel 88 | wayland-scanner private-code $(wayproto_dir)/unstable/linux-dmabuf/linux-dmabuf-unstable-v1.xml obj/linux-dmabuf-unstable-v1-protocol.c 89 | obj/linux-dmabuf-unstable-v1-protocol.o: obj/.sentinel obj/linux-dmabuf-unstable-v1-protocol.c 90 | gcc $(flags) -c -fPIC $(way_cflags) -o obj/linux-dmabuf-unstable-v1-protocol.o obj/linux-dmabuf-unstable-v1-protocol.c 91 | 92 | obj/frontend_fb.o: obj/.sentinel frontend_fb.c 93 | gcc $(flags) -c -fPIC -o obj/frontend_fb.o frontend_fb.c 94 | 95 | obj/frontend_term.o: obj/.sentinel frontend_term.c 96 | gcc $(flags) -c -fPIC -o obj/frontend_term.o frontend_term.c 97 | 98 | obj/common.o: obj/.sentinel common.c 99 | gcc $(flags) -c -fPIC -o obj/common.o common.c 100 | 101 | # Misc 102 | 103 | obj/.sentinel: 104 | mkdir -p obj 105 | touch obj/.sentinel 106 | 107 | clean: 108 | rm -f obj/*.h obj/*.c obj/*.o obj/*.moc latency_cv_xcb latency_cv_wayland latency_cv_qt latency_cv_fb latency_cv_term latency_flicker_term latency_xcb_term latency_v4l_wayland_gl latency_v4l_wayland_gbm latency_v4l_wayland latency_v4l_xcb 109 | 110 | .PHONY: all clean 111 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Latencytool 2 | 3 | This is/will be a set of programs that can be used to measure the round-trip 4 | latency between application, screen, and camera. 5 | 6 | To use, attach USB camera, and run `latency_cv_qt N` where N is the 7 | corresponding `/dev/videoN` device. This should open a window that is either 8 | white or black; moving the camera directly in front of the window should start 9 | rapidly switching the window color to the opposite of whatever the camera sees. 10 | stdout will eventually print the average time for a color switch operation -- 11 | this is the round-trip latency. Tuning constants in `backend_opencv.cpp` may be 12 | helpful. The V4L backend, on the other hand, will not work correctly unless 13 | you adjust constants to match your camera. 14 | 15 | It is recommended to test with as small a window as feasible, both to reduce 16 | computational overhead, and because staring at large blinking lights of 10-12 Hz 17 | for the several minutes needed to get a stable measurement will give you a 18 | headache. 19 | 20 | # Status 21 | 22 | An OpenCV and a V4L backend have been written. Frontends are available for 23 | terminal output, xcb, Wayland (Standard, OpenGL, GBM variants), /dev/fb0, and 24 | Qt. 25 | 26 | # Uses 27 | 28 | Given a camera with a reasonably high framerate and known latency, one can 29 | estimate the total delay between application rendering and the time the screen 30 | update completes. 31 | 32 | For example, with a PS3 Eye at 187 Hz, and a 60 Hz laptop monitor from 2015, we 33 | can compare the amounts of lag introduced by various display server 34 | combinations. With `latency_cv_qt`: 35 | 36 | * X11, no compositing, small window: 25ms average round trip 37 | * X11, no compositing, large window: 30ms average round trip 38 | * kwin_wayland, small window: 45ms average round trip 39 | * sway, small window: 47ms average round trip 40 | * sway, small window, nested under X11: 43ms average round trip 41 | * weston, small window: 41ms average round trip 42 | 43 | With `latency_cv_xcb` on X11 without compositing, the average round trip time 44 | is reduced to 24ms for small windows, and 25ms for large windows. It *also* 45 | reduces the average round trip time for sway to 44ms, and weston to 41ms. 46 | 47 | With `latency_cv_wayland` on sway, as a small window, 44ms average round 48 | trip time is observed. 49 | 50 | With `latency_cv_fb`, 25ms average round trip time is observed. 51 | 52 | # Installation 53 | 54 | Current requirements are: 55 | 56 | * GNU make 57 | * pkgconfig 58 | * opencv (tested with 4.0.1) 59 | * Qt5 (tested with 5.12) 60 | * libxcb (tested with 1.13.1) 61 | * wayland (tested with 1.16.0) 62 | * wayland-protocols (tested with 1.17.1) 63 | * EGL (tested with 1.5) 64 | * OpenGL (any version) 65 | * gbm (tested with Mesa 21.2.1) 66 | * V4L (as preferred opencv backend) 67 | * Linux (for the framebuffer frontend, and the V4L backend) 68 | 69 | To compile, run `make`. 70 | -------------------------------------------------------------------------------- /autoformat.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Formatting..." 4 | root=`dirname ${BASH_SOURCE[0]}` 5 | echo $root 6 | time clang-format -style="{BasedOnStyle: llvm, IndentWidth: 4}" -i $root/*.c $root/*.h $root/*.cpp 7 | echo "Done" 8 | -------------------------------------------------------------------------------- /backend_flicker.c: -------------------------------------------------------------------------------- 1 | #include "interface.h" 2 | 3 | #include 4 | #include 5 | 6 | struct state { 7 | bool show_dark; 8 | }; 9 | 10 | void *setup_backend(int camera) { return calloc(1, sizeof(struct state)); } 11 | 12 | enum WhatToDo update_backend(void *state) { 13 | struct state *s = (struct state *)state; 14 | s->show_dark = !s->show_dark; 15 | return s->show_dark ? DisplayDark : DisplayLight; 16 | } 17 | 18 | void cleanup_backend(void *state) { free(state); } 19 | -------------------------------------------------------------------------------- /backend_opencv.cpp: -------------------------------------------------------------------------------- 1 | #include "opencv2/opencv.hpp" 2 | 3 | #include "interface.h" 4 | #include 5 | #include 6 | #include 7 | 8 | // in [0,1], i.e, what brightness level is the light/dark cutoff 9 | #define THRESHOLD 0.3 10 | 11 | struct state { 12 | // Readout 13 | cv::VideoCapture *cap; 14 | cv::Mat graylevel; 15 | cv::Mat bgrframe; 16 | 17 | enum WhatToDo output_state; 18 | struct analysis control; 19 | }; 20 | 21 | static void *isetup(int camera) { 22 | struct state *s = new struct state; 23 | s->cap = new cv::VideoCapture(camera, cv::CAP_V4L); 24 | if (!s->cap->isOpened()) { 25 | delete s->cap; 26 | delete s; 27 | return NULL; 28 | } 29 | 30 | fprintf(stderr, "Camera #%d loaded with backend '%s'\n", camera, 31 | s->cap->getBackendName().c_str()); 32 | 33 | // V4L will typically select for the combination closest to this. 34 | // This gives us the fastest / lowest resolution result. 35 | bool w1 = s->cap->set(cv::CAP_PROP_FRAME_WIDTH, 1.); 36 | bool w2 = s->cap->set(cv::CAP_PROP_FRAME_HEIGHT, 1.); 37 | bool w3 = s->cap->set(cv::CAP_PROP_FPS, 1000.); 38 | fprintf(stderr, "Can set? Width %c Height %c Fps %c\n", w1 ? 'Y' : 'n', 39 | w2 ? 'Y' : 'n', w3 ? 'Y' : 'n'); 40 | 41 | // We disable all automatic correction effects. While these may be helpful 42 | // getting a decent picture, alternating black-and-white images will only 43 | // confuse the predictor 44 | bool w4 = s->cap->set(cv::CAP_PROP_AUTO_EXPOSURE, 0.); 45 | bool w5 = s->cap->set(cv::CAP_PROP_AUTO_WB, 0.); 46 | fprintf(stderr, "Can set? Auto exposure %c Auto whitebalance %c\n", 47 | w4 ? 'Y' : 'n', w5 ? 'Y' : 'n'); 48 | fprintf(stderr, 49 | "Be sure to check `v4l2-ctl -d %d -l` and disable auto gain\n", 50 | camera); 51 | 52 | double fps = s->cap->get(cv::CAP_PROP_FPS); 53 | double width = s->cap->get(cv::CAP_PROP_FRAME_WIDTH); 54 | double height = s->cap->get(cv::CAP_PROP_FRAME_HEIGHT); 55 | double autoexp = s->cap->get(cv::CAP_PROP_AUTO_EXPOSURE); 56 | double autowb = s->cap->get(cv::CAP_PROP_AUTO_WB); 57 | fprintf( 58 | stderr, 59 | "nominal fps=%.0f width=%.0f height=%.0f autoexp=%.0f autowb=%.0f\n", 60 | fps, width, height, autoexp, autowb); 61 | 62 | if (setup_analysis(&s->control) < 0) { 63 | delete s->cap; 64 | delete s; 65 | return NULL; 66 | } 67 | s->output_state = DisplayLight; 68 | return s; 69 | } 70 | 71 | extern "C" { 72 | 73 | void *setup_backend(int camera) { return isetup(camera); } 74 | 75 | enum WhatToDo update_backend(void *state) { 76 | struct state *s = (struct state *)state; 77 | // We return the opposite of the current camera state, and record/print 78 | // brightness transitions 79 | bool success = s->cap->read(s->bgrframe); 80 | if (!success) { 81 | return s->output_state; 82 | } 83 | 84 | // Record frame capture time *before* postprocessing, as though it 85 | // had zero cost. 86 | struct timespec capture_time; 87 | clock_gettime(CLOCK_MONOTONIC, &capture_time); 88 | 89 | cv::cvtColor(s->bgrframe, s->graylevel, cv::COLOR_BGR2GRAY); 90 | double level = cv::mean(s->graylevel)[0] / 255.0; 91 | 92 | s->output_state = 93 | update_analysis(&s->control, capture_time, level, THRESHOLD); 94 | return s->output_state; 95 | } 96 | 97 | void cleanup_backend(void *state) { 98 | if (state) { 99 | struct state *s = (struct state *)state; 100 | cleanup_analysis(&s->control); 101 | 102 | delete s->cap; 103 | delete s; 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /backend_v4l.c: -------------------------------------------------------------------------------- 1 | #include "interface.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | #include 19 | 20 | // Parameters for PS3 Eye 21 | #define CAMERA_WIDTH 320 22 | #define CAMERA_HEIGHT 240 23 | #define CAMERA_PIXELFORMAT V4L2_PIX_FMT_SGBRG8 24 | #define CAMERA_FIELD V4L2_FIELD_NONE 25 | #define CAMERA_TIMEPERFRAME_NUM 1000 26 | #define CAMERA_TIMEPERFRAME_DENOM 187000 27 | #define THRESHOLD 0.3 28 | 29 | #define NUM_BUFS 5 30 | 31 | struct buf { 32 | void *data; 33 | size_t len; 34 | }; 35 | 36 | struct state { 37 | int fd; 38 | struct buf bufs[NUM_BUFS]; 39 | 40 | enum WhatToDo output_state; 41 | struct analysis control; 42 | }; 43 | 44 | static int ioctl_loop(int fd, unsigned long int req, void *arg) { 45 | // In case frontend has weird signal settings, check for EINTR 46 | while (1) { 47 | int r = ioctl(fd, req, arg); 48 | if (r == -1 && errno == EINTR) { 49 | continue; 50 | } 51 | return r; 52 | } 53 | } 54 | 55 | void *setup_backend(int camera) { 56 | struct state *s = calloc(1, sizeof(struct state)); 57 | 58 | char devname[50]; 59 | sprintf(devname, "/dev/video%d", camera); 60 | 61 | s->fd = open(devname, O_RDWR | O_NONBLOCK, 0); 62 | if (s->fd == -1) { 63 | fprintf(stderr, "Failed to open fd at %s: %s\n", devname, 64 | strerror(errno)); 65 | goto fail_free; 66 | } 67 | 68 | struct v4l2_capability cap; 69 | if (ioctl_loop(s->fd, VIDIOC_QUERYCAP, &cap) < 0) { 70 | fprintf(stderr, "Not a video device: %s\n", strerror(errno)); 71 | goto fail_vfd; 72 | } 73 | 74 | uint32_t needed_caps = V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING; 75 | if ((cap.capabilities & needed_caps) != needed_caps) { 76 | fprintf(stderr, "Lacks needed capabilities: %x\n", cap.capabilities); 77 | goto fail_vfd; 78 | } 79 | 80 | struct v4l2_streamparm sparm; 81 | memset(&sparm, 0, sizeof(sparm)); 82 | sparm.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; 83 | sparm.parm.capture.timeperframe.numerator = CAMERA_TIMEPERFRAME_NUM; 84 | sparm.parm.capture.timeperframe.denominator = CAMERA_TIMEPERFRAME_DENOM; 85 | if (ioctl_loop(s->fd, VIDIOC_S_PARM, &sparm) < 0) { 86 | fprintf(stderr, "Failed to set FPS: %s\n", strerror(errno)); 87 | goto fail_vfd; 88 | } 89 | if (ioctl_loop(s->fd, VIDIOC_G_PARM, &sparm) < 0) { 90 | fprintf(stderr, "Failed to get FPS: %s\n", strerror(errno)); 91 | goto fail_vfd; 92 | } 93 | fprintf(stderr, "Camera FPS is: %f\n", 94 | sparm.parm.capture.timeperframe.denominator / 95 | (double)sparm.parm.capture.timeperframe.numerator); 96 | 97 | struct v4l2_format fmt; 98 | memset(&fmt, 0, sizeof(fmt)); 99 | fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; 100 | fmt.fmt.pix.width = CAMERA_WIDTH; 101 | fmt.fmt.pix.height = CAMERA_HEIGHT; 102 | fmt.fmt.pix.pixelformat = CAMERA_PIXELFORMAT; 103 | fmt.fmt.pix.field = CAMERA_FIELD; 104 | if (ioctl_loop(s->fd, VIDIOC_S_FMT, &fmt) < 0) { 105 | fprintf(stderr, "Failed to set video format: %s\n", strerror(errno)); 106 | goto fail_vfd; 107 | } 108 | fprintf(stderr, 109 | "width=%d/%d height=%d/%d pixelfmt=%x/%x field=%d/%d colorspace=%d " 110 | "xfer_func=%d\n", 111 | fmt.fmt.pix.width, CAMERA_WIDTH, fmt.fmt.pix.height, CAMERA_HEIGHT, 112 | fmt.fmt.pix.pixelformat, CAMERA_PIXELFORMAT, fmt.fmt.pix.field, 113 | CAMERA_FIELD, fmt.fmt.pix.colorspace, fmt.fmt.pix.xfer_func); 114 | 115 | if (fmt.fmt.pix.width != CAMERA_WIDTH || 116 | fmt.fmt.pix.height != CAMERA_HEIGHT || 117 | fmt.fmt.pix.field != CAMERA_FIELD) { 118 | fprintf(stderr, "Video does not accept the hard-coded dimensions: %s\n", 119 | strerror(errno)); 120 | goto fail_vfd; 121 | } 122 | 123 | struct v4l2_requestbuffers reqbufs; 124 | memset(&reqbufs, 0, sizeof(reqbufs)); 125 | reqbufs.count = NUM_BUFS; 126 | reqbufs.memory = V4L2_MEMORY_MMAP; 127 | reqbufs.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; 128 | 129 | if (ioctl_loop(s->fd, VIDIOC_REQBUFS, &reqbufs) < 0) { 130 | fprintf(stderr, "Failed to request buffers: %s\n", strerror(errno)); 131 | goto fail_vfd; 132 | } 133 | 134 | for (int i = 0; i < NUM_BUFS; i++) { 135 | struct v4l2_buffer buf; 136 | memset(&buf, 0, sizeof(buf)); 137 | buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; 138 | buf.memory = V4L2_MEMORY_MMAP; 139 | buf.index = i; 140 | 141 | if (ioctl_loop(s->fd, VIDIOC_QUERYBUF, &buf) < 0) { 142 | fprintf(stderr, "Failed to query buffers info %d/%d: %s\n", i, 143 | NUM_BUFS, strerror(errno)); 144 | goto fail_bufs; 145 | } 146 | if (buf.length == 0) { 147 | fprintf(stderr, "Zero buffer length\n"); 148 | goto fail_bufs; 149 | } 150 | 151 | s->bufs[i].len = buf.length; 152 | s->bufs[i].data = mmap(NULL, buf.length, PROT_READ | PROT_WRITE, 153 | MAP_SHARED, s->fd, buf.m.offset); 154 | if (s->bufs[i].data == MAP_FAILED) { 155 | fprintf(stderr, "Failed to map buffer\n"); 156 | goto fail_bufs; 157 | } 158 | 159 | if (ioctl_loop(s->fd, VIDIOC_QBUF, &buf) < 0) { 160 | fprintf(stderr, "Failed to queue buffer: %s\n", strerror(errno)); 161 | goto fail_bufs; 162 | } 163 | } 164 | 165 | enum v4l2_buf_type buftype; 166 | buftype = V4L2_BUF_TYPE_VIDEO_CAPTURE; 167 | if (ioctl_loop(s->fd, VIDIOC_STREAMON, &buftype) < 0) { 168 | fprintf(stderr, "Failed to start stream1: %s\n", strerror(errno)); 169 | goto fail_bufs; 170 | } 171 | 172 | s->output_state = DisplayLight; 173 | if (setup_analysis(&s->control) < 0) { 174 | goto fail_bufs; 175 | } 176 | 177 | fprintf(stderr, "All set up\n"); 178 | return s; 179 | fail_bufs: 180 | for (int i = 0; i < NUM_BUFS; i++) { 181 | if (s->bufs[i].len) { 182 | munmap(s->bufs[i].data, s->bufs[i].len); 183 | } 184 | } 185 | fail_vfd: 186 | close(s->fd); 187 | fail_free: 188 | free(s); 189 | return NULL; 190 | } 191 | 192 | enum WhatToDo update_backend(void *state) { 193 | struct state *s = (struct state *)state; 194 | 195 | struct pollfd pfd; 196 | pfd.fd = s->fd; 197 | pfd.events = POLLIN; 198 | int p = poll(&pfd, 1, 1); // 1 msec max timeout 199 | if (p < 0) { 200 | fprintf(stderr, "Poll failed: %s\n", strerror(errno)); 201 | goto end; 202 | } else if (p == 0) { 203 | goto end; 204 | } 205 | 206 | struct v4l2_buffer buf; 207 | memset(&buf, 0, sizeof(buf)); 208 | buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; 209 | buf.memory = V4L2_MEMORY_MMAP; 210 | if (ioctl_loop(s->fd, VIDIOC_DQBUF, &buf) < 0) { 211 | if (errno == EAGAIN) { 212 | goto end; 213 | } else { 214 | fprintf(stderr, "Dequeue failed: %s\n", strerror(errno)); 215 | goto end; 216 | } 217 | } 218 | 219 | struct timespec captime; 220 | clock_gettime(CLOCK_MONOTONIC, &captime); 221 | 222 | int length = s->bufs[buf.index].len; 223 | uint8_t *data = (uint8_t *)s->bufs[buf.index].data; 224 | 225 | // TODO: does interpreting the colors & Bayer layout make sense, 226 | // or should the fact that we just feed light/dark inputs mean that 227 | // we can safely average pixel values and get 'good-enough' results? 228 | uint64_t net_val = 0; 229 | for (int i = 0; i < length; i++) { 230 | net_val += data[i]; 231 | } 232 | double avg_val = net_val / (double)(length) / 255.0; 233 | 234 | if (ioctl_loop(s->fd, VIDIOC_QBUF, &buf) < 0) { 235 | fprintf(stderr, "Requeue failed: %s\n", strerror(errno)); 236 | } 237 | 238 | s->output_state = update_analysis(&s->control, captime, avg_val, THRESHOLD); 239 | end: 240 | return s->output_state; 241 | } 242 | 243 | void cleanup_backend(void *state) { 244 | struct state *s = state; 245 | 246 | enum v4l2_buf_type type; 247 | type = V4L2_BUF_TYPE_VIDEO_CAPTURE; 248 | ioctl_loop(s->fd, VIDIOC_STREAMOFF, &type); 249 | for (int i = 0; i < NUM_BUFS; i++) { 250 | if (s->bufs[i].len) { 251 | munmap(s->bufs[i].data, s->bufs[i].len); 252 | } 253 | } 254 | close(s->fd); 255 | cleanup_analysis(&s->control); 256 | 257 | free(s); 258 | } 259 | -------------------------------------------------------------------------------- /backend_xcb.c: -------------------------------------------------------------------------------- 1 | #include "interface.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #define TARGET_PIXEL_X 100 10 | #define TARGET_PIXEL_Y 100 11 | 12 | struct state { 13 | xcb_connection_t *conn; 14 | xcb_drawable_t root; 15 | uint32_t black_pixel; 16 | uint32_t white_pixel; 17 | bool was_dark; 18 | int location; 19 | struct timespec last_time; 20 | }; 21 | 22 | void *setup_backend(int camera) { 23 | struct state *s = calloc(1, sizeof(struct state)); 24 | s->conn = xcb_connect(NULL, NULL); 25 | s->location = camera; 26 | fprintf(stdout, "Backend: watching pixel at (%d, %d)\n", s->location, 27 | s->location); 28 | 29 | xcb_screen_t *screen = 30 | xcb_setup_roots_iterator(xcb_get_setup(s->conn)).data; 31 | s->root = screen->root; 32 | 33 | s->black_pixel = screen->black_pixel; 34 | s->white_pixel = screen->white_pixel; 35 | 36 | xcb_flush(s->conn); 37 | 38 | s->was_dark = false; 39 | clock_gettime(CLOCK_MONOTONIC, &s->last_time); 40 | 41 | setvbuf(stdout, NULL, _IONBF, 0); 42 | return s; 43 | } 44 | 45 | enum WhatToDo update_backend(void *state) { 46 | struct state *s = (struct state *)state; 47 | 48 | xcb_get_image_cookie_t cookie = 49 | xcb_get_image(s->conn, XCB_IMAGE_FORMAT_Z_PIXMAP, s->root, s->location, 50 | s->location, 1, 1, 0xffffffff); 51 | xcb_get_image_reply_t *image_reply = 52 | xcb_get_image_reply(s->conn, cookie, NULL); 53 | if (!image_reply) { 54 | printf("Failed to get image reply.\n"); 55 | 56 | return DisplayDark; 57 | } 58 | uint8_t *data = xcb_get_image_data(image_reply); 59 | uint32_t length = xcb_get_image_data_length(image_reply); 60 | free(image_reply); 61 | if (length != 4) { 62 | printf("Response has unexpected shape\n"); 63 | return DisplayDark; 64 | } 65 | uint8_t channels[4] = {data[0], data[1], data[2], data[3]}; 66 | 67 | bool is_dark = ((uint32_t)channels[1] + (uint32_t)channels[2]) < 256; 68 | if (is_dark != s->was_dark) { 69 | s->was_dark = is_dark; 70 | struct timespec tp; 71 | clock_gettime(CLOCK_MONOTONIC, &tp); 72 | 73 | double offset = 1.0 * (tp.tv_sec - s->last_time.tv_sec) + 74 | 1e-9 * (tp.tv_nsec - s->last_time.tv_nsec); 75 | fprintf(stdout, "%s %10.5fms\n", is_dark ? "L->D" : "D->L", 76 | 1e3 * offset); 77 | 78 | // On transition, introduce a random delay, so that we are not 79 | // phase locked by accident 80 | struct timespec delay; 81 | delay.tv_sec = 0; 82 | uint32_t r = rand(); 83 | delay.tv_nsec = 40000000 + r % 40000000; 84 | nanosleep(&delay, NULL); 85 | 86 | clock_gettime(CLOCK_MONOTONIC, &s->last_time); 87 | } 88 | 89 | return is_dark ? DisplayLight : DisplayDark; 90 | } 91 | 92 | void cleanup_backend(void *state) { 93 | struct state *s = state; 94 | xcb_disconnect(s->conn); 95 | free(s); 96 | } 97 | -------------------------------------------------------------------------------- /common.c: -------------------------------------------------------------------------------- 1 | #include "interface.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | // Tradeoff between statistical convergence and minimum time 8 | #define FIR_LENGTH 100 9 | /* Wait long enough for the brightness to stabilize. */ 10 | #define HOLD_MIN_TIME 0.040 11 | #define HOLD_MAX_TIME 0.100 12 | 13 | static double mean(double m0, double m1) { return m0 > 0. ? m1 / m0 : -1.; } 14 | 15 | static double stdev(double m0, double m1, double m2) { 16 | return m0 > 2. ? sqrt((m2 - m1 * m1 / m0) / (m0 - 1.)) : -1.; 17 | } 18 | 19 | int setup_analysis(struct analysis *a) { 20 | a->fir = calloc(FIR_LENGTH, sizeof(double)); 21 | if (!a->fir) { 22 | fprintf(stderr, "Failed to allocate ring buffer\n"); 23 | return -1; 24 | } 25 | a->nframes = 0; 26 | a->fir_head = 0; 27 | char *logpath = getenv("LATENCYTOOL_LOG"); 28 | if (logpath) { 29 | a->log = fopen(logpath, "w"); 30 | } else { 31 | a->log = NULL; 32 | } 33 | 34 | clock_gettime(CLOCK_MONOTONIC, &a->setup_time); 35 | 36 | a->want_switch = false; 37 | // State initialization is arbitrary 38 | a->current_camera_level = 1.0; 39 | a->showing_dark = false; 40 | a->capture_time.tv_sec = 0; 41 | a->capture_time.tv_nsec = 0; 42 | 43 | return 0; 44 | } 45 | void cleanup_analysis(struct analysis *a) { 46 | free(a->fir); 47 | if (a->log) { 48 | fclose(a->log); 49 | } 50 | } 51 | 52 | static void update_fir(struct analysis *a, double delay, bool now_is_dark) { 53 | int idx = a->nframes % FIR_LENGTH; 54 | a->nframes++; 55 | a->fir[idx] = (now_is_dark ? delay : -delay) * 1e3; 56 | 57 | double n_ltd = 0., sum_ltd = 0., sum_ltd2 = 0.; 58 | double n_dtl = 0., sum_dtl = 0., sum_dtl2 = 0.; 59 | double min_tot = 1e100; 60 | double max_tot = -1e100; 61 | for (int i = 0; i < fmin(a->nframes - 2, FIR_LENGTH); i++) { 62 | double idel = a->fir[(FIR_LENGTH + a->nframes - i - 1) % FIR_LENGTH]; 63 | if (idel > 0) { 64 | n_ltd += 1.; 65 | sum_ltd += idel; 66 | sum_ltd2 += idel * idel; 67 | } else { 68 | n_dtl += 1.; 69 | sum_dtl += -idel; 70 | sum_dtl2 += idel * idel; 71 | } 72 | max_tot = fmax(max_tot, fabs(idel)); 73 | min_tot = fmin(min_tot, fabs(idel)); 74 | } 75 | double n_tot = n_ltd + n_dtl, sum_tot = sum_ltd + sum_dtl, 76 | sum_tot2 = sum_ltd2 + sum_dtl2; 77 | 78 | double mean_ltd = mean(n_ltd, sum_ltd); 79 | double mean_dtl = mean(n_dtl, sum_dtl); 80 | double mean_tot = mean(n_tot, sum_tot); 81 | double std_ltd = stdev(n_ltd, sum_ltd, sum_ltd2); 82 | double std_dtl = stdev(n_dtl, sum_dtl, sum_dtl2); 83 | double std_tot = stdev(n_tot, sum_tot, sum_tot2); 84 | fprintf(stdout, 85 | "Net: (%5.2f < %5.2f±%4.2f < %5.2f)ms L->D: (%5.2f±%4.2f)ms; " 86 | "D->L: (%5.2f±%4.2f)ms\n", 87 | min_tot, mean_tot, std_tot, max_tot, mean_ltd, std_ltd, mean_dtl, 88 | std_dtl); 89 | fflush(stdout); 90 | } 91 | 92 | enum WhatToDo update_analysis(struct analysis *a, struct timespec meas_time, 93 | double meas_level, double threshold) { 94 | struct timespec last_capture_time = a->capture_time; 95 | float last_camera_level = a->current_camera_level; 96 | a->current_camera_level = meas_level; 97 | a->capture_time = meas_time; 98 | 99 | bool was_dark = last_camera_level <= threshold; 100 | bool is_dark = a->current_camera_level <= threshold; 101 | 102 | struct timespec transition_time = last_capture_time; 103 | if (was_dark != is_dark) { 104 | // With a reasonably fast camera, the screen color change 105 | // curve can be reasonably well captured by linear interpolation. 106 | double t = (threshold - last_camera_level) / 107 | (a->current_camera_level - last_camera_level); 108 | int64_t nsec_gap = get_delta_nsec(last_capture_time, a->capture_time); 109 | int64_t step = (int64_t)(t * nsec_gap); 110 | transition_time = advance_time(last_capture_time, step); 111 | 112 | // Delay computed relative to old switch time 113 | double delay = 114 | get_delta_nsec(a->next_switch_time, transition_time) * 1e-9; 115 | 116 | // Randomly pick the amount of time to wait after the transition, 117 | // to avoid accidentally synchronizing with something. 118 | double hold_time = HOLD_MIN_TIME + (HOLD_MAX_TIME - HOLD_MIN_TIME) * 119 | (rand() / (double)RAND_MAX); 120 | a->next_switch_time = advance_time(transition_time, hold_time * 1e9); 121 | a->want_switch = true; 122 | 123 | // Update the ringbuffer of transition delays 124 | update_fir(a, delay, is_dark); 125 | } 126 | 127 | // Change at requested time 128 | int display_transition = 0; 129 | if (a->want_switch && 130 | get_delta_nsec(a->next_switch_time, a->capture_time) >= 0) { 131 | a->showing_dark = !is_dark; 132 | display_transition = a->showing_dark ? 1 : -1; 133 | a->want_switch = false; 134 | } 135 | 136 | // State logging 137 | if (a->log) { 138 | fprintf(a->log, "%.9f %.3f %d\n", 139 | get_delta_nsec(a->setup_time, meas_time) * 1e-9, meas_level, 140 | display_transition); 141 | } 142 | 143 | end: 144 | return a->showing_dark ? DisplayDark : DisplayLight; 145 | } 146 | -------------------------------------------------------------------------------- /frontend_fb.c: -------------------------------------------------------------------------------- 1 | #include "interface.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | int main(int argc, char **argv) { 18 | int camera_number = 0; 19 | if (argc != 2 || sscanf(argv[1], "%d", &camera_number) != 1) { 20 | fprintf(stderr, "Usage: latency_qt_fb camera_number\n"); 21 | fprintf(stderr, "Framebuffer frontend for latency tester\n"); 22 | fprintf(stderr, "\n"); 23 | fprintf(stderr, "Arguments:\n"); 24 | fprintf(stderr, " camera_number Which camera device to read from. " 25 | "Should be /dev/videoN\n"); 26 | return EXIT_FAILURE; 27 | } 28 | 29 | __uid_t uid = geteuid(); 30 | if (uid == 0) { 31 | fprintf(stderr, "Please do not run this program as root.\n"); 32 | return EXIT_FAILURE; 33 | } 34 | 35 | // Oddly enough, O_WRONLY fails even though we only mmap with PROT_WRITE 36 | int fbd = open("/dev/fb0", O_RDWR); 37 | if (fbd == -1) { 38 | fprintf(stderr, "Failed to open /dev/fb0, the first framebuffer. Are " 39 | "you (and /dev/fb0) both in group 'video'?\n"); 40 | return EXIT_FAILURE; 41 | } 42 | 43 | struct fb_var_screeninfo vari; 44 | if (ioctl(fbd, FBIOGET_VSCREENINFO, &vari) == -1) { 45 | fprintf(stderr, "Failed to read variable screen data\n"); 46 | return EXIT_FAILURE; 47 | } 48 | struct fb_fix_screeninfo fixi; 49 | if (ioctl(fbd, FBIOGET_FSCREENINFO, &fixi) == -1) { 50 | fprintf(stderr, "Failed to read variable screen data\n"); 51 | return EXIT_FAILURE; 52 | } 53 | 54 | fprintf(stderr, "screen virt xres = %d, yres = %d\n", vari.xres_virtual, 55 | vari.yres_virtual); 56 | fprintf(stderr, "screen data length %d\n", fixi.smem_len); 57 | uint32_t *mem = 58 | (uint32_t *)mmap(0, fixi.smem_len, PROT_WRITE, MAP_SHARED, fbd, 0); 59 | if (mem == MAP_FAILED) { 60 | fprintf(stderr, "Failed to map screen device: %s\n", strerror(errno)); 61 | return EXIT_FAILURE; 62 | } 63 | 64 | void *state = setup_backend(camera_number); 65 | if (!state) { 66 | fprintf(stderr, "Failed to open camera #%d", camera_number); 67 | return EXIT_FAILURE; 68 | } 69 | 70 | bool was_dark = true; 71 | // note: cancel the loop with Ctrl+C 72 | while (1) { 73 | // todo: 1ms pause? 74 | enum WhatToDo wtd = update_backend(state); 75 | bool is_dark = wtd == DisplayDark; 76 | if (is_dark != was_dark) { 77 | was_dark = is_dark; 78 | 79 | memset(mem, is_dark ? 0 : 255, fixi.smem_len); 80 | } 81 | } 82 | 83 | munmap(mem, fixi.smem_len); 84 | close(fbd); 85 | 86 | cleanup_backend(state); 87 | return EXIT_SUCCESS; 88 | } 89 | -------------------------------------------------------------------------------- /frontend_qt.cpp: -------------------------------------------------------------------------------- 1 | #include "interface.h" 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | class MainWindow : public QWidget { 14 | Q_OBJECT 15 | public: 16 | MainWindow(void *s) : QWidget(NULL) { 17 | state = s; 18 | screen_dark = true; 19 | setWindowFlag(Qt::Window); 20 | setAttribute(Qt::WA_OpaquePaintEvent); 21 | setAttribute(Qt::WA_PaintUnclipped); 22 | // TODO: add mode with WA_PaintOnScreen ? 23 | 24 | timer.setSingleShot(false); 25 | timer.setInterval(2); 26 | timer.start(); 27 | connect(&timer, &QTimer::timeout, this, &MainWindow::checkCamera); 28 | } 29 | 30 | virtual void paintEvent(QPaintEvent *event) override { 31 | QPainter p(this); 32 | p.fillRect(this->rect(), screen_dark ? Qt::black : Qt::white); 33 | } 34 | 35 | virtual QSize sizeHint() const override { 36 | return QSize(SMALL_WINDOW_SIZE, SMALL_WINDOW_SIZE); 37 | } 38 | public slots: 39 | void checkCamera() { 40 | enum WhatToDo wtd = update_backend(state); 41 | bool next_dark = wtd == DisplayDark; 42 | if (next_dark != screen_dark) { 43 | screen_dark = next_dark; 44 | update(); 45 | } 46 | } 47 | 48 | private: 49 | QTimer timer; 50 | void *state; 51 | bool screen_dark; 52 | }; 53 | 54 | int main(int argc, char **argv) { 55 | QApplication app(argc, argv); 56 | 57 | QCommandLineParser parser; 58 | parser.setApplicationDescription("Qt frontend for latency tester"); 59 | parser.addHelpOption(); 60 | parser.addPositionalArgument( 61 | "camera_number", 62 | "Which camera device to read from. Should be /dev/videoN"); 63 | bool succeeded = parser.parse(app.arguments()); 64 | if (!succeeded) { 65 | return EXIT_FAILURE; 66 | } 67 | if (parser.positionalArguments().size() != 1) { 68 | parser.showHelp(); 69 | return EXIT_FAILURE; 70 | } 71 | 72 | QString cn = parser.positionalArguments().first(); 73 | bool ok; 74 | int camera_number = cn.toInt(&ok); 75 | if (!ok) { 76 | parser.showHelp(); 77 | return EXIT_FAILURE; 78 | } 79 | 80 | void *state = setup_backend(camera_number); 81 | if (!state) { 82 | qDebug("Failed to open camera #%d", camera_number); 83 | return EXIT_FAILURE; 84 | } 85 | 86 | MainWindow window(state); 87 | window.setVisible(true); 88 | int ret = app.exec(); 89 | cleanup_backend(state); 90 | return ret; 91 | } 92 | 93 | #include "obj/frontend_qt.moc" 94 | -------------------------------------------------------------------------------- /frontend_term.c: -------------------------------------------------------------------------------- 1 | #include "interface.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #define ESC "\x1b[" 8 | #if 0 9 | // For normal color schemes (with a true black and true white) 10 | #define WHITE (ESC "47m" ESC "2J") 11 | #define BLACK (ESC "40m" ESC "2J") 12 | #else 13 | // For terminals with nonstandard palettes that nevertheless support truecolor 14 | #define WHITE \ 15 | (ESC "48;5;255;255;255m" ESC "2J" \ 16 | "\n") 17 | #define BLACK \ 18 | (ESC "48;5;0;0;0m" ESC "2J" \ 19 | "\n") 20 | #endif 21 | 22 | int main(int argc, char **argv) { 23 | int camera_number = 0; 24 | if (argc != 2 || sscanf(argv[1], "%d", &camera_number) != 1) { 25 | fprintf(stderr, "Usage: latency_qt_term camera_number\n"); 26 | fprintf(stderr, "Terminal frontend for latency tester.\n"); 27 | fprintf(stderr, 28 | "(It is recommended to pipe stdout to file, display stderr.\n"); 29 | fprintf(stderr, "\n"); 30 | fprintf(stderr, "Arguments:\n"); 31 | fprintf(stderr, " camera_number Which camera device to read from. " 32 | "Should be /dev/videoN\n"); 33 | return EXIT_FAILURE; 34 | } 35 | 36 | void *state = setup_backend(camera_number); 37 | if (!state) { 38 | fprintf(stderr, "Failed to open camera #%d", camera_number); 39 | return EXIT_FAILURE; 40 | } 41 | 42 | // Disable buffering, in case it was not already disabled 43 | setvbuf(stderr, NULL, _IONBF, 0); 44 | 45 | bool was_dark = true; 46 | fprintf(stderr, BLACK); 47 | 48 | // note: cancel the loop with Ctrl+C 49 | while (1) { 50 | // todo: 1ms pause? 51 | enum WhatToDo wtd = update_backend(state); 52 | bool is_dark = wtd == DisplayDark; 53 | if (is_dark != was_dark) { 54 | was_dark = is_dark; 55 | if (is_dark) { 56 | fprintf(stderr, BLACK); 57 | } else { 58 | fprintf(stderr, WHITE); 59 | } 60 | } 61 | } 62 | 63 | fprintf(stderr, ESC "0m\n"); 64 | 65 | cleanup_backend(state); 66 | return EXIT_SUCCESS; 67 | } 68 | -------------------------------------------------------------------------------- /frontend_wayland.c: -------------------------------------------------------------------------------- 1 | #include "interface.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include "obj/xdg-shell-stable-client-protocol.h" 14 | #include 15 | 16 | struct globals { 17 | struct wl_compositor *compositor; 18 | struct wl_registry *registry; 19 | struct xdg_wm_base *wm_base; 20 | struct wl_shm *shm; 21 | struct wl_surface *surface; 22 | struct xdg_surface *xdg_surface; 23 | struct wl_buffer *buffer_dark; 24 | struct wl_buffer *buffer_light; 25 | struct wl_callback *frame_callback; 26 | struct wl_callback_listener frame_listener; 27 | int32_t width, height; 28 | int size_changed; 29 | int is_dark; 30 | int is_running; 31 | }; 32 | 33 | static struct wl_buffer *make_buffer(struct wl_shm *shm, int width, int height, 34 | int is_dark) { 35 | int stride = width * 4; 36 | int size = stride * height; 37 | struct wl_buffer *buffer = NULL; 38 | 39 | const char *buffer_name = is_dark ? "/buffer_dark" : "/buffer_light"; 40 | shm_unlink(buffer_name); 41 | int fd = shm_open(buffer_name, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR); 42 | if (fd == -1) { 43 | fprintf(stderr, "Failed to shm_open. Try deleting garbage in " 44 | "/dev/shm/, perhaps?\n"); 45 | goto cleanup; 46 | } 47 | int fret = ftruncate(fd, size); 48 | if (fret == -1) { 49 | fprintf(stderr, "Failed to ftruncate\n"); 50 | goto cleanup; 51 | } 52 | 53 | // Drawing is easy! 54 | void *shm_data = 55 | mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); 56 | if (shm_data == MAP_FAILED) { 57 | fprintf(stderr, "Failed to mmap\n"); 58 | goto cleanup; 59 | } 60 | memset(shm_data, is_dark ? 0 : 255, size); 61 | munmap(shm_data, size); 62 | 63 | struct wl_shm_pool *pool = wl_shm_create_pool(shm, fd, size); 64 | buffer = wl_shm_pool_create_buffer(pool, 0, width, height, stride, 65 | WL_SHM_FORMAT_XRGB8888); 66 | wl_shm_pool_destroy(pool); 67 | 68 | cleanup: 69 | close(fd); 70 | shm_unlink(buffer_name); 71 | return buffer; 72 | } 73 | 74 | static void registry_add(void *data, struct wl_registry *wl_registry, 75 | uint32_t name, const char *interface, 76 | uint32_t version) { 77 | struct globals *glob = (struct globals *)data; 78 | 79 | if (!strcmp("xdg_wm_base", interface)) { 80 | glob->wm_base = 81 | wl_registry_bind(glob->registry, name, &xdg_wm_base_interface, 1); 82 | } 83 | if (!strcmp("wl_compositor", interface)) { 84 | glob->compositor = 85 | wl_registry_bind(glob->registry, name, &wl_compositor_interface, 1); 86 | } 87 | if (!strcmp("wl_shm", interface)) { 88 | glob->shm = 89 | wl_registry_bind(glob->registry, name, &wl_shm_interface, 1); 90 | } 91 | } 92 | 93 | static void registry_remove(void *data, struct wl_registry *wl_registry, 94 | uint32_t name) { 95 | // maybe someday 96 | } 97 | static void update_surface(void *data, struct wl_callback *wl_callback, 98 | uint32_t callback_data) { 99 | struct globals *glob = (struct globals *)data; 100 | if (glob->size_changed) { 101 | if (glob->buffer_dark) { 102 | wl_buffer_destroy(glob->buffer_dark); 103 | } 104 | if (glob->buffer_light) { 105 | wl_buffer_destroy(glob->buffer_light); 106 | } 107 | glob->buffer_light = 108 | make_buffer(glob->shm, glob->width, glob->height, 0); 109 | glob->buffer_dark = 110 | make_buffer(glob->shm, glob->width, glob->height, 1); 111 | 112 | glob->size_changed = 0; 113 | } 114 | if (!glob->buffer_dark || !glob->buffer_light) { 115 | return; 116 | } 117 | 118 | wl_surface_attach(glob->surface, 119 | glob->is_dark ? glob->buffer_dark : glob->buffer_light, 0, 120 | 0); 121 | wl_surface_damage(glob->surface, 0, 0, glob->width, glob->height); 122 | 123 | if (glob->frame_callback) { 124 | // doesn't the server do this? 125 | wl_callback_destroy(glob->frame_callback); 126 | } 127 | glob->frame_callback = wl_surface_frame(glob->surface); 128 | wl_callback_add_listener(glob->frame_callback, &glob->frame_listener, glob); 129 | wl_surface_commit(glob->surface); 130 | } 131 | 132 | static void xdgsurf_configure(void *data, struct xdg_surface *xdg_surface, 133 | uint32_t serial) { 134 | struct globals *glob = (struct globals *)data; 135 | xdg_surface_ack_configure(xdg_surface, serial); 136 | 137 | update_surface(glob, NULL, 0); 138 | } 139 | static void xdgtop_configure(void *data, struct xdg_toplevel *xdg_toplevel, 140 | int32_t width, int32_t height, 141 | struct wl_array *states) { 142 | struct globals *glob = (struct globals *)data; 143 | if (width != glob->width || height != glob->height) { 144 | glob->size_changed = 1; 145 | } 146 | glob->width = width ? width : SMALL_WINDOW_SIZE; 147 | glob->height = height ? height : SMALL_WINDOW_SIZE; 148 | } 149 | static void xdgtop_close(void *data, struct xdg_toplevel *xdg_toplevel) { 150 | struct globals *glob = (struct globals *)data; 151 | glob->is_running = 0; 152 | } 153 | 154 | int main(int argc, char **argv) { 155 | int camera_number = 0; 156 | if (argc != 2 || sscanf(argv[1], "%d", &camera_number) != 1) { 157 | fprintf(stderr, "Usage: latency_qt_wayland camera_number\n"); 158 | fprintf(stderr, "Wayland frontend for latency tester\n"); 159 | fprintf(stderr, "\n"); 160 | fprintf(stderr, "Arguments:\n"); 161 | fprintf(stderr, " camera_number Which camera device to read from. " 162 | "Should be /dev/videoN\n"); 163 | return EXIT_FAILURE; 164 | } 165 | 166 | void *state = setup_backend(camera_number); 167 | if (!state) { 168 | fprintf(stderr, "Failed to open camera #%d", camera_number); 169 | return EXIT_FAILURE; 170 | } 171 | 172 | struct wl_display *display = wl_display_connect(NULL); 173 | if (!display) { 174 | fprintf(stderr, "Failed to connect to a display\n"); 175 | return EXIT_FAILURE; 176 | } 177 | 178 | // Error handling often ignored for simplicity 179 | struct globals glob = {0}; 180 | glob.width = SMALL_WINDOW_SIZE; 181 | glob.height = SMALL_WINDOW_SIZE; 182 | glob.size_changed = 1; 183 | glob.is_running = 1; 184 | glob.registry = wl_display_get_registry(display); 185 | struct wl_registry_listener reg_listen = {®istry_add, ®istry_remove}; 186 | wl_registry_add_listener(glob.registry, ®_listen, &glob); 187 | wl_display_roundtrip(display); // Wait until registry filled 188 | if (!glob.shm || !glob.wm_base || !glob.compositor) { 189 | fprintf(stderr, 190 | "Failed to acquire global: compositor %c shm %c wm_base %c\n", 191 | glob.compositor ? 'Y' : 'N', glob.shm ? 'Y' : 'N', 192 | glob.wm_base ? 'Y' : 'N'); 193 | return EXIT_FAILURE; 194 | } 195 | 196 | wl_display_dispatch(display); // wait for compositor to send requests 197 | 198 | // Make surface, then shell surface 199 | glob.surface = wl_compositor_create_surface(glob.compositor); 200 | if (!glob.surface) { 201 | fprintf(stderr, "Failed to create a surface\n"); 202 | return EXIT_FAILURE; 203 | } 204 | 205 | glob.frame_listener.done = update_surface; 206 | 207 | glob.xdg_surface = xdg_wm_base_get_xdg_surface(glob.wm_base, glob.surface); 208 | struct xdg_surface_listener xdgsurf_listen = {.configure = 209 | xdgsurf_configure}; 210 | xdg_surface_add_listener(glob.xdg_surface, &xdgsurf_listen, &glob); 211 | struct xdg_toplevel *xdg_toplevel = 212 | xdg_surface_get_toplevel(glob.xdg_surface); 213 | struct xdg_toplevel_listener xdgtop_listen = {.configure = xdgtop_configure, 214 | .close = xdgtop_close}; 215 | xdg_toplevel_add_listener(xdg_toplevel, &xdgtop_listen, &glob); 216 | xdg_toplevel_set_title(xdg_toplevel, "wayland shm frontend"); 217 | wl_surface_commit(glob.surface); 218 | 219 | struct timespec old_time; 220 | clock_gettime(CLOCK_MONOTONIC, &old_time); 221 | while (glob.is_running) { 222 | if (wl_display_dispatch_pending(display) == -1 || 223 | wl_display_flush(display) == -1) { 224 | break; 225 | } 226 | 227 | struct pollfd fds[1]; 228 | fds[0].fd = wl_display_get_fd(display); 229 | fds[0].events = POLLIN; 230 | fds[0].revents = 0; 231 | poll(fds, sizeof fds / sizeof fds[0], 1); // wait up to 1 ms 232 | 233 | if (fds[0].revents && wl_display_dispatch(display) == -1) { 234 | break; 235 | } 236 | 237 | struct timespec new_time; 238 | clock_gettime(CLOCK_MONOTONIC, &new_time); 239 | double time_difference = (new_time.tv_sec - old_time.tv_sec) * 1.0 + 240 | (new_time.tv_nsec - old_time.tv_nsec) * 1e-9; 241 | if (time_difference > 0.001) { 242 | old_time = new_time; 243 | enum WhatToDo wtd = update_backend(state); 244 | int next_dark = wtd == DisplayDark; 245 | if (next_dark != glob.is_dark) { 246 | glob.is_dark = next_dark; 247 | // Update surface on color change 248 | update_surface(&glob, NULL, 1); 249 | } 250 | } 251 | } 252 | 253 | wl_display_disconnect(display); 254 | cleanup_backend(state); 255 | return EXIT_SUCCESS; 256 | } 257 | -------------------------------------------------------------------------------- /frontend_wayland_gbm.c: -------------------------------------------------------------------------------- 1 | #include "interface.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include 14 | 15 | #include "obj/xdg-shell-stable-client-protocol.h" 16 | #include "obj/linux-dmabuf-unstable-v1-client-protocol.h" 17 | #include 18 | 19 | struct globals { 20 | struct wl_compositor *compositor; 21 | struct wl_registry *registry; 22 | struct xdg_wm_base *wm_base; 23 | struct zwp_linux_dmabuf_v1 *dmabuf; 24 | struct wl_surface *surface; 25 | struct xdg_surface *xdg_surface; 26 | struct wl_buffer *buffer_dark; 27 | struct wl_buffer *buffer_light; 28 | struct wl_callback *frame_callback; 29 | struct wl_callback_listener frame_listener; 30 | struct gbm_device *gbm; 31 | int32_t width, height; 32 | int size_changed; 33 | int is_dark; 34 | int is_running; 35 | }; 36 | 37 | static struct wl_buffer *make_buffer(struct gbm_device* gbm, struct zwp_linux_dmabuf_v1 *dmabuf, int width, int height, 38 | int is_dark) { 39 | // Linear is generally available; scanout buffers can be directly presented 40 | struct gbm_bo *bo = gbm_bo_create(gbm, width, height, GBM_FORMAT_XRGB8888, 41 | GBM_BO_USE_LINEAR | GBM_BO_USE_SCANOUT); 42 | if (!bo) { 43 | fprintf(stderr, "Failed at gbm_bo_create.\n"); 44 | return NULL; 45 | } 46 | 47 | uint32_t stride = 0; 48 | void *map_handle = NULL; 49 | uint8_t *data = (uint8_t *)gbm_bo_map(bo, 0, 0, width, height, GBM_BO_TRANSFER_WRITE, 50 | &stride, &map_handle); 51 | if (!data) { 52 | gbm_bo_destroy(bo); 53 | fprintf(stderr, "Failed at to map GBM buffer object\n"); 54 | return NULL; 55 | } 56 | for (int y = 0; y < height; y++) { 57 | uint32_t *row = (uint32_t *)(data + y * stride); 58 | for (int x = 0; x < width; x++) { 59 | row[x] = is_dark ? 0xff000000 : 0xffffffff; 60 | } 61 | } 62 | gbm_bo_unmap(bo, map_handle); 63 | 64 | // stride need not match that used above 65 | stride = gbm_bo_get_stride(bo); 66 | int buffer_fd = gbm_bo_get_fd(bo); 67 | gbm_bo_destroy(bo); 68 | 69 | struct zwp_linux_buffer_params_v1 *params = 70 | zwp_linux_dmabuf_v1_create_params(dmabuf); 71 | zwp_linux_buffer_params_v1_add(params, buffer_fd, 0, 72 | 0, stride, 0, 0); 73 | struct wl_buffer *buffer = zwp_linux_buffer_params_v1_create_immed(params, width, height, GBM_FORMAT_XRGB8888, 0); 74 | zwp_linux_buffer_params_v1_destroy(params); 75 | 76 | return buffer; 77 | } 78 | 79 | static void registry_add(void *data, struct wl_registry *wl_registry, 80 | uint32_t name, const char *interface, 81 | uint32_t version) { 82 | struct globals *glob = (struct globals *)data; 83 | 84 | if (!strcmp(xdg_wm_base_interface.name, interface)) { 85 | glob->wm_base = 86 | wl_registry_bind(glob->registry, name, &xdg_wm_base_interface, 1); 87 | } 88 | if (!strcmp(wl_compositor_interface.name, interface)) { 89 | glob->compositor = 90 | wl_registry_bind(glob->registry, name, &wl_compositor_interface, 1); 91 | } 92 | if (!strcmp(zwp_linux_dmabuf_v1_interface.name, interface)) { 93 | glob->dmabuf = 94 | wl_registry_bind(glob->registry, name, &zwp_linux_dmabuf_v1_interface, 3); 95 | } 96 | } 97 | 98 | static void registry_remove(void *data, struct wl_registry *wl_registry, 99 | uint32_t name) { 100 | // maybe someday 101 | } 102 | static void update_surface(void *data, struct wl_callback *wl_callback, 103 | uint32_t callback_data) { 104 | struct globals *glob = (struct globals *)data; 105 | if (glob->size_changed) { 106 | if (glob->buffer_dark) { 107 | wl_buffer_destroy(glob->buffer_dark); 108 | } 109 | if (glob->buffer_light) { 110 | wl_buffer_destroy(glob->buffer_light); 111 | } 112 | glob->buffer_light = 113 | make_buffer(glob->gbm, glob->dmabuf, glob->width, glob->height, 0); 114 | glob->buffer_dark = 115 | make_buffer(glob->gbm, glob->dmabuf, glob->width, glob->height, 1); 116 | 117 | glob->size_changed = 0; 118 | } 119 | if (!glob->buffer_dark || !glob->buffer_light) { 120 | return; 121 | } 122 | 123 | wl_surface_attach(glob->surface, 124 | glob->is_dark ? glob->buffer_dark : glob->buffer_light, 0, 125 | 0); 126 | wl_surface_damage(glob->surface, 0, 0, glob->width, glob->height); 127 | 128 | if (glob->frame_callback) { 129 | // doesn't the server do this? 130 | wl_callback_destroy(glob->frame_callback); 131 | } 132 | glob->frame_callback = wl_surface_frame(glob->surface); 133 | wl_callback_add_listener(glob->frame_callback, &glob->frame_listener, glob); 134 | wl_surface_commit(glob->surface); 135 | } 136 | 137 | static void xdgsurf_configure(void *data, struct xdg_surface *xdg_surface, 138 | uint32_t serial) { 139 | struct globals *glob = (struct globals *)data; 140 | xdg_surface_ack_configure(xdg_surface, serial); 141 | 142 | update_surface(glob, NULL, 0); 143 | } 144 | static void xdgtop_configure(void *data, struct xdg_toplevel *xdg_toplevel, 145 | int32_t width, int32_t height, 146 | struct wl_array *states) { 147 | struct globals *glob = (struct globals *)data; 148 | if (width != glob->width || height != glob->height) { 149 | glob->size_changed = 1; 150 | } 151 | glob->width = width ? width : SMALL_WINDOW_SIZE; 152 | glob->height = height ? height : SMALL_WINDOW_SIZE; 153 | } 154 | static void xdgtop_close(void *data, struct xdg_toplevel *xdg_toplevel) { 155 | struct globals *glob = (struct globals *)data; 156 | glob->is_running = 0; 157 | } 158 | 159 | int main(int argc, char **argv) { 160 | int camera_number = 0; 161 | if (argc != 2 || sscanf(argv[1], "%d", &camera_number) != 1) { 162 | fprintf(stderr, "Usage: %s camera_number\n", argv[0]); 163 | fprintf(stderr, "Wayland frontend for latency tester, using fixed buffers created with GBM\n"); 164 | fprintf(stderr, "\n"); 165 | fprintf(stderr, "Arguments:\n"); 166 | fprintf(stderr, " camera_number Which camera device to read from. " 167 | "Should be /dev/videoN\n"); 168 | return EXIT_FAILURE; 169 | } 170 | 171 | void *state = setup_backend(camera_number); 172 | if (!state) { 173 | fprintf(stderr, "Failed to open camera #%d", camera_number); 174 | return EXIT_FAILURE; 175 | } 176 | 177 | struct wl_display *display = wl_display_connect(NULL); 178 | if (!display) { 179 | fprintf(stderr, "Failed to connect to a display\n"); 180 | return EXIT_FAILURE; 181 | } 182 | 183 | // Error handling often ignored for simplicity 184 | struct globals glob = {0}; 185 | glob.width = SMALL_WINDOW_SIZE; 186 | glob.height = SMALL_WINDOW_SIZE; 187 | glob.size_changed = 1; 188 | glob.is_running = 1; 189 | glob.registry = wl_display_get_registry(display); 190 | struct wl_registry_listener reg_listen = {®istry_add, ®istry_remove}; 191 | wl_registry_add_listener(glob.registry, ®_listen, &glob); 192 | wl_display_roundtrip(display); // Wait until registry filled 193 | if (!glob.dmabuf || !glob.wm_base || !glob.compositor) { 194 | fprintf(stderr, 195 | "Failed to acquire global: compositor %c linux-dmabuf %c wm_base %c\n", 196 | glob.compositor ? 'Y' : 'N', glob.dmabuf ? 'Y' : 'N', 197 | glob.wm_base ? 'Y' : 'N'); 198 | return EXIT_FAILURE; 199 | } 200 | 201 | int drm_fd = open("/dev/dri/renderD128", O_RDWR); 202 | if (drm_fd == -1) { 203 | fprintf(stderr, "Failed to connect to DRM device at /dev/dri/renderD128\n"); 204 | return EXIT_FAILURE; 205 | } 206 | glob.gbm = gbm_create_device(drm_fd); 207 | if (!glob.gbm) { 208 | fprintf(stderr, "Failed to create GBM device\n"); 209 | return EXIT_FAILURE; 210 | } 211 | 212 | wl_display_dispatch(display); // wait for compositor to send requests 213 | 214 | // Make surface, then shell surface 215 | glob.surface = wl_compositor_create_surface(glob.compositor); 216 | if (!glob.surface) { 217 | fprintf(stderr, "Failed to create a surface\n"); 218 | return EXIT_FAILURE; 219 | } 220 | 221 | glob.frame_listener.done = update_surface; 222 | 223 | glob.xdg_surface = xdg_wm_base_get_xdg_surface(glob.wm_base, glob.surface); 224 | struct xdg_surface_listener xdgsurf_listen = {.configure = 225 | xdgsurf_configure}; 226 | xdg_surface_add_listener(glob.xdg_surface, &xdgsurf_listen, &glob); 227 | struct xdg_toplevel *xdg_toplevel = 228 | xdg_surface_get_toplevel(glob.xdg_surface); 229 | struct xdg_toplevel_listener xdgtop_listen = {.configure = xdgtop_configure, 230 | .close = xdgtop_close}; 231 | xdg_toplevel_add_listener(xdg_toplevel, &xdgtop_listen, &glob); 232 | xdg_toplevel_set_title(xdg_toplevel, "wayland shm frontend"); 233 | wl_surface_commit(glob.surface); 234 | 235 | struct timespec old_time; 236 | clock_gettime(CLOCK_MONOTONIC, &old_time); 237 | while (glob.is_running) { 238 | if (wl_display_dispatch_pending(display) == -1 || 239 | wl_display_flush(display) == -1) { 240 | break; 241 | } 242 | 243 | struct pollfd fds[1]; 244 | fds[0].fd = wl_display_get_fd(display); 245 | fds[0].events = POLLIN; 246 | fds[0].revents = 0; 247 | poll(fds, sizeof fds / sizeof fds[0], 1); // wait up to 1 ms 248 | 249 | if (fds[0].revents && wl_display_dispatch(display) == -1) { 250 | break; 251 | } 252 | 253 | struct timespec new_time; 254 | clock_gettime(CLOCK_MONOTONIC, &new_time); 255 | double time_difference = (new_time.tv_sec - old_time.tv_sec) * 1.0 + 256 | (new_time.tv_nsec - old_time.tv_nsec) * 1e-9; 257 | if (time_difference > 0.001) { 258 | old_time = new_time; 259 | enum WhatToDo wtd = update_backend(state); 260 | int next_dark = wtd == DisplayDark; 261 | if (next_dark != glob.is_dark) { 262 | glob.is_dark = next_dark; 263 | // Update surface on color change 264 | update_surface(&glob, NULL, 1); 265 | } 266 | } 267 | } 268 | 269 | gbm_device_destroy(glob.gbm); 270 | close(drm_fd); 271 | wl_display_disconnect(display); 272 | cleanup_backend(state); 273 | return EXIT_SUCCESS; 274 | } 275 | -------------------------------------------------------------------------------- /frontend_wayland_gl.c: -------------------------------------------------------------------------------- 1 | #include "interface.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include 14 | #include 15 | #include 16 | 17 | #include "obj/xdg-shell-stable-client-protocol.h" 18 | #include 19 | #include 20 | 21 | struct globals { 22 | struct wl_compositor *compositor; 23 | struct wl_registry *registry; 24 | struct xdg_wm_base *wm_base; 25 | struct wl_egl_window *window; 26 | struct wl_surface *surface; 27 | struct xdg_surface *xdg_surface; 28 | struct wl_callback *frame_callback; 29 | struct wl_callback_listener frame_listener; 30 | EGLDisplay egl_display; 31 | EGLConfig egl_config; 32 | EGLContext egl_context; 33 | EGLSurface egl_surface; 34 | int32_t width, height; 35 | int size_changed; 36 | int is_dark; 37 | int is_running; 38 | }; 39 | 40 | static void registry_add(void *data, struct wl_registry *wl_registry, 41 | uint32_t name, const char *interface, 42 | uint32_t version) { 43 | struct globals *glob = (struct globals *)data; 44 | 45 | if (!strcmp("xdg_wm_base", interface)) { 46 | glob->wm_base = 47 | wl_registry_bind(glob->registry, name, &xdg_wm_base_interface, 1); 48 | } 49 | if (!strcmp("wl_compositor", interface)) { 50 | glob->compositor = 51 | wl_registry_bind(glob->registry, name, &wl_compositor_interface, 1); 52 | } 53 | } 54 | 55 | static void registry_remove(void *data, struct wl_registry *wl_registry, 56 | uint32_t name) { 57 | // maybe someday 58 | } 59 | static void update_surface(void *data, struct wl_callback *wl_callback, 60 | uint32_t callback_data) { 61 | struct globals *glob = (struct globals *)data; 62 | if (glob->size_changed) { 63 | wl_egl_window_resize(glob->window, glob->width, glob->height, 0, 0); 64 | } 65 | 66 | // While glClear is fairly efficient, the cost is still roughly proportional 67 | // to the window area. 68 | if (glob->is_dark) { 69 | glClearColor(0.0, 0.0, 0.0, 1.0); 70 | } else { 71 | glClearColor(1.0, 1.0, 1.0, 1.0); 72 | } 73 | glClear(GL_COLOR_BUFFER_BIT); 74 | if (!eglSwapBuffers(glob->egl_display, glob->egl_surface)) { 75 | glob->is_running = 0; 76 | fprintf(stderr, "Failed to swap buffers\n"); 77 | return; 78 | } 79 | // wl_surface_damage(glob->surface, 0, 0, glob->width, glob->height); 80 | 81 | if (glob->frame_callback) { 82 | // doesn't the server do this? 83 | wl_callback_destroy(glob->frame_callback); 84 | } 85 | glob->frame_callback = wl_surface_frame(glob->surface); 86 | wl_callback_add_listener(glob->frame_callback, &glob->frame_listener, glob); 87 | wl_surface_commit(glob->surface); 88 | } 89 | 90 | static void xdgsurf_configure(void *data, struct xdg_surface *xdg_surface, 91 | uint32_t serial) { 92 | struct globals *glob = (struct globals *)data; 93 | xdg_surface_ack_configure(xdg_surface, serial); 94 | 95 | if (!glob->window) { 96 | // First configure, create the window 97 | glob->size_changed = 0; 98 | glob->window = 99 | wl_egl_window_create(glob->surface, glob->width, glob->height); 100 | if (!glob->window) { 101 | glob->is_running = 0; 102 | fprintf(stderr, "Failed to create EGL window\n"); 103 | return; 104 | } 105 | glob->egl_surface = 106 | eglCreateWindowSurface(glob->egl_display, glob->egl_config, 107 | (EGLNativeWindowType)glob->window, NULL); 108 | if (!glob->egl_surface) { 109 | glob->is_running = 0; 110 | fprintf(stderr, "Failed to create EGL surface\n"); 111 | return; 112 | } 113 | glob->egl_context = eglCreateContext( 114 | glob->egl_display, glob->egl_config, EGL_NO_CONTEXT, NULL); 115 | if (glob->egl_context == EGL_NO_CONTEXT) { 116 | glob->is_running = 0; 117 | fprintf(stderr, "Failed to create EGL context\n"); 118 | return; 119 | } 120 | if (!eglMakeCurrent(glob->egl_display, glob->egl_surface, 121 | glob->egl_surface, 122 | glob->egl_context)) { 123 | glob->is_running = 0; 124 | fprintf(stderr, 125 | "Failed to make EGL context current on EGL surface: %x\n", 126 | eglGetError()); 127 | } 128 | } 129 | 130 | update_surface(glob, NULL, 0); 131 | } 132 | static void xdgtop_configure(void *data, struct xdg_toplevel *xdg_toplevel, 133 | int32_t width, int32_t height, 134 | struct wl_array *states) { 135 | struct globals *glob = (struct globals *)data; 136 | if (width != glob->width || height != glob->height) { 137 | glob->size_changed = 1; 138 | } 139 | glob->width = width ? width : SMALL_WINDOW_SIZE; 140 | glob->height = height ? height : SMALL_WINDOW_SIZE; 141 | } 142 | static void xdgtop_close(void *data, struct xdg_toplevel *xdg_toplevel) { 143 | struct globals *glob = (struct globals *)data; 144 | glob->is_running = 0; 145 | } 146 | 147 | int main(int argc, char **argv) { 148 | int camera_number = 0; 149 | if (argc != 2 || sscanf(argv[1], "%d", &camera_number) != 1) { 150 | fprintf(stderr, "Usage: latency_wayland_gl camera_number\n"); 151 | fprintf(stderr, "Wayland frontend for latency tester\n"); 152 | fprintf(stderr, "\n"); 153 | fprintf(stderr, "Arguments:\n"); 154 | fprintf(stderr, " camera_number Which camera device to read from. " 155 | "Should be /dev/videoN\n"); 156 | return EXIT_FAILURE; 157 | } 158 | 159 | void *state = setup_backend(camera_number); 160 | if (!state) { 161 | fprintf(stderr, "Failed to open camera #%d", camera_number); 162 | return EXIT_FAILURE; 163 | } 164 | 165 | struct wl_display *display = wl_display_connect(NULL); 166 | if (!display) { 167 | fprintf(stderr, "Failed to connect to a display\n"); 168 | return EXIT_FAILURE; 169 | } 170 | 171 | // Error handling often ignored for simplicity 172 | struct globals glob = {0}; 173 | glob.width = SMALL_WINDOW_SIZE; 174 | glob.height = SMALL_WINDOW_SIZE; 175 | glob.size_changed = 1; 176 | glob.is_running = 1; 177 | glob.registry = wl_display_get_registry(display); 178 | struct wl_registry_listener reg_listen = {®istry_add, ®istry_remove}; 179 | wl_registry_add_listener(glob.registry, ®_listen, &glob); 180 | 181 | // Initialize EGL 182 | glob.egl_display = 183 | eglGetPlatformDisplay(EGL_PLATFORM_WAYLAND_KHR, display, NULL); 184 | if (!glob.egl_display) { 185 | fprintf(stderr, "Failed to create EGL display\n"); 186 | return EXIT_FAILURE; 187 | } 188 | wl_display_roundtrip(display); // Wait until registry filled 189 | if (!glob.wm_base || !glob.compositor) { 190 | fprintf(stderr, "Failed to acquire global: compositor %c wm_base %c\n", 191 | glob.compositor ? 'Y' : 'N', glob.wm_base ? 'Y' : 'N'); 192 | return EXIT_FAILURE; 193 | } 194 | 195 | int major_version = -1, minor_version = -1; 196 | if (!eglInitialize(glob.egl_display, &major_version, &minor_version)) { 197 | fprintf(stderr, "Failed to initialize EGL display\n"); 198 | return EXIT_FAILURE; 199 | } 200 | if (!eglBindAPI(EGL_OPENGL_API)) { 201 | fprintf(stderr, "Failed to bind OpenGL API\n"); 202 | return EXIT_FAILURE; 203 | } 204 | EGLint config_attrib_list[] = {EGL_SURFACE_TYPE, 205 | EGL_WINDOW_BIT, 206 | EGL_RED_SIZE, 207 | 1, 208 | EGL_GREEN_SIZE, 209 | 1, 210 | EGL_BLUE_SIZE, 211 | 1, 212 | EGL_RENDERABLE_TYPE, 213 | EGL_OPENGL_BIT, 214 | EGL_NONE}; 215 | int num_configs = 0; 216 | if (!eglChooseConfig(glob.egl_display, config_attrib_list, &glob.egl_config, 217 | 1, &num_configs) || 218 | num_configs < 1) { 219 | fprintf(stderr, "Failed to get a valid EGL config\n"); 220 | return EXIT_FAILURE; 221 | } 222 | 223 | // Make surface, then shell surface 224 | glob.surface = wl_compositor_create_surface(glob.compositor); 225 | if (!glob.surface) { 226 | fprintf(stderr, "Failed to create a surface\n"); 227 | return EXIT_FAILURE; 228 | } 229 | 230 | glob.frame_listener.done = update_surface; 231 | 232 | glob.xdg_surface = xdg_wm_base_get_xdg_surface(glob.wm_base, glob.surface); 233 | struct xdg_surface_listener xdgsurf_listen = {.configure = 234 | xdgsurf_configure}; 235 | xdg_surface_add_listener(glob.xdg_surface, &xdgsurf_listen, &glob); 236 | struct xdg_toplevel *xdg_toplevel = 237 | xdg_surface_get_toplevel(glob.xdg_surface); 238 | struct xdg_toplevel_listener xdgtop_listen = {.configure = xdgtop_configure, 239 | .close = xdgtop_close}; 240 | xdg_toplevel_add_listener(xdg_toplevel, &xdgtop_listen, &glob); 241 | xdg_toplevel_set_title(xdg_toplevel, "wayland shm frontend"); 242 | wl_surface_commit(glob.surface); 243 | 244 | struct timespec old_time; 245 | clock_gettime(CLOCK_MONOTONIC, &old_time); 246 | while (glob.is_running) { 247 | if (wl_display_dispatch_pending(display) == -1 || 248 | wl_display_flush(display) == -1) { 249 | break; 250 | } 251 | 252 | struct pollfd fds[1]; 253 | fds[0].fd = wl_display_get_fd(display); 254 | fds[0].events = POLLIN; 255 | fds[0].revents = 0; 256 | poll(fds, sizeof fds / sizeof fds[0], 1); // wait up to 1 ms 257 | 258 | if (fds[0].revents && wl_display_dispatch(display) == -1) { 259 | break; 260 | } 261 | 262 | struct timespec new_time; 263 | clock_gettime(CLOCK_MONOTONIC, &new_time); 264 | double time_difference = (new_time.tv_sec - old_time.tv_sec) * 1.0 + 265 | (new_time.tv_nsec - old_time.tv_nsec) * 1e-9; 266 | if (time_difference > 0.001) { 267 | old_time = new_time; 268 | enum WhatToDo wtd = update_backend(state); 269 | int next_dark = wtd == DisplayDark; 270 | if (next_dark != glob.is_dark) { 271 | glob.is_dark = next_dark; 272 | // Update surface on color change 273 | update_surface(&glob, NULL, 1); 274 | } 275 | } 276 | } 277 | 278 | wl_display_disconnect(display); 279 | cleanup_backend(state); 280 | return EXIT_SUCCESS; 281 | } 282 | -------------------------------------------------------------------------------- /frontend_xcb.c: -------------------------------------------------------------------------------- 1 | #include "interface.h" 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | int main(int argc, char **argv) { 9 | int camera_number = 0; 10 | if (argc != 2 || sscanf(argv[1], "%d", &camera_number) != 1) { 11 | fprintf(stderr, "Usage: latency_qt_xcb camera_number\n"); 12 | fprintf(stderr, "XCB frontend for latency tester\n"); 13 | fprintf(stderr, "\n"); 14 | fprintf(stderr, "Arguments:\n"); 15 | fprintf(stderr, " camera_number Which camera device to read from. " 16 | "Should be /dev/videoN\n"); 17 | return EXIT_FAILURE; 18 | } 19 | 20 | void *state = setup_backend(camera_number); 21 | if (!state) { 22 | fprintf(stderr, "Failed to open camera #%d", camera_number); 23 | return EXIT_FAILURE; 24 | } 25 | 26 | // Boilerplate for xcb setup 27 | xcb_connection_t *connection = xcb_connect(NULL, NULL); 28 | xcb_screen_t *screen = 29 | xcb_setup_roots_iterator(xcb_get_setup(connection)).data; 30 | xcb_drawable_t window = screen->root; 31 | xcb_gcontext_t foreground = xcb_generate_id(connection); 32 | uint32_t mask = XCB_GC_FOREGROUND | XCB_GC_GRAPHICS_EXPOSURES; 33 | uint32_t values[2] = {screen->black_pixel, 0}; 34 | xcb_create_gc(connection, foreground, window, mask, values); 35 | window = xcb_generate_id(connection); 36 | 37 | mask = XCB_CW_BACK_PIXEL | XCB_CW_EVENT_MASK; 38 | values[0] = screen->white_pixel; 39 | values[1] = XCB_EVENT_MASK_EXPOSURE | XCB_EVENT_MASK_KEY_PRESS; 40 | 41 | xcb_create_window(connection, XCB_COPY_FROM_PARENT, window, screen->root, 0, 42 | 0, SMALL_WINDOW_SIZE, SMALL_WINDOW_SIZE, 0, 43 | XCB_WINDOW_CLASS_INPUT_OUTPUT, screen->root_visual, mask, 44 | values); 45 | 46 | /* Map the window on the screen and flush*/ 47 | xcb_map_window(connection, window); 48 | xcb_flush(connection); 49 | 50 | int is_dark = 1; 51 | int quitting = 0; 52 | xcb_generic_event_t *event; 53 | int fdes = xcb_get_file_descriptor(connection); 54 | struct timespec timeout = {0, 1000000}; // 1ms tick, accuracy unimportant 55 | fd_set fds; 56 | while (1) { 57 | FD_ZERO(&fds); 58 | FD_SET(fdes, &fds); 59 | if (pselect(fdes + 1, &fds, NULL, NULL, &timeout, NULL) > 0) { 60 | // We assume no overflow 61 | if ((event = xcb_poll_for_event(connection))) { 62 | switch (event->response_type & ~0x80) { 63 | case XCB_EXPOSE: 64 | values[0] = 65 | is_dark ? screen->black_pixel : screen->white_pixel; 66 | xcb_change_window_attributes(connection, window, 67 | XCB_CW_BACK_PIXEL, values); 68 | xcb_flush(connection); 69 | break; 70 | case XCB_KEY_PRESS: { 71 | xcb_key_press_event_t *key_event = 72 | (xcb_key_press_event_t *)event; 73 | 74 | /* ESC or Q, by keyboard position */ 75 | if (key_event->detail == 9 || key_event->detail == 24) { 76 | xcb_destroy_window(connection, window); 77 | quitting = 1; 78 | } 79 | } break; 80 | default: 81 | break; // Unimportant 82 | } 83 | } 84 | } else { 85 | // Poll the backend. 86 | enum WhatToDo wtd = update_backend(state); 87 | is_dark = wtd == DisplayDark; 88 | 89 | values[0] = is_dark ? screen->black_pixel : screen->white_pixel; 90 | xcb_change_window_attributes(connection, window, XCB_CW_BACK_PIXEL, 91 | values); 92 | xcb_clear_area(connection, 0, window, 0, 0, 0, 0); 93 | xcb_flush(connection); 94 | } 95 | if (quitting) { 96 | break; 97 | } 98 | } 99 | 100 | xcb_destroy_window(connection, window); 101 | xcb_disconnect(connection); 102 | 103 | cleanup_backend(state); 104 | return EXIT_SUCCESS; 105 | } 106 | -------------------------------------------------------------------------------- /interface.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #ifdef __cplusplus 8 | extern "C" { 9 | #endif 10 | 11 | #define SMALL_WINDOW_SIZE 400 12 | 13 | enum WhatToDo { DisplayDark, DisplayLight }; 14 | void *setup_backend(int camera); 15 | enum WhatToDo update_backend(void *state); 16 | void cleanup_backend(void *state); 17 | 18 | struct analysis { 19 | // Analysis of delays 20 | double current_camera_level; // What color did the camera last see? 21 | int showing_dark; // What color should the screen show now? 22 | int want_switch; // Is a time scheduled to switch screen colors? 23 | struct timespec capture_time; 24 | struct timespec next_switch_time; 25 | 26 | // Analysis of delays 27 | double *fir; 28 | int fir_head; 29 | int nframes; 30 | 31 | // To record raw data to file 32 | struct timespec setup_time; 33 | FILE *log; 34 | }; 35 | 36 | int setup_analysis(struct analysis *a); 37 | void cleanup_analysis(struct analysis *a); 38 | enum WhatToDo update_analysis(struct analysis *s, 39 | struct timespec measurement_time, 40 | double measurement, double threshold); 41 | 42 | inline int64_t get_delta_nsec(const struct timespec x0, 43 | const struct timespec x1) { 44 | return (x1.tv_sec - x0.tv_sec) * 1000000000 + (x1.tv_nsec - x0.tv_nsec); 45 | } 46 | 47 | inline struct timespec advance_time(struct timespec previous, 48 | int64_t step_nsec) { 49 | struct timespec next; 50 | next.tv_sec = previous.tv_sec + step_nsec / 1000000000; 51 | next.tv_nsec = previous.tv_nsec + step_nsec % 1000000000; 52 | if (next.tv_nsec >= 1000000000) { 53 | next.tv_nsec -= 1000000000; 54 | next.tv_sec++; 55 | } 56 | return next; 57 | } 58 | 59 | #ifdef __cplusplus 60 | } 61 | #endif 62 | --------------------------------------------------------------------------------