This document is designed to be read by both AI coding agents and human developers. It defines how to work on libdrmtap in a way that is clear, predictable, and reviewable.
What is libdrmtap? A C library for capturing the Linux screen by reading the active scanout framebuffer directly via DRM/KMS. No Wayland portal, no PipeWire, no consent dialog — works under X11, Wayland, headless, and VMs.
Key files to read first:
docs/research/05_api_and_architecture.md— API design and architecturedocs/research/02_drm_kms_mechanism.md— How DRM/KMS capture worksdocs/research/08_reframe_egl_analysis.md— EGL GPU-universal detiling (the primary detile path)docs/research/06_github_issues_analysis.md— Known gotchas
Build system: Meson
Language: C11
License: MIT
Tests: unit (no hardware) + integration via VKMS (Virtual KMS) or a real GPU
Version: ONE shared line across the C library, meson, libdrmtap-sys and libdrmtap (since 0.5.1). The canonical number lives in include/drmtap.h (DRMTAP_VERSION_*); tools/check-version.sh verifies every site. Deliberately not repeated here, because a copy of it goes stale on every release
// ✅ CORRECT: snake_case, 4 spaces, braces on same line
int drmtap_grab_mapped(drmtap_ctx *ctx, drmtap_frame_info *frame) {
if (!ctx || !frame) {
return -EINVAL;
}
int ret = refresh_plane_state(ctx);
if (ret < 0) {
set_error(ctx, "Failed to refresh plane: %s", strerror(-ret));
return ret;
}
return 0;
}
// ❌ WRONG: camelCase, tabs, K&R braces
int drmtapGrabMapped(DrmtapCtx *ctx, DrmtapFrameInfo *frame)
{
if (!ctx || !frame) return -EINVAL;
}| Rule | Convention |
|---|---|
| Indent | 4 spaces, never tabs |
| Naming: functions | snake_case, prefixed drmtap_ for public API |
| Naming: variables | snake_case |
| Naming: macros/constants | UPPER_SNAKE_CASE, prefixed DRMTAP_ |
| Naming: types | snake_case_t for internal, drmtap_* for public |
| Braces | Same line (1TBS style) |
| Line length | 100 chars soft limit, 120 hard limit |
| Comments | // for single-line, /* */ for multi-line |
| Header guards | #ifndef DRMTAP_MODULE_H / #define DRMTAP_MODULE_H |
| Error handling | Return negative errno values, never abort |
| Memory | Always check malloc returns, always free in cleanup |
// Every .c and .h file MUST start with this header block:
/*
* 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 drm_enumerate.c
* @brief Plane, CRTC, and connector enumeration via DRM/KMS
*/
// After the header block, the file follows this structure:
// 1. Includes (grouped: system, library, project)
#include <stdio.h> // system
#include <xf86drm.h> // library
#include "drmtap.h" // project
// 2. Private types and constants
#define MAX_PLANES 32
// 3. Static (private) functions
static int find_primary_plane(int drm_fd, uint32_t *plane_id) { ... }
// 4. Public functions (matching header declaration order)
int drmtap_list_displays(drmtap_ctx *ctx, ...) { ... }
// Public API functions in drmtap.h use Doxygen comments:
/**
* @brief Capture a frame with mapped pixel data.
*
* Returns a pointer to linear RGBA pixel data in frame->data.
* Handles GPU tiling → linear conversion automatically.
*
* @param ctx Capture context from drmtap_open()
* @param frame Output frame info (caller-allocated)
* @return 0 on success, negative errno on error
* @retval -EACCES Helper not found or not configured
* @retval -ENODEV Display disconnected or CRTC inactive
*/
int drmtap_grab_mapped(drmtap_ctx *ctx, drmtap_frame_info *frame);
// Internal static functions use simple // comments:
// Refresh plane fb_id from the kernel (never cache!)
static int refresh_plane_state(drmtap_ctx *ctx) { ... }component: short description (imperative mood)
Longer explanation of what and why (not how).
Reference issues with #NNN.
Examples:
grab: refresh plane fb_id on every frame
helper: export scanout as DMA-BUF and pass fd via SCM_RIGHTS
gpu-egl: detile CCS framebuffers via EGLImage import
tests: add vkms enumeration test
docs: update GPU compatibility table
Prefixes: grab, enumerate, formats, cursor, helper, gpu-egl, gpu-intel, gpu-amd, gpu-nvidia, gpu-generic, tests, docs, build, ci.
Every change must satisfy ALL of these before merge:
meson setup build && meson compile -C build
# Zero warnings with -Wall -Wextra -Werrormeson test -C build
# All existing tests pass- Any new function should have a corresponding test
- Exception: GPU-specific code that requires real hardware (document this)
- Public API changes → update
include/drmtap.hcomments - New gotchas discovered → update
docs/research/06_github_issues_analysis.md - Architecture changes → update
docs/research/05_api_and_architecture.md
- Never crash, never abort
- Return error codes (negative errno)
- Set human-readable error via
set_error(ctx, "...") - Clean up resources on error paths (goto cleanup pattern)
// ✅ CORRECT error handling pattern
int drmtap_grab(drmtap_ctx *ctx, drmtap_frame_info *frame) {
int ret;
int prime_fd = -1;
void *mapped = MAP_FAILED;
ret = get_framebuffer(ctx, &prime_fd);
if (ret < 0) {
goto cleanup;
}
mapped = mmap(NULL, size, PROT_READ, MAP_SHARED, prime_fd, 0);
if (mapped == MAP_FAILED) {
ret = -errno;
set_error(ctx, "mmap failed: %s", strerror(errno));
goto cleanup;
}
// ... use mapped ...
ret = 0;
cleanup:
if (mapped != MAP_FAILED) munmap(mapped, size);
if (prime_fd >= 0) close(prime_fd);
return ret;
}tests/
├── test_enumerate.c # Plane/CRTC enumeration
├── test_formats.c # Pixel format detection and conversion
├── test_capture.c # Frame capture (requires vkms)
├── test_helper.c # Privileged helper IPC
├── test_deswizzle.c # Tiling format conversion (unit, no GPU)
├── test_connector_names.c # Connector naming
├── test_convert.c # Pixel conversion paths
├── test_cursor.c # Cursor helper-fallback classification (unit)
├── test_refresh.c # Exact CRTC refresh fraction (unit, no GPU)
├── test_hdr.c # HDR metadata
├── test_outbuf.c # Output buffer growth and caps
├── test_scanout.c # Scanout geometry
└── test_wire.c # Helper wire protocol
# Unit tests (no hardware needed)
meson test -C build --suite unit
# Integration tests (need a DRM device): enumerate, capture. CONTRIBUTING.md has
# which card they open and when the capture test really grabs.
sudo modprobe vkms
meson test -C build --suite integrationThere are two suites only: unit and integration. There is no separate
gpu suite.
// Tests use simple assert-based validation
// No external test framework needed
#include <assert.h>
#include <stdio.h>
#include "drmtap.h"
static void test_open_close(void) {
drmtap_config cfg = {0};
drmtap_ctx *ctx = drmtap_open(&cfg);
assert(ctx != NULL);
const char *driver = drmtap_gpu_driver(ctx);
assert(driver != NULL);
printf(" driver: %s\n", driver);
drmtap_close(ctx);
printf(" PASS: test_open_close\n");
}
int main(void) {
printf("Running enumeration tests...\n");
test_open_close();
printf("All tests passed!\n");
return 0;
}[component] Short description
Examples:
[grab] Refresh fb_id on every frame to fix double-buffering skip[gpu-amd] Add SDMA copy fallback for tiled framebuffers[docs] Document Intel CCS workaround
## What
Brief description of what this PR does.
## Why
What problem does it solve? Link to issue if applicable.
## How
High-level explanation of the approach.
## Testing
- [ ] Compiles with `meson compile -C build` (zero warnings)
- [ ] All existing tests pass
- [ ] New tests added (if applicable)
- [ ] Tested on: [hardware/driver/distro]
## Checklist
- [ ] Follows code style (AGENTS.md)
- [ ] Error handling uses goto-cleanup pattern
- [ ] Public API documented in header comments
- [ ] No memory leaks (checked cleanup paths)
## AI Disclosure (optional)
If AI tools were used, briefly mention which and for what.
This is optional and has no effect on PR review.- Read the research docs — especially
05_api_and_architecture.mdand06_github_issues_analysis.md - Check the gotcha checklist in
06_github_issues_analysis.md— don't re-introduce known bugs - Understand the architecture layers — don't mix concerns between grab/enumerate/convert/helper
- Follow the error handling pattern —
goto cleanup, never abort - Never cache
fb_id— always refresh viadrmModeGetPlane()each frame - Detect
handles[0] == 0— this means CAP_SYS_ADMIN is missing, trigger helper - Use Prime path (not GEM_FLINK) — GEM_FLINK doesn't work with vkms
- Check modifier for tiling —
DRM_FORMAT_MOD_LINEARmeans no deswizzle needed - EGL is the primary detile path — tiled/compressed FBs (Intel CCS, AMD, Nvidia
block-linear, vendor modifiers) go through
gpu_egl.c(import DMA-BUF as EGLImage, draw,glReadPixelsto linear RGBA). CPU deswizzle is only a fallback for some formats. - HDR10 is tone-mapped to SDR — when the connector reports HDR
(
HDR_OUTPUT_METADATA, PQ),AR30/XR30and 16-bitXR48/AR48/XB48/AB48scanouts are PQ-decoded, BT.2020 → BT.709 gamut-mapped, tone-mapped and sRGB-encoded to 8-bit, in both the CPU and the EGL (tiled) paths. Plain SDR 10-bit gets a straight bit-depth reduction.P010(overlay-video YUV) and HLG are not tone-mapped — don't claim those as HDR-supported.
- vkms is LINEAR only — don't test tiling/deswizzle against vkms
- vkms needs a compositor — bare vkms has no active planes
- Use DRM_DEVICE env var — never hardcode
/dev/dri/card0
- English always
- Include date and sources in research docs
- Update the gotcha checklist when discovering new issues
- Link to kernel docs or upstream sources when referencing DRM APIs
- One concern per PR — don't mix GPU backends with API changes
- Include test results — even if just "tested with vkms on Ubuntu 24.04"
- Reference the research docs when your change is based on a finding
libdrmtap/
├── AGENTS.md ← YOU ARE HERE (agent + human guidelines)
├── README.md ← Project overview
├── LICENSE ← MIT
├── CONTRIBUTING.md ← How to contribute
├── SECURITY.md ← Threat model + helper hardening
├── meson.build ← Build system (its own project() version; parses
│ include/drmtap.h only to fail on drift)
├── include/
│ └── drmtap.h ← Public API (the only public header)
├── src/
│ ├── drmtap.c ← Main context management + lifecycle
│ ├── drmtap_internal.h ← Internal shared declarations (NOT installed)
│ ├── drm_enumerate.c ← Plane/CRTC/connector enumeration
│ ├── drm_grab.c ← Scanout framebuffer capture (V3 zero-copy / V2 fallback)
│ ├── cursor.c ← Hardware cursor plane: image, position, hotspot
│ ├── pixel_convert.c ← CPU deswizzle + format conversion (fallback path)
│ ├── gpu_egl.c ← GPU-universal EGL/GLES2 detiler (PRIMARY detile path)
│ ├── gpu_intel.c ← Intel i915/xe tiling modifiers (CCS/X/Y-tiled)
│ ├── gpu_amd.c ← AMD amdgpu tiling (RX560 here, RX Vega 64 by a tester)
│ ├── gpu_nvidia.c ← Nvidia: linear passthrough, block-linear -> -ENOTSUP (no CPU decoder)
│ ├── gpu_generic.c ← Generic/VM linear backend (virtio-gpu, simple VMs)
│ ├── privilege_helper.c ← Helper spawn + SCM_RIGHTS / DMA-BUF passing
│ └── privilege_helper_stub.c ← The same four entry points. Spawn, grab and
│ get_cursor return -EACCES; stop is a void no-op,
│ because drmtap_close calls it unconditionally.
│ Selected by -Dhelper=disabled, which leaves no
│ fork, exec or search path in the .so. Keep its
│ signatures in step with privilege_helper.c.
├── helper/
│ └── drmtap-helper.c ← Privileged helper binary (CAP_SYS_ADMIN, seccomp-hardened)
├── tests/
│ ├── test_enumerate.c ← integration suite (needs DRM device)
│ ├── test_capture.c ← integration suite (needs vkms or real GPU)
│ ├── test_formats.c ← unit suite
│ ├── test_helper.c ← unit suite
│ ├── test_deswizzle.c ← unit suite
│ ├── ... ← eight more unit suites, see the Testing section
│ └── lsan.supp ← LeakSanitizer suppressions
├── examples/
│ ├── screenshot.c ← Capture one frame → PPM on stdout
│ ├── stream.c ← Continuous capture
│ └── vnc_server.c ← VNC demo (optional libvncserver)
├── bindings/
│ └── rust/ ← libdrmtap-sys (FFI, embeds C sources + helper) + libdrmtap (safe)
├── docs/
│ ├── AI_DEVELOPMENT.md ← AI development philosophy
│ ├── README.md ← Docs index
│ └── research/ ← 9 technical research documents (00–08)
└── .github/
├── workflows/ci.yml ← Build & Test (Ubuntu 22.04/24.04), cppcheck, Rust crate,
version + csrc coherence, CodeQL
├── ISSUE_TEMPLATE/
│ ├── bug_report.md
│ └── feature_request.md
└── PULL_REQUEST_TEMPLATE.md