libdrmtap-sys 0.4.7

Raw FFI bindings for libdrmtap — DRM/KMS screen capture library for Linux. Includes embedded C sources compiled statically.
Documentation
/*
 * libdrmtap — DRM/KMS screen capture library for Linux
 * https://github.com/fxd0h/libdrmtap
 *
 * Copyright (c) 2026 Mariano Abad <weimaraner@gmail.com>
 * SPDX-License-Identifier: MIT
 */

/**
 * @file drmtap.c
 * @brief Context management, DRM device open, error handling, and debug logging
 */

#define _GNU_SOURCE
#define _POSIX_C_SOURCE 200809L

#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <stdarg.h>
#include <fcntl.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <pthread.h>

#include <xf86drm.h>
#include <xf86drmMode.h>

#include "drmtap_internal.h"

/* Error string for when ctx is NULL (from failed drmtap_open). Thread-local so
 * concurrent NULL-ctx failures on different threads don't race on one buffer. */
static _Thread_local char g_static_error[512] = "";

/* ========================================================================= */
/* Internal helpers (exported via drmtap_internal.h)                         */
/* ========================================================================= */

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(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);
}

/* ========================================================================= */
/* DRM device discovery                                                      */
/* ========================================================================= */

// Auto-detect DRM device: scan /dev/dri/card* for one with KMS resources
static int open_drm_auto(drmtap_ctx *ctx) {
    char path[64];

    /* Check DRM_DEVICE env var first */
    const char *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));
    }

    /* Two-pass scan of /dev/dri/card0..card15:
     *   Pass 1: find a device with an ACTIVE CRTC (monitor connected)
     *   Pass 2: fallback to any device with KMS resources
     */
    int fallback_fd = -1;
    char fallback_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;
        }

        /* Check for active CRTCs (monitor driving a display) */
        int has_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);
                    has_active = 1;
                }
                drmModeFreeCrtc(crtc);
                if (has_active) break;
            }
        }
        drmModeFreeResources(res);

        if (has_active) {
            /* Found the GPU driving a monitor — use this one */
            if (fallback_fd >= 0) close(fallback_fd);
            snprintf(ctx->device_path, sizeof(ctx->device_path), "%s", path);
            return fd;
        }

        /* Keep as fallback (first device with KMS but no active CRTC) */
        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);
        }
    }

    /* No device with active CRTC found — use fallback if available */
    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;
}

// Detect GPU driver name from the DRM fd
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");
    }
}

/* ========================================================================= */
/* Public API                                                                */
/* ========================================================================= */

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;
    }

    /* Parse config */
    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);
        }
    }

    /* Check DRMTAP_DEBUG env var */
    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());

    /* Open DRM device */
    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]) {
        /* Explicit device path */
        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 {
        /* Auto-detect */
        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;
        }
    }

    /* ── Protect fd from async runtime hijacking ──
     * Async runtimes (tokio, etc.) can close/reuse low-numbered fds.
     * Duplicate our DRM fd to a number >= 100 so it's safe. */
    {
        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 fcntl fails, keep the original fd (best effort) */
    }

    /* Detect GPU driver */
    detect_driver(ctx);

    /* Set universal planes — needed for cursor plane detection */
    if (drmSetClientCap(ctx->drm_fd, DRM_CLIENT_CAP_UNIVERSAL_PLANES, 1) < 0) {
        drmtap_debug_log(ctx,
                         "warning: DRM_CLIENT_CAP_UNIVERSAL_PLANES not supported");
    }

    drmtap_debug_log(ctx, "context opened: %s (%s)",
                     ctx->device_path, ctx->driver_name);

    /* TODO: Phase 3 — locate and spawn helper if needed */

    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");

    /* Clean up persistent fast-grab state */
    drmtap_fast_cleanup(ctx);

    /* Stop helper if running */
    drmtap_helper_stop(ctx);

    if (ctx->drm_fd >= 0) {
        close(ctx->drm_fd);
        ctx->drm_fd = -1;
    }

    /* Context-owned, reused-across-frames buffers (the deswizzle/EGL shadow and
     * the helper-mode pixel receive buffer) must be released here —
     * frame_release only drops per-frame state. */
    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;
}