#define _GNU_SOURCE
#define _POSIX_C_SOURCE 200809L
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <stdarg.h>
#include <dirent.h>
#include <fcntl.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/syscall.h>
#include <linux/capability.h>
#include <xf86drm.h>
#include <xf86drmMode.h>
#include "drmtap_internal.h"
#ifndef DRM_CLIENT_CAP_CURSOR_PLANE_HOTSPOT
#define DRM_CLIENT_CAP_CURSOR_PLANE_HOTSPOT 6
#endif
static int drmtap_have_cap_sys_admin(void) {
struct __user_cap_header_struct hdr = {
.version = _LINUX_CAPABILITY_VERSION_3,
.pid = 0,
};
struct __user_cap_data_struct data[2] = {{0, 0, 0}, {0, 0, 0}};
if (syscall(SYS_capget, &hdr, data) != 0) {
return (geteuid() == 0 || getuid() == 0);
}
return (data[0].effective & (1u << CAP_SYS_ADMIN)) != 0;
}
static _Thread_local char g_static_error[512] = "";
void drmtap_set_error(drmtap_ctx *ctx, const char *fmt, ...) {
va_list ap;
va_start(ap, fmt);
if (ctx) {
vsnprintf(ctx->error_msg, sizeof(ctx->error_msg), fmt, ap);
} else {
vsnprintf(g_static_error, sizeof(g_static_error), fmt, ap);
}
va_end(ap);
}
void drmtap_debug_log(const drmtap_ctx *ctx, const char *fmt, ...) {
if (!ctx || !ctx->debug) {
return;
}
va_list ap;
va_start(ap, fmt);
fprintf(stderr, "[drmtap] ");
vfprintf(stderr, fmt, ap);
fprintf(stderr, "\n");
va_end(ap);
}
static int open_drm_auto(drmtap_ctx *ctx) {
char path[64];
const char *env_dev = NULL;
if (getuid() != 0 && geteuid() != 0) {
env_dev = getenv("DRM_DEVICE");
}
if (env_dev) {
drmtap_debug_log(ctx, "trying DRM_DEVICE=%s", env_dev);
int fd = open(env_dev, O_RDWR | O_CLOEXEC);
if (fd >= 0) {
snprintf(ctx->device_path, sizeof(ctx->device_path), "%s", env_dev);
return fd;
}
drmtap_debug_log(ctx, "DRM_DEVICE open failed: %s", strerror(errno));
}
int fallback_fd = -1;
char fallback_path[64] = {0};
int best_fd = -1;
int best_active = 0;
char best_path[64] = {0};
for (int i = 0; i < 16; i++) {
snprintf(path, sizeof(path), "/dev/dri/card%d", i);
int fd = open(path, O_RDWR | O_CLOEXEC);
if (fd < 0) {
continue;
}
drmtap_debug_log(ctx, "probing %s", path);
drmModeRes *res = drmModeGetResources(fd);
if (!res) {
drmtap_debug_log(ctx, " no KMS resources, skipping");
close(fd);
continue;
}
int active = 0;
for (int j = 0; j < res->count_crtcs; j++) {
drmModeCrtc *crtc = drmModeGetCrtc(fd, res->crtcs[j]);
if (crtc) {
if (crtc->mode_valid && crtc->buffer_id > 0) {
drmtap_debug_log(ctx, " CRTC %u active (%dx%d)",
crtc->crtc_id, crtc->width, crtc->height);
active++;
}
drmModeFreeCrtc(crtc);
}
}
drmModeFreeResources(res);
if (active > 0) {
if (active > best_active) {
if (best_fd >= 0) {
close(best_fd);
}
best_fd = fd;
best_active = active;
snprintf(best_path, sizeof(best_path), "%s", path);
} else {
close(fd);
}
continue;
}
if (fallback_fd < 0) {
fallback_fd = fd;
snprintf(fallback_path, sizeof(fallback_path), "%s", path);
drmtap_debug_log(ctx, " no active CRTC, saved as fallback");
} else {
close(fd);
}
}
if (best_fd >= 0) {
if (fallback_fd >= 0) {
close(fallback_fd);
}
snprintf(ctx->device_path, sizeof(ctx->device_path), "%s", best_path);
drmtap_debug_log(ctx, "using %s (%d active display(s))", best_path,
best_active);
return best_fd;
}
if (fallback_fd >= 0) {
snprintf(ctx->device_path, sizeof(ctx->device_path), "%s", fallback_path);
drmtap_debug_log(ctx, "using fallback device %s (no active CRTC found)", fallback_path);
return fallback_fd;
}
return -1;
}
static void detect_driver(drmtap_ctx *ctx) {
drmVersion *ver = drmGetVersion(ctx->drm_fd);
if (ver) {
snprintf(ctx->driver_name, sizeof(ctx->driver_name), "%.*s",
ver->name_len, ver->name);
drmtap_debug_log(ctx, "GPU driver: %s (v%d.%d.%d)",
ctx->driver_name, ver->version_major,
ver->version_minor, ver->version_patchlevel);
drmFreeVersion(ver);
} else {
drmtap_debug_log(ctx, "warning: could not get DRM version");
}
#ifdef HAVE_EGL
drmtap_debug_log(ctx, "EGL detile backend: compiled in");
#else
drmtap_debug_log(ctx,
"EGL detile backend: NOT COMPILED IN. The CPU path still decodes the "
"plain Intel tilings (X/Y/Yf-tiled) and linear "
"scanouts, but a COMPRESSED scanout, or any tiled layout it does not "
"decode, will fail closed -- and that is what current Intel and AMD "
"desktops scan out. Install libegl-dev and libgles2-mesa-dev, then "
"reconfigure with -Degl=enabled");
#endif
}
int drmtap_version(void) {
return (DRMTAP_VERSION_MAJOR << 16) |
(DRMTAP_VERSION_MINOR << 8) |
DRMTAP_VERSION_PATCH;
}
drmtap_ctx *drmtap_open(const drmtap_config *config) {
drmtap_ctx *ctx = calloc(1, sizeof(drmtap_ctx));
if (!ctx) {
drmtap_set_error(NULL, "Failed to allocate context: %s",
strerror(errno));
return NULL;
}
ctx->drm_fd = -1;
ctx->helper_pid = -1;
ctx->helper_fd = -1;
for (int i = 0; i < DRMTAP_FAST_SLOTS; i++) {
ctx->fast_slots[i].prime_fd = -1;
}
if (config) {
ctx->debug = config->debug;
ctx->crtc_id = config->crtc_id;
if (config->device_path) {
snprintf(ctx->device_path, sizeof(ctx->device_path),
"%s", config->device_path);
}
if (config->helper_path) {
snprintf(ctx->helper_path, sizeof(ctx->helper_path),
"%s", config->helper_path);
}
}
const char *dbg_env = getenv("DRMTAP_DEBUG");
if (dbg_env && dbg_env[0] == '1') {
ctx->debug = 1;
}
drmtap_debug_log(ctx, "drmtap v%d.%d.%d opening (pid=%d uid=%d)",
DRMTAP_VERSION_MAJOR, DRMTAP_VERSION_MINOR,
DRMTAP_VERSION_PATCH, getpid(), getuid());
drmtap_debug_log(ctx, "device_path=[%s] DRM_DEVICE=[%s]", ctx->device_path, getenv("DRM_DEVICE") ? getenv("DRM_DEVICE") : "(null)");
if (ctx->device_path[0]) {
ctx->drm_fd = open(ctx->device_path, O_RDWR | O_CLOEXEC);
if (ctx->drm_fd < 0) {
drmtap_set_error(NULL, "Failed to open %s: %s",
ctx->device_path, strerror(errno));
drmtap_debug_log(ctx, "open(%s) FAILED: %s", ctx->device_path, strerror(errno));
goto fail;
}
} else {
ctx->drm_fd = open_drm_auto(ctx);
if (ctx->drm_fd < 0) {
drmtap_set_error(NULL, "No DRM device found — is a GPU attached?");
goto fail;
}
}
{
int high_fd = fcntl(ctx->drm_fd, F_DUPFD_CLOEXEC, 100);
if (high_fd >= 0) {
close(ctx->drm_fd);
ctx->drm_fd = high_fd;
}
}
if (drmtap_have_cap_sys_admin() &&
drmDropMaster(ctx->drm_fd) == 0) {
drmtap_debug_log(ctx, "dropped implicit DRM master on %s", ctx->device_path);
}
detect_driver(ctx);
if (drmSetClientCap(ctx->drm_fd, DRM_CLIENT_CAP_UNIVERSAL_PLANES, 1) < 0) {
drmtap_debug_log(ctx,
"warning: DRM_CLIENT_CAP_UNIVERSAL_PLANES not supported");
}
if (drmSetClientCap(ctx->drm_fd, DRM_CLIENT_CAP_ATOMIC, 1) < 0) {
drmtap_debug_log(ctx,
"warning: DRM_CLIENT_CAP_ATOMIC not supported "
"(connector CRTC_ID fallback unavailable)");
}
if (drmSetClientCap(ctx->drm_fd, DRM_CLIENT_CAP_CURSOR_PLANE_HOTSPOT, 1) < 0) {
drmtap_debug_log(ctx,
"DRM_CLIENT_CAP_CURSOR_PLANE_HOTSPOT refused (%s); expected on a "
"non-virtualized driver",
strerror(errno));
}
drmtap_debug_log(ctx, "context opened: %s (%s)",
ctx->device_path, ctx->driver_name);
return ctx;
fail:
if (ctx->drm_fd >= 0) {
close(ctx->drm_fd);
}
free(ctx);
return NULL;
}
int drmtap_list_devices(drmtap_device *out, int max_count) {
if (!out || max_count <= 0) {
return -EINVAL;
}
int found = 0;
for (int i = 0; i < 16 && found < max_count; i++) {
char path[64];
snprintf(path, sizeof(path), "/dev/dri/card%d", i);
int fd = open(path, O_RDWR | O_CLOEXEC);
if (fd < 0) {
continue;
}
if (drmtap_have_cap_sys_admin()) {
drmDropMaster(fd);
}
drmModeRes *res = drmModeGetResources(fd);
if (!res) {
close(fd);
continue;
}
drmtap_device *dev = &out[found];
memset(dev, 0, sizeof(*dev));
snprintf(dev->path, sizeof(dev->path), "%s", path);
for (int j = 0; j < res->count_crtcs; j++) {
drmModeCrtc *crtc = drmModeGetCrtc(fd, res->crtcs[j]);
if (!crtc) {
continue;
}
if (crtc->mode_valid && crtc->buffer_id > 0) {
dev->display_count++;
}
drmModeFreeCrtc(crtc);
}
drmModeFreeResources(res);
drmVersionPtr ver = drmGetVersion(fd);
if (ver) {
if (ver->name) {
snprintf(dev->driver, sizeof(dev->driver), "%s", ver->name);
}
drmFreeVersion(ver);
}
drmDevicePtr dd = NULL;
if (drmGetDevice2(fd, 0, &dd) == 0 && dd) {
if ((dd->available_nodes & (1 << DRM_NODE_RENDER)) &&
dd->nodes[DRM_NODE_RENDER]) {
snprintf(dev->render_node, sizeof(dev->render_node), "%s",
dd->nodes[DRM_NODE_RENDER]);
}
drmFreeDevice(&dd);
}
close(fd);
found++;
}
return found;
}
static int read_sysfs_line(const char *path, char *buf, size_t len) {
int fd = open(path, O_RDONLY | O_CLOEXEC);
if (fd < 0) {
return -1;
}
ssize_t n = read(fd, buf, len - 1);
close(fd);
if (n <= 0) {
return -1;
}
buf[n] = '\0';
char *nl = strchr(buf, '\n');
if (nl) {
*nl = '\0';
}
return 0;
}
static int card_for_render_node(const char *render_name, char *out,
size_t out_len) {
char dir_path[320];
snprintf(dir_path, sizeof(dir_path), "/sys/class/drm/%s/device/drm",
render_name);
DIR *dir = opendir(dir_path);
if (!dir) {
return -1;
}
int found = -1;
const struct dirent *ent;
while ((ent = readdir(dir)) != NULL) {
if (strncmp(ent->d_name, "card", 4) != 0 || !ent->d_name[4] ||
strchr(ent->d_name, '-') != NULL) {
continue;
}
size_t len = strlen(ent->d_name);
if (len >= out_len) {
continue;
}
memcpy(out, ent->d_name, len + 1);
found = 0;
break;
}
closedir(dir);
return found;
}
static int card_output_rank(const char *card_name) {
DIR *dir = opendir("/sys/class/drm");
if (!dir) {
return 0;
}
size_t card_len = strlen(card_name);
int rank = 0;
const struct dirent *ent;
while ((ent = readdir(dir)) != NULL) {
if (strncmp(ent->d_name, card_name, card_len) != 0 ||
ent->d_name[card_len] != '-') {
continue;
}
char path[320], val[32];
snprintf(path, sizeof(path), "/sys/class/drm/%s/status", ent->d_name);
if (read_sysfs_line(path, val, sizeof(val)) != 0 ||
strcmp(val, "connected") != 0) {
continue;
}
if (rank < 1) {
rank = 1;
}
snprintf(path, sizeof(path), "/sys/class/drm/%s/enabled", ent->d_name);
if (read_sysfs_line(path, val, sizeof(val)) == 0 &&
strcmp(val, "enabled") == 0) {
rank = 2;
break;
}
}
closedir(dir);
return rank;
}
static int pick_scanout_render_node(char *out, size_t out_len) {
DIR *dir = opendir("/sys/class/drm");
if (!dir) {
return -1;
}
int best_rank = 0;
char best[64] = {0};
const struct dirent *ent;
while ((ent = readdir(dir)) != NULL) {
if (strncmp(ent->d_name, "renderD", 7) != 0) {
continue;
}
char card[64];
if (card_for_render_node(ent->d_name, card, sizeof(card)) != 0) {
continue;
}
size_t len = strlen(ent->d_name);
if (len >= sizeof(best)) {
continue;
}
int rank = card_output_rank(card);
if (rank > best_rank) {
best_rank = rank;
memcpy(best, ent->d_name, len + 1);
if (rank >= 2) {
break;
}
}
}
closedir(dir);
if (best_rank == 0) {
return -1;
}
snprintf(out, out_len, "/dev/dri/%s", best);
return 0;
}
const char *drmtap_render_node(drmtap_ctx *ctx) {
if (!ctx || ctx->drm_fd < 0) {
return NULL;
}
if (ctx->render_node[0]) {
return ctx->render_node;
}
drmDevicePtr dev = NULL;
if (drmGetDevice2(ctx->drm_fd, 0, &dev) != 0 || !dev) {
return NULL;
}
if ((dev->available_nodes & (1 << DRM_NODE_RENDER)) &&
dev->nodes[DRM_NODE_RENDER]) {
snprintf(ctx->render_node, sizeof(ctx->render_node), "%s",
dev->nodes[DRM_NODE_RENDER]);
}
drmFreeDevice(&dev);
return ctx->render_node[0] ? ctx->render_node : NULL;
}
drmtap_ctx *drmtap_open_render(const char *render_node) {
drmtap_ctx *ctx = calloc(1, sizeof(drmtap_ctx));
if (!ctx) {
drmtap_set_error(NULL, "Failed to allocate context: %s",
strerror(errno));
return NULL;
}
ctx->drm_fd = -1;
ctx->helper_pid = -1;
ctx->helper_fd = -1;
ctx->is_render_only = 1;
for (int i = 0; i < DRMTAP_FAST_SLOTS; i++) {
ctx->fast_slots[i].prime_fd = -1;
}
const char *dbg_env = getenv("DRMTAP_DEBUG");
if (dbg_env && dbg_env[0] == '1') {
ctx->debug = 1;
}
drmtap_debug_log(ctx, "drmtap v%d.%d.%d opening render-only (pid=%d uid=%d)",
DRMTAP_VERSION_MAJOR, DRMTAP_VERSION_MINOR,
DRMTAP_VERSION_PATCH, getpid(), getuid());
if (render_node && render_node[0]) {
snprintf(ctx->device_path, sizeof(ctx->device_path), "%s", render_node);
ctx->drm_fd = open(render_node, O_RDWR | O_CLOEXEC);
if (ctx->drm_fd < 0) {
drmtap_set_error(NULL, "Failed to open %s: %s",
render_node, strerror(errno));
goto fail;
}
} else {
char preferred[96];
if (pick_scanout_render_node(preferred, sizeof(preferred)) == 0) {
int fd = open(preferred, O_RDWR | O_CLOEXEC);
if (fd >= 0) {
snprintf(ctx->device_path, sizeof(ctx->device_path), "%s",
preferred);
ctx->drm_fd = fd;
drmtap_debug_log(ctx, "render node %s selected: its card "
"drives a display", preferred);
} else {
drmtap_debug_log(ctx, "render node %s drives a display but "
"could not be opened (%s); scanning",
preferred, strerror(errno));
}
}
}
if (ctx->drm_fd < 0 && !(render_node && render_node[0])) {
for (int i = 128; i < 128 + 64; i++) {
char path[64];
snprintf(path, sizeof(path), "/dev/dri/renderD%d", i);
int fd = open(path, O_RDWR | O_CLOEXEC);
if (fd >= 0) {
snprintf(ctx->device_path, sizeof(ctx->device_path),
"%s", path);
ctx->drm_fd = fd;
break;
}
}
if (ctx->drm_fd < 0) {
drmtap_set_error(NULL,
"No DRM render node found (/dev/dri/renderD*)");
goto fail;
}
}
{
int high_fd = fcntl(ctx->drm_fd, F_DUPFD_CLOEXEC, 100);
if (high_fd >= 0) {
close(ctx->drm_fd);
ctx->drm_fd = high_fd;
}
}
detect_driver(ctx);
drmtap_debug_log(ctx, "render context opened: %s (%s)",
ctx->device_path, ctx->driver_name);
return ctx;
fail:
if (ctx->drm_fd >= 0) {
close(ctx->drm_fd);
}
free(ctx);
return NULL;
}
void drmtap_close(drmtap_ctx *ctx) {
if (!ctx) {
return;
}
drmtap_debug_log(ctx, "closing context");
drmtap_fast_cleanup(ctx);
drmtap_gpu_egl_thread_cleanup();
drmtap_helper_stop(ctx);
if (ctx->drm_fd >= 0) {
close(ctx->drm_fd);
ctx->drm_fd = -1;
}
free(ctx->deswizzle_buf);
ctx->deswizzle_buf = NULL;
ctx->deswizzle_buf_size = 0;
free(ctx->pixel_buf);
ctx->pixel_buf = NULL;
ctx->pixel_buf_size = 0;
free(ctx);
}
const char *drmtap_error(drmtap_ctx *ctx) {
if (ctx) {
return ctx->error_msg[0] ? ctx->error_msg : NULL;
}
return g_static_error[0] ? g_static_error : NULL;
}
const char *drmtap_gpu_driver(drmtap_ctx *ctx) {
if (!ctx || !ctx->driver_name[0]) {
return NULL;
}
return ctx->driver_name;
}
int drmtap_drm_fd(drmtap_ctx *ctx) {
if (!ctx) {
return -1;
}
return ctx->drm_fd;
}
int drmtap_set_output_buffer(drmtap_ctx *ctx, void *dst, size_t len) {
if (!ctx) {
return -EINVAL;
}
if (!dst) {
ctx->user_out = NULL;
ctx->user_out_len = 0;
drmtap_debug_log(ctx, "output buffer cleared; conversions go back to the "
"library-owned buffer");
return 0;
}
if (len == 0) {
drmtap_set_error(ctx, "output buffer length is 0 "
"(pass dst=NULL to clear it instead)");
return -EINVAL;
}
ctx->user_out = dst;
ctx->user_out_len = len;
drmtap_debug_log(ctx, "output buffer set: %p (%zu bytes)", dst, len);
return 0;
}