Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ int drmtap_list_displays(drmtap_ctx *ctx, ...) { ... }
/**
* @brief Capture a frame with mapped pixel data.
*
* Returns a pointer to linear RGBA pixel data in frame->data.
* Returns linear 8-bit pixels in frame->data, laid out as frame->format.
* Handles GPU tiling → linear conversion automatically.
*
* @param ctx Capture context from drmtap_open()
Expand Down
15 changes: 8 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,10 @@ int main() {
drmtap_ctx *ctx = drmtap_open(NULL); // auto-detect GPU

drmtap_frame_info frame;
drmtap_grab_mapped(ctx, &frame); // capture screen → RGBA
drmtap_grab_mapped(ctx, &frame); // capture screen → linear 8-bit pixels

// frame.data = linear RGBA pixels
// frame.data = linear pixels, 4 bytes each, laid out as frame.format
// (XRGB8888 once converted; a linear 8-bit scanout keeps its own order)
// frame.width, frame.height, frame.stride

drmtap_frame_release(ctx, &frame);
Expand Down Expand Up @@ -168,17 +169,17 @@ println!("{}x{} pixels captured", frame.width(), frame.height());
> primary desktop scanout) is not handled.
>
> **FP16 half-float scanouts** (`XRGB16161616F` and its BGR/alpha siblings) are
> reduced to 8-bit sRGB on the CPU fallback — linear-light decode through the sRGB
> OETF, not HDR tone-mapped (values above 1.0 clip to white).
> reduced to 8-bit sRGB when the scanout is linear — linear-light decode through
> the sRGB OETF, not HDR tone-mapped (values above 1.0 clip to white).

## Quick Start

### Requirements

The tiled/compressed framebuffer path (Intel/AMD/Nvidia modifiers) uses the DRM
`GETFB2` ioctl, which needs **Linux 4.20+** — i.e. Ubuntu 20.04 (5.4), 22.04
(5.15), 24.04 (6.8) or newer. Linear framebuffers (virtio-gpu and similar VMs)
work on older kernels.
`GETFB2` ioctl, which mainline added in **Linux 5.7**. Ubuntu 20.04 (5.4) has it
backported; 22.04 (5.15), 24.04 (6.8) and newer have it. Linear framebuffers
(virtio-gpu and similar VMs) work on older kernels.

### Build

Expand Down
11 changes: 7 additions & 4 deletions bindings/rust/libdrmtap-sys/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@ libdrmtap captures screen contents at the kernel level using DRM/KMS APIs. Unlik

## Requirements

- Linux with DRM/KMS support (kernel 4.20+ for the tiled/modifier framebuffer
path, i.e. Ubuntu 20.04+; linear/VM framebuffers work on older kernels)
- Linux with DRM/KMS support (kernel 5.7+ for the tiled/modifier framebuffer
path, which needs `GETFB2`; Ubuntu 20.04 has it backported to its 5.4;
linear/VM framebuffers work on older kernels)
- A C compiler — the embedded C sources are compiled statically at build time.
There is **no** system `libdrmtap` install, `meson install`, or `pkg-config`
lookup of a shared library.
Expand All @@ -57,11 +58,13 @@ FORTIFY, PIE, full RELRO). Its path is exported to downstream build scripts as
`DEP_DRMTAP_HELPER_BIN` so a consumer (e.g. RustDesk) can copy and `setcap` it.
The library captures directly when it already has DRM master / `CAP_SYS_ADMIN`;
otherwise it spawns the helper over a socketpair to read other clients'
framebuffers, returning the scanout as a zero-copy DMA-BUF fd via `SCM_RIGHTS`.
framebuffers. The helper returns a tiled or virtio-gpu scanout as a zero-copy
DMA-BUF fd via `SCM_RIGHTS`, and copies the pixels of a linear one.

## Pixel output

Frames are returned as 8-bit `XRGB8888` (BGRA in memory). Tiled and compressed
Converted frames are 8-bit `XRGB8888` (BGRA in memory); a linear 8-bit scanout is
returned in its own order, which `format` names (e.g. `XBGR8888`). Tiled and compressed
framebuffers (Intel X/Y-tiled + CCS, AMD, Nvidia block-linear, virtio/virgl) are
GPU-detiled through an EGL/GLES2 backend, and **HDR10** scanouts (PQ / BT.2020 —
`AR30`/`XR30` and 16-bit `XR48`/`AR48`/`XB48`/`AB48`) are tone-mapped to SDR when
Expand Down
20 changes: 16 additions & 4 deletions bindings/rust/libdrmtap-sys/csrc/drmtap.h
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ typedef struct {
* a zero-copy grab -- that it happens to work on one GPU and returns NULL on
* another is exactly how issue #36 was mistaken for a driver bug. */
void *data;
int dma_buf_fd; /**< DMA-BUF fd (zero-copy) or -1 (mapped) */
int dma_buf_fd; /**< DMA-BUF fd (zero-copy), or -1 when the frame carries pixels */
uint32_t width; /**< Frame width in pixels */
uint32_t height; /**< Frame height in pixels */
uint32_t stride; /**< Bytes per row (may include padding) */
Expand All @@ -189,7 +189,13 @@ typedef struct {
* Returns a DMA-BUF file descriptor in frame->dma_buf_fd that can be
* passed directly to VAAPI/V4L2 encoders without copying pixel data.
*
* THERE ARE NO USABLE CPU PIXELS HERE. This call does no conversion and no detiling.
* One exception: through the privileged helper, a linear scanout on a GPU other
* than virtio-gpu is not exported. The helper copies its pixels, and the frame
* comes back with dma_buf_fd = -1 and frame->data pointing at those raw,
* unconverted pixels, valid until the next grab on the same context or
* drmtap_close().
*
* Otherwise THERE ARE NO USABLE CPU PIXELS HERE. This call does no conversion and no detiling.
* `frame->data` is whatever the raw scanout mapping happened to give: still tiled where
* the scanout is tiled, and NULL on a GPU whose scanout cannot be CPU-mapped at all
* (amdgpu GFX9+, discrete VRAM, nvidia) -- and the call still returns 0 in both cases.
Expand All @@ -211,8 +217,14 @@ int drmtap_grab(drmtap_ctx *ctx, drmtap_frame_info *frame);
/**
* @brief Capture a frame — mapped path.
*
* Returns a pointer to linear RGBA pixel data in frame->data.
* Handles GPU tiling → linear conversion automatically.
* Returns linear 8-bit pixels in frame->data, 4 bytes each, laid out as
* frame->format: XRGB8888 once converted (detiling, 10/16-bit and FP16
* reduction), or the scanout's own order for a linear 8-bit scanout (e.g.
* XBGR8888). Handles GPU tiling → linear conversion automatically.
*
* frame->data stays valid until drmtap_frame_release() when
* drmtap_frame_owns_data() returns 1. When it returns 0 the pixels are in
* memory the context reuses: the next grab on the same context overwrites them.
*
* @param ctx Capture context
* @param frame Output frame info (caller-allocated)
Expand Down
13 changes: 9 additions & 4 deletions bindings/rust/libdrmtap/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ Safe Rust wrapper for [libdrmtap](https://github.com/fxd0h/libdrmtap) — DRM/KM

Capture the screen at the kernel level: login screens, Wayland, headless — no user prompts.

Frames come back as 8-bit BGRA. Tiled/compressed framebuffers are GPU-detiled,
Frames come back as 8-bit, 4 bytes per pixel: BGRA (`XRGB8888`) once converted, or
a linear 8-bit scanout in its own order (`Frame::format()` says which, e.g.
`XBGR8888` is R, G, B in memory). Tiled/compressed framebuffers are GPU-detiled,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
and **HDR10** scanouts (PQ/BT.2020) are tone-mapped to SDR when the connector
reports HDR (`P010` overlay-video and HLG excepted).

Expand Down Expand Up @@ -65,7 +67,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
## Features

- **`DrmTap::open()`** — auto-detect GPU and display
- **`grab()`** — zero-copy DMA-BUF fd (for hardware encoders)
- **`grab()`** — zero-copy DMA-BUF fd (for hardware encoders). Through the helper, a
linear scanout on a GPU other than virtio-gpu comes back as copied pixels instead, with no
fd: `data()` has them, raw and unconverted
- **`grab_desc()`** (since 0.5.7) — the same zero-copy grab plus a `DmabufDesc`: the plane layout
(`num_planes`/`offsets`/`pitches`) and HDR state that a `Frame` does not carry. Without them a
compressed (Intel CCS) or HDR scanout cannot be imported at all, because you hold the fd and no
Expand Down Expand Up @@ -103,8 +107,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {

- Rust 1.66 or newer (`std::os::fd`, which the frame's descriptor accessors are built on). Declared
as `rust-version`, so an older toolchain says so instead of failing on a type
- Linux with DRM/KMS (kernel 4.20+ for the tiled/modifier path; linear/VM
framebuffers work on older kernels)
- Linux with DRM/KMS (kernel 5.7+ for the tiled/modifier path, which needs
`GETFB2`; Ubuntu 20.04 has it backported to its 5.4; linear/VM framebuffers
work on older kernels)
- A C compiler and the development packages `libdrmtap-sys` builds against. On
Debian/Ubuntu: `libdrm-dev libegl-dev libgles2-mesa-dev libseccomp-dev
libcap-dev`. libdrm, libseccomp and libcap are linked.
Expand Down
10 changes: 7 additions & 3 deletions bindings/rust/libdrmtap/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,10 @@ impl DrmTap {
unsafe { ffi::drmtap_displays_changed(self.ctx.0) != 0 }
}

/// Capture a frame (zero-copy — DMA-BUF fd only).
/// Capture a frame (zero-copy — a DMA-BUF fd, no conversion).
///
/// Through the privileged helper, a linear scanout on a GPU other than virtio-gpu is not
/// exported: the frame has no fd and `data()` holds a copy of its raw, unconverted pixels.
pub fn grab(&mut self) -> Result<Frame> {
let mut raw = unsafe { std::mem::zeroed::<ffi::drmtap_frame_info>() };
let ret = unsafe { ffi::drmtap_grab(self.ctx.0, &mut raw) };
Expand Down Expand Up @@ -634,8 +637,9 @@ impl Frame {

/// Access mapped pixel data as a byte slice.
///
/// Returns `None` if the frame was captured with `grab()` (zero-copy)
/// or if mmap failed.
/// After `grab_mapped()`, linear 8-bit pixels laid out as `format()`. After `grab()` this is
/// not converted pixel data: `None` where the scanout cannot be CPU-mapped, the raw (possibly
/// still tiled) mapping where it can, or the helper's copy of a linear scanout.
pub fn data(&self) -> Option<&[u8]> {
let len = self.raw.stride as usize * self.raw.height as usize;
if let Some(owned) = &self.owned {
Expand Down
20 changes: 16 additions & 4 deletions include/drmtap.h
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ typedef struct {
* a zero-copy grab -- that it happens to work on one GPU and returns NULL on
* another is exactly how issue #36 was mistaken for a driver bug. */
void *data;
int dma_buf_fd; /**< DMA-BUF fd (zero-copy) or -1 (mapped) */
int dma_buf_fd; /**< DMA-BUF fd (zero-copy), or -1 when the frame carries pixels */
uint32_t width; /**< Frame width in pixels */
uint32_t height; /**< Frame height in pixels */
uint32_t stride; /**< Bytes per row (may include padding) */
Expand All @@ -189,7 +189,13 @@ typedef struct {
* Returns a DMA-BUF file descriptor in frame->dma_buf_fd that can be
* passed directly to VAAPI/V4L2 encoders without copying pixel data.
*
* THERE ARE NO USABLE CPU PIXELS HERE. This call does no conversion and no detiling.
* One exception: through the privileged helper, a linear scanout on a GPU other
* than virtio-gpu is not exported. The helper copies its pixels, and the frame
* comes back with dma_buf_fd = -1 and frame->data pointing at those raw,
* unconverted pixels, valid until the next grab on the same context or
* drmtap_close().
*
* Otherwise THERE ARE NO USABLE CPU PIXELS HERE. This call does no conversion and no detiling.
* `frame->data` is whatever the raw scanout mapping happened to give: still tiled where
* the scanout is tiled, and NULL on a GPU whose scanout cannot be CPU-mapped at all
* (amdgpu GFX9+, discrete VRAM, nvidia) -- and the call still returns 0 in both cases.
Expand All @@ -211,8 +217,14 @@ int drmtap_grab(drmtap_ctx *ctx, drmtap_frame_info *frame);
/**
* @brief Capture a frame — mapped path.
*
* Returns a pointer to linear RGBA pixel data in frame->data.
* Handles GPU tiling → linear conversion automatically.
* Returns linear 8-bit pixels in frame->data, 4 bytes each, laid out as
* frame->format: XRGB8888 once converted (detiling, 10/16-bit and FP16
* reduction), or the scanout's own order for a linear 8-bit scanout (e.g.
* XBGR8888). Handles GPU tiling → linear conversion automatically.
*
* frame->data stays valid until drmtap_frame_release() when
* drmtap_frame_owns_data() returns 1. When it returns 0 the pixels are in
* memory the context reuses: the next grab on the same context overwrites them.
*
* @param ctx Capture context
* @param frame Output frame info (caller-allocated)
Expand Down
Loading