diff --git a/AGENTS.md b/AGENTS.md index e97b3ac..70718ea 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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() diff --git a/CHANGELOG.md b/CHANGELOG.md index 583cd9c..f6a0b7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 @@ -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 diff --git a/README.md b/README.md index b05a821..fea6610 100644 --- a/README.md +++ b/README.md @@ -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); @@ -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 diff --git a/bindings/rust/libdrmtap-sys/Cargo.toml b/bindings/rust/libdrmtap-sys/Cargo.toml index 95341f8..d6a19e0 100644 --- a/bindings/rust/libdrmtap-sys/Cargo.toml +++ b/bindings/rust/libdrmtap-sys/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "libdrmtap-sys" -version = "0.5.11" +version = "0.5.12" links = "drmtap" edition = "2021" authors = ["Mariano Abad "] diff --git a/bindings/rust/libdrmtap-sys/README.md b/bindings/rust/libdrmtap-sys/README.md index 8e490c5..bb49bef 100644 --- a/bindings/rust/libdrmtap-sys/README.md +++ b/bindings/rust/libdrmtap-sys/README.md @@ -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. @@ -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 diff --git a/bindings/rust/libdrmtap-sys/csrc/drmtap.h b/bindings/rust/libdrmtap-sys/csrc/drmtap.h index 26ce352..6c74c67 100644 --- a/bindings/rust/libdrmtap-sys/csrc/drmtap.h +++ b/bindings/rust/libdrmtap-sys/csrc/drmtap.h @@ -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. @@ -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) */ @@ -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. @@ -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) diff --git a/bindings/rust/libdrmtap/Cargo.toml b/bindings/rust/libdrmtap/Cargo.toml index 6f8fcb3..f70a01c 100644 --- a/bindings/rust/libdrmtap/Cargo.toml +++ b/bindings/rust/libdrmtap/Cargo.toml @@ -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. @@ -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 diff --git a/bindings/rust/libdrmtap/README.md b/bindings/rust/libdrmtap/README.md index 69ff7bc..5de0411 100644 --- a/bindings/rust/libdrmtap/README.md +++ b/bindings/rust/libdrmtap/README.md @@ -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 @@ -65,7 +68,9 @@ fn main() -> Result<(), Box> { ## 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 @@ -103,8 +108,9 @@ fn main() -> Result<(), Box> { - 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. diff --git a/bindings/rust/libdrmtap/src/lib.rs b/bindings/rust/libdrmtap/src/lib.rs index a9b87e9..f78df8a 100644 --- a/bindings/rust/libdrmtap/src/lib.rs +++ b/bindings/rust/libdrmtap/src/lib.rs @@ -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 { let mut raw = unsafe { std::mem::zeroed::() }; let ret = unsafe { ffi::drmtap_grab(self.ctx.0, &mut raw) }; @@ -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 { diff --git a/include/drmtap.h b/include/drmtap.h index 26ce352..6c74c67 100644 --- a/include/drmtap.h +++ b/include/drmtap.h @@ -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. @@ -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) */ @@ -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. @@ -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) diff --git a/meson.build b/meson.build index 9db1b5b..c67c786 100644 --- a/meson.build +++ b/meson.build @@ -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',