Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
33 changes: 33 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,38 @@ the `libdrmtap` wrapper crate all share ONE version. 0.5.0 declared that move an
did not complete it - the wrapper still shipped 0.3.4 pinned to a `-sys` range that
could not reach 0.5.0 - so the shared line only actually holds from 0.5.1.

## [0.5.12] - 2026-10-07

Documentation only, no code change. The READMEs of both crates and of the repo, the public
header and the docs of the Rust wrapper said things the code does not do.

### Fixed: the tiled path needs Linux 5.7, not 4.20

The tiled and compressed path reads the framebuffer with `GETFB2`, which mainline added in
5.7 (455e00f1412f). Ubuntu 20.04 runs it as a backport in its 5.4 (LP: #1863874, since
5.4.0-16.19), which is why 20.04 works.

### Fixed: the layout of the pixels of a frame

`drmtap_grab_mapped()` returns `XRGB8888` once it converts a frame, but a linear 8-bit
scanout keeps its own order (an `XBGR8888` scanout is R, G, B in memory) and
`frame->format` names it. `drmtap_grab()` converts nothing. The docs said every frame was
BGRA.

### Fixed: the helper does not always hand over a DMA-BUF

Through the privileged helper a linear scanout on a GPU other than virtio-gpu is copied, not
exported, so `drmtap_grab()` returns its pixels with `dma_buf_fd` set to -1. The header, the
README of `libdrmtap-sys` and the docs of `grab()` and `data()` in the wrapper said it always
returned a DMA-BUF and no pixels.

### Added: how long `frame->data` lives

The doc of `drmtap_grab_mapped()` says that `frame->data` stays valid until the release when
`drmtap_frame_owns_data()` returns 1, and that the next grab on the context overwrites it when
it returns 0. The FP16 note of the README describes only the linear case, the one
`reduce_linear_to_xrgb8888` covers.

## [0.5.11] - 2026-10-05

### Fixed: the pixels of a mapped frame could change under it at the next grab
Expand Down Expand Up @@ -1069,6 +1101,7 @@ entry point is additive and would not on its own have justified more than a patc
- amdgpu EGL detile fix, privileged-helper hardening, and a batch of full-audit
fixes.

[0.5.12]: https://github.com/fxd0h/libdrmtap/releases/tag/v0.5.12
[0.5.11]: https://github.com/fxd0h/libdrmtap/releases/tag/v0.5.11
[0.5.10]: https://github.com/fxd0h/libdrmtap/releases/tag/v0.5.10
[0.5.9]: https://github.com/fxd0h/libdrmtap/releases/tag/v0.5.9
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
2 changes: 1 addition & 1 deletion bindings/rust/libdrmtap-sys/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "libdrmtap-sys"
version = "0.5.11"
version = "0.5.12"
links = "drmtap"
edition = "2021"
authors = ["Mariano Abad <weimaraner@gmail.com>"]
Expand Down
13 changes: 9 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,15 @@ 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
This is what `drmtap_grab_mapped()` returns; `drmtap_grab()` converts nothing and hands
the scanout over as it is. 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
22 changes: 17 additions & 5 deletions bindings/rust/libdrmtap-sys/csrc/drmtap.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ extern "C" {
* site. */
#define DRMTAP_VERSION_MAJOR 0
#define DRMTAP_VERSION_MINOR 5
#define DRMTAP_VERSION_PATCH 11
#define DRMTAP_VERSION_PATCH 12

/**
* @brief Get the library version as a packed integer.
Expand Down 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
4 changes: 2 additions & 2 deletions bindings/rust/libdrmtap/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "libdrmtap"
version = "0.5.11"
version = "0.5.12"
edition = "2021"
# std::os::fd (BorrowedFd/OwnedFd), which the frame's fd accessors are built on, is 1.66.
# Declared so a downstream on an older toolchain gets that sentence instead of a type error.
Expand All @@ -16,7 +16,7 @@ keywords = ["drm", "kms", "screen-capture", "wayland", "remote-desktop"]
categories = ["multimedia::video", "os::linux-apis"]

[dependencies]
libdrmtap-sys = { version = "0.5.11", path = "../libdrmtap-sys" }
libdrmtap-sys = { version = "0.5.12", path = "../libdrmtap-sys" }
# errno values differ by architecture (ENOTSUP is 95 on x86, 45 on SPARC): compare by name.
libc = "0.2"
# Optional on purpose: a third-party type in a public signature ties our semver to theirs. See the
Expand Down
16 changes: 11 additions & 5 deletions bindings/rust/libdrmtap/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ 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,
`grab_mapped()` returns 8-bit frames, 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,
and **HDR10** scanouts (PQ/BT.2020) are tone-mapped to SDR when the connector
reports HDR (`P010` overlay-video and HLG excepted).
reports HDR (`P010` overlay-video and HLG excepted). `grab()` converts nothing: its
frame is the scanout as it is, in the format `Frame::format()` names.

## ⚠️ Testing Status

Expand Down Expand Up @@ -65,7 +68,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 +108,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
22 changes: 17 additions & 5 deletions include/drmtap.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ extern "C" {
* site. */
#define DRMTAP_VERSION_MAJOR 0
#define DRMTAP_VERSION_MINOR 5
#define DRMTAP_VERSION_PATCH 11
#define DRMTAP_VERSION_PATCH 12

/**
* @brief Get the library version as a packed integer.
Expand Down 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
2 changes: 1 addition & 1 deletion meson.build
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# SPDX-License-Identifier: MIT

project('libdrmtap', 'c',
version: '0.5.11',
version: '0.5.12',
license: 'MIT',
default_options: [
'c_std=c11',
Expand Down
Loading