mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-08 13:31:03 +03:00
feat(drm): phase-2 split, pass the dma-buf fd instead of the converted frame
move the egl detile and rgba pack out of the root --service and into the unprivileged --server. the root now calls only drmtap_open + drmtap_grab_desc and exports a raw dma-buf fd; the fd rides the _drm channel over SCM_RIGHTS with a small descriptor (geometry, per-plane offsets/pitches, modifier, hdr) instead of the full rgba frame, dropping the per-frame copy. the --server imports the fd with drmtap_open_render + drmtap_convert_dmabuf, keyed by the import-once egl cache, and the render context is created and dropped on the recv thread. the _drm transport moves off Framed<BytesCodec> (which cannot carry a fd) to a bespoke sendmsg/recvmsg framing (DrmConn) that attaches one SCM_RIGHTS cmsg only when a fd is present and rejects a truncated ancillary message. the split symbols are bound optionally so an older libdrmtap still loads the cpu path, and the whole thing degrades to the cpu BGRA path or PipeWire when no render node is available. pins libdrmtap-sys to =0.4.13 with the Cargo.lock checksum. folds in the DP-MST, ldconfig-restart and per-display PipeWire-fallback review fixes and a udev hotplug refresh.
This commit is contained in:
@@ -11,7 +11,11 @@ edition = "2018"
|
||||
|
||||
[features]
|
||||
wayland = ["gstreamer", "gstreamer-app", "gstreamer-video", "dbus", "tracing", "zbus"]
|
||||
drm = []
|
||||
# `drm` pulls in the exact-pinned `libdrmtap-sys` crate (see the Linux target table below). rustdesk
|
||||
# still dlopen's `libdrmtap.so.0` at runtime (`drmtap_dl.rs`) rather than link-time linking it, so the
|
||||
# graceful PipeWire fallback when the .so/EGL is absent is preserved; the crate is pinned purely so its
|
||||
# committed Cargo.lock checksum supply-chain-pins the vendored source that build.py bundles.
|
||||
drm = ["dep:libdrmtap-sys"]
|
||||
mediacodec = ["ndk"]
|
||||
linux-pkg-config = ["dep:pkg-config"]
|
||||
hwcodec = ["dep:hwcodec"]
|
||||
@@ -59,6 +63,15 @@ gstreamer = { version = "0.16", optional = true }
|
||||
gstreamer-app = { version = "0.16", features = ["v1_10"], optional = true }
|
||||
gstreamer-video = { version = "0.16", optional = true }
|
||||
zbus = { version = "3.15", optional = true }
|
||||
# libdrmtap ABI, pinned EXACTLY (the leading `=` blocks ^0.4.x semver drift). This is the ONLY
|
||||
# supply-chain pin for libdrmtap: the committed Cargo.lock `checksum` for this exact version pins the
|
||||
# bytes, so `cargo build --locked` refuses a tampered/republished 0.4.13. It must move in lockstep
|
||||
# with build.py's `DRMTAP_REF = v0.4.13` (the bundled runtime .so) — the loader only checks ABI-major.
|
||||
# NOTE for the integrator: after this edit the lockfile MUST be regenerated + committed with
|
||||
# cargo update -p libdrmtap-sys --precise 0.4.13
|
||||
# so Cargo.lock gains the `[[package]] libdrmtap-sys 0.4.13 checksum=<sha256>` block (this stage does
|
||||
# not run cargo). `=0.4.13` alone is a version requirement, not a byte pin — the checksum is the pin.
|
||||
libdrmtap-sys = { version = "=0.4.13", optional = true }
|
||||
|
||||
[dependencies.hwcodec]
|
||||
git = "https://github.com/rustdesk-org/hwcodec"
|
||||
|
||||
@@ -10,12 +10,13 @@
|
||||
// before opening. The DRM_DEVICE env is intentionally NOT consulted here.
|
||||
|
||||
use super::drmtap_dl::{
|
||||
self, drmtap_config, drmtap_ctx, drmtap_cursor_info, drmtap_display, drmtap_frame_info,
|
||||
DrmtapLib,
|
||||
self, drmtap_config, drmtap_ctx, drmtap_cursor_info, drmtap_display, drmtap_dmabuf_desc,
|
||||
drmtap_frame_info, DrmtapLib,
|
||||
};
|
||||
use hbb_common::log;
|
||||
use std::ffi::CString;
|
||||
use std::io;
|
||||
use std::os::fd::{FromRawFd, OwnedFd};
|
||||
|
||||
// Largest scanout we will copy; also bounds w*4*h against overflow. 16384 covers
|
||||
// 8K+ with headroom; anything larger is rejected as a bogus/hostile geometry.
|
||||
@@ -215,6 +216,142 @@ impl DrmReader {
|
||||
}
|
||||
}
|
||||
|
||||
/// True if the loaded libdrmtap exposes the split-capture export entry point
|
||||
/// (`drmtap_grab_desc`, libdrmtap >= 0.4.9). When false the caller must use
|
||||
/// the CPU-mapped `grab()` path (an older `.so`).
|
||||
pub fn supports_grab_desc(&self) -> bool {
|
||||
self.lib.grab_desc.is_some()
|
||||
}
|
||||
|
||||
/// Zero-copy EXPORT grab for the split-capture path (root `--service`). Calls
|
||||
/// `drmtap_grab_desc`, which fills a `drmtap_dmabuf_desc` (the scanout dma-buf
|
||||
/// fd + the full plane layout + HDR metadata) WITHOUT mapping, detiling or
|
||||
/// copying any pixels — so on this path the root process never loads
|
||||
/// libEGL/libGLESv2 (the EGL convert now lives in the unprivileged `--server`).
|
||||
///
|
||||
/// The scanout `dma_buf_fd` is dup'd into an `OwnedFd` BEFORE the frame is
|
||||
/// released, so we keep an independently-owned reference to the buffer that
|
||||
/// survives `drmtap_frame_release` (the dma-buf refcount keeps the memory
|
||||
/// alive while the peer also holds a reference). The descriptor is validated
|
||||
/// on METADATA ONLY (no pixel access on the export side): the fourcc gate
|
||||
/// (kept from `grab()`), `MAX_DIM`, and `num_planes` in `1..=4`.
|
||||
///
|
||||
/// Returns the owned fd + the validated descriptor with `dma_buf_fd` reset to
|
||||
/// `-1` (the `OwnedFd` owns the fd now; the descriptor's local int must never
|
||||
/// be closed or re-used). Errno mapping mirrors `grab()`: EAGAIN/EBUSY/EINTR
|
||||
/// -> WouldBlock (retry); ENOTSUP -> a distinct `Unsupported` error (this
|
||||
/// seat/driver produced pixels but no transferable dma-buf) so the caller
|
||||
/// falls back to the mapped/PipeWire path instead of a per-frame rebuild loop;
|
||||
/// any other errno -> hard error. Errors when `grab_desc` is unbound (old .so).
|
||||
pub fn grab_desc(&mut self) -> io::Result<(OwnedFd, drmtap_dmabuf_desc)> {
|
||||
let grab_desc = self.lib.grab_desc.ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::Unsupported,
|
||||
"libdrmtap too old: drmtap_grab_desc unavailable (need >= 0.4.9)",
|
||||
)
|
||||
})?;
|
||||
// SAFETY: self.ctx is a valid context; desc/frame are zeroed before the
|
||||
// call and the frame is released on every return path (after the dup).
|
||||
unsafe {
|
||||
let mut desc: drmtap_dmabuf_desc = std::mem::zeroed();
|
||||
let mut frame: drmtap_frame_info = std::mem::zeroed();
|
||||
let ret = grab_desc(self.ctx, &mut desc, &mut frame);
|
||||
if ret < 0 {
|
||||
let errno = -ret;
|
||||
if errno == hbb_common::libc::EAGAIN
|
||||
|| errno == hbb_common::libc::EBUSY
|
||||
|| errno == hbb_common::libc::EINTR
|
||||
{
|
||||
return Err(io::ErrorKind::WouldBlock.into());
|
||||
}
|
||||
if errno == hbb_common::libc::ENOTSUP {
|
||||
// Pixels exist but there is no transferable dma-buf on this
|
||||
// seat/driver: the split export can never work here. A distinct
|
||||
// Unsupported error so the caller degrades (CPU-mapped/PipeWire)
|
||||
// rather than tight-looping a rebuild.
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Unsupported,
|
||||
"drmtap_grab_desc: no transferable dma-buf (ENOTSUP)",
|
||||
));
|
||||
}
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("drmtap_grab_desc failed: errno {errno}"),
|
||||
));
|
||||
}
|
||||
// The canonical fd is `desc.dma_buf_fd` (what split_capture.c sends);
|
||||
// `frame` also owns it and `frame_release` will close the library's
|
||||
// copy. A negative fd here means no new scanout this grab -> retry.
|
||||
let raw_fd = if desc.dma_buf_fd >= 0 {
|
||||
desc.dma_buf_fd
|
||||
} else {
|
||||
frame.dma_buf_fd
|
||||
};
|
||||
if raw_fd < 0 {
|
||||
(self.lib.frame_release)(self.ctx, &mut frame);
|
||||
return Err(io::ErrorKind::WouldBlock.into());
|
||||
}
|
||||
// ---- METADATA-ONLY validation (no pixel access on the export side) ----
|
||||
let w = desc.width;
|
||||
let h = desc.height;
|
||||
if w == 0 || h == 0 || w > MAX_DIM || h > MAX_DIM {
|
||||
(self.lib.frame_release)(self.ctx, &mut frame);
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("DRM scanout geometry {w}x{h} out of range"),
|
||||
));
|
||||
}
|
||||
// fourcc gate (see grab()): reject a scanout the converter could not
|
||||
// present as BGRA. 0/unknown is allowed here — an older .so may not set
|
||||
// it, and the converter reads `frame.format` authoritatively per frame.
|
||||
const DRM_FORMAT_XRGB8888: u32 = 0x3432_5258; // 'XR24'
|
||||
const DRM_FORMAT_ARGB8888: u32 = 0x3432_5241; // 'AR24'
|
||||
if desc.format != 0
|
||||
&& desc.format != DRM_FORMAT_XRGB8888
|
||||
&& desc.format != DRM_FORMAT_ARGB8888
|
||||
{
|
||||
log::warn!(
|
||||
"DRM scanout fourcc {:#010x} is not BGRA-compatible; falling back",
|
||||
desc.format
|
||||
);
|
||||
(self.lib.frame_release)(self.ctx, &mut frame);
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"unsupported DRM scanout format",
|
||||
));
|
||||
}
|
||||
// num_planes must index offsets/pitches (0 is treated as 1 per the ABI).
|
||||
let planes = if desc.num_planes == 0 { 1 } else { desc.num_planes };
|
||||
if planes > 4 {
|
||||
(self.lib.frame_release)(self.ctx, &mut frame);
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("DRM scanout num_planes {} out of range (1..=4)", desc.num_planes),
|
||||
));
|
||||
}
|
||||
// dup the fd into an OwnedFd BEFORE releasing the frame: after release
|
||||
// the library may recycle its handle, but our dup (an independent fd on
|
||||
// the same open dma-buf) keeps the buffer alive for the peer.
|
||||
let dup_fd = hbb_common::libc::dup(raw_fd);
|
||||
if dup_fd < 0 {
|
||||
let e = io::Error::last_os_error();
|
||||
(self.lib.frame_release)(self.ctx, &mut frame);
|
||||
return Err(e);
|
||||
}
|
||||
let owned = OwnedFd::from_raw_fd(dup_fd);
|
||||
// Release now that the fd is safely dup'd (split_capture.c releases only
|
||||
// after the send; we release after the dup, which is equivalent because
|
||||
// the dup holds its own reference to the dma-buf).
|
||||
(self.lib.frame_release)(self.ctx, &mut frame);
|
||||
// Normalize num_planes and blank the descriptor's local fd int: the
|
||||
// OwnedFd owns the fd, and the wire descriptor carries `has_fd` + the
|
||||
// ancillary fd, never this integer.
|
||||
desc.num_planes = planes;
|
||||
desc.dma_buf_fd = -1;
|
||||
Ok((owned, desc))
|
||||
}
|
||||
}
|
||||
|
||||
/// Read the hardware cursor plane. Returns a hidden sentinel when the plane
|
||||
/// reports the cursor invisible, the real shape when visible, or None on a
|
||||
/// read error / unsupported cursor. Ported from the old drm.rs update_cursor.
|
||||
|
||||
195
libs/scrap/src/common/drm_render.rs
Normal file
195
libs/scrap/src/common/drm_render.rs
Normal file
@@ -0,0 +1,195 @@
|
||||
// Unprivileged (`--server`) render-side converter for the split DRM/KMS capture
|
||||
// path. This is the OTHER half of the split introduced with libdrmtap >= 0.4.9:
|
||||
// the root `--service` now only EXPORTS a scanout dma-buf fd + a small metadata
|
||||
// descriptor (see `drm_reader::grab_desc`), and THIS side imports that fd and does
|
||||
// the EGL detile / RGBA convert. Because the convert lives here, libEGL/libGLESv2
|
||||
// are dlopen'd in the UNPRIVILEGED process, never in the privileged root service.
|
||||
//
|
||||
// A `RenderConverter` wraps a `drmtap_open_render(NULL)` render-node context and
|
||||
// converts one imported dma-buf per `convert()` call. The EGL context and the
|
||||
// import-once EGLImage cache it holds are THREAD-LOCAL inside libdrmtap: the
|
||||
// context MUST be created, used (`convert`), and closed (`drop`) on the SAME
|
||||
// thread (the consumer's `recv_thread`). Dropping it off-thread would strand the
|
||||
// cached EGLImages — the exact leak class behind the 0.4.8 OOM regression. The raw
|
||||
// ctx pointer makes `RenderConverter` !Send/!Sync, which enforces that at the type
|
||||
// level.
|
||||
|
||||
use super::drmtap_dl::{self, drmtap_ctx, drmtap_dmabuf_desc, drmtap_frame_info, DrmtapLib};
|
||||
use super::Pixfmt;
|
||||
use hbb_common::log;
|
||||
use std::io;
|
||||
use std::os::fd::RawFd;
|
||||
|
||||
// DRM fourccs of the 32-bit linear formats libdrmtap's convert can emit. XRGB/ARGB
|
||||
// are little-endian B,G,R,{X,A} in memory == our `Pixfmt::BGRA`; XBGR/ABGR are
|
||||
// R,G,B,{X,A} == `Pixfmt::RGBA`. libdrmtap normalizes the EGL path to XRGB8888, but
|
||||
// we read `frame.format` per frame so a CPU-fallback convert that keeps the source
|
||||
// channel order is still presented correctly (not hardcoded BGRA).
|
||||
const DRM_FORMAT_XRGB8888: u32 = 0x3432_5258; // 'XR24'
|
||||
const DRM_FORMAT_ARGB8888: u32 = 0x3432_5241; // 'AR24'
|
||||
const DRM_FORMAT_XBGR8888: u32 = 0x3432_4258; // 'XB24'
|
||||
const DRM_FORMAT_ABGR8888: u32 = 0x3432_4241; // 'AB24'
|
||||
|
||||
// Same geometry / size guards as the export side (`drm_reader`), applied to the
|
||||
// convert OUTPUT so a malformed `frame_info` cannot make us build an out-of-range
|
||||
// slice from the context-owned pointer. 16384 covers 8K+; 256 MiB covers an 8K
|
||||
// BGRA frame (7680x4320x4 ~= 127 MiB) with margin.
|
||||
const MAX_DIM: u32 = 16384;
|
||||
const MAX_FRAME_BYTES: usize = 256 * 1024 * 1024;
|
||||
|
||||
/// An unprivileged DRM render-node convert context (`drmtap_open_render`). Imports a
|
||||
/// scanout dma-buf (received over SCM_RIGHTS) and EGL-detiles it to linear pixels.
|
||||
/// Deliberately !Send/!Sync (the raw ctx pointer): the context and libdrmtap's
|
||||
/// thread-local EGL state must stay on ONE thread for the context's whole life
|
||||
/// (create + convert + close).
|
||||
pub struct RenderConverter {
|
||||
lib: &'static DrmtapLib,
|
||||
ctx: *mut drmtap_ctx,
|
||||
}
|
||||
|
||||
impl RenderConverter {
|
||||
/// Open an unprivileged DRM render-node convert context (`drmtap_open_render(NULL)`
|
||||
/// auto-selects a render node — it opens no KMS card, spawns no helper, and needs
|
||||
/// no elevated capability). Returns `None` when libdrmtap is unavailable, the split
|
||||
/// convert symbols are missing (a pre-0.4.9 `.so`), or no render node could be
|
||||
/// opened (a locked-down seat with no `/dev/dri/renderD*` access) — the caller then
|
||||
/// degrades to the CPU-mapped / PipeWire path. MUST be called on the thread that
|
||||
/// will later `convert()` and drop it.
|
||||
pub fn open_render() -> Option<RenderConverter> {
|
||||
let lib = drmtap_dl::get()?;
|
||||
// The converter needs BOTH split symbols; bail (so the caller degrades) if either
|
||||
// is absent, rather than open a ctx we could never convert with.
|
||||
let open_render = lib.open_render?;
|
||||
if lib.convert_dmabuf.is_none() {
|
||||
log::info!(
|
||||
"libdrmtap exposes drmtap_open_render but not drmtap_convert_dmabuf; \
|
||||
cannot convert dma-buf frames (old .so)"
|
||||
);
|
||||
return None;
|
||||
}
|
||||
// SAFETY: `open_render` is a resolved C entry point; NULL requests auto-selection
|
||||
// of a render node.
|
||||
let ctx = unsafe { open_render(std::ptr::null()) };
|
||||
if ctx.is_null() {
|
||||
log::info!("drmtap_open_render(NULL) failed; no usable DRM render node");
|
||||
return None;
|
||||
}
|
||||
log::info!("drm: opened unprivileged render-node convert context");
|
||||
Some(RenderConverter { lib, ctx })
|
||||
}
|
||||
|
||||
/// Import + convert one scanout dma-buf. `desc` is the descriptor rebuilt from the
|
||||
/// wire `DmabufDesc`; `received_fd` is the fd number this process obtained via
|
||||
/// SCM_RIGHTS (or `-1` for an import-once cache hit, where libdrmtap reuses the
|
||||
/// EGLImage it already holds for `desc.fb_id`). The fd is written into
|
||||
/// `desc.dma_buf_fd` before the call (LOAD-BEARING: the integer the exporter
|
||||
/// serialized was process-local and never crossed the wire).
|
||||
///
|
||||
/// On success returns a borrow of the CONTEXT-OWNED linear pixels plus the frame
|
||||
/// width/height and the `Pixfmt` read from `frame.format`. The slice covers
|
||||
/// `stride * height` bytes, so the caller can recover the (possibly padded) row
|
||||
/// stride as `data.len() / height`. It is valid ONLY until the next `convert()`
|
||||
/// (or drop) — do NOT free it and do NOT call `drmtap_frame_release` on it
|
||||
/// (libdrmtap owns it). The `&mut self` borrow keeps the slice from outliving the
|
||||
/// next convert; copy it out (into the latest-wins slot) before the next call.
|
||||
pub fn convert(
|
||||
&mut self,
|
||||
desc: &mut drmtap_dmabuf_desc,
|
||||
received_fd: RawFd,
|
||||
) -> io::Result<(&[u8], u32, u32, Pixfmt)> {
|
||||
let convert_dmabuf = self.lib.convert_dmabuf.ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::Unsupported,
|
||||
"libdrmtap too old: drmtap_convert_dmabuf unavailable (need >= 0.4.9)",
|
||||
)
|
||||
})?;
|
||||
// Overwrite the descriptor's fd with the one THIS process received (split_capture.c
|
||||
// does the same at recv time). -1 means "reuse the cached import for `fb_id`".
|
||||
desc.dma_buf_fd = received_fd;
|
||||
// SAFETY: self.ctx is a valid render context; `desc` points to a fully-initialized
|
||||
// descriptor; `frame` is zeroed before the call. libdrmtap OWNS the returned
|
||||
// `frame.data` (no release/free from this side, per drmtap.h).
|
||||
unsafe {
|
||||
let mut frame: drmtap_frame_info = std::mem::zeroed();
|
||||
let ret = convert_dmabuf(self.ctx, &*desc as *const drmtap_dmabuf_desc, &mut frame);
|
||||
if ret < 0 {
|
||||
let errno = -ret;
|
||||
// Transient contention (device busy, interrupted syscall) -> retry rather
|
||||
// than tear the stream down.
|
||||
if errno == hbb_common::libc::EAGAIN
|
||||
|| errno == hbb_common::libc::EBUSY
|
||||
|| errno == hbb_common::libc::EINTR
|
||||
{
|
||||
return Err(io::ErrorKind::WouldBlock.into());
|
||||
}
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("drmtap_convert_dmabuf failed: errno {errno}"),
|
||||
));
|
||||
}
|
||||
if frame.data.is_null() || frame.width == 0 || frame.height == 0 || frame.stride == 0 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"drmtap_convert_dmabuf produced an empty frame",
|
||||
));
|
||||
}
|
||||
let w = frame.width;
|
||||
let h = frame.height;
|
||||
let stride = frame.stride as usize;
|
||||
// Guard the slice we are about to build from a hostile/garbage `frame_info`:
|
||||
// reject an insane geometry or a stride below 32bpp (would under-size the row
|
||||
// and, read as BGRA downstream, disclose adjacent memory).
|
||||
if w > MAX_DIM || h > MAX_DIM || stride < (w as usize) * 4 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!(
|
||||
"drmtap_convert_dmabuf bad geometry {w}x{h} stride {stride} fourcc {:#010x}",
|
||||
frame.format
|
||||
),
|
||||
));
|
||||
}
|
||||
let len = match stride.checked_mul(h as usize) {
|
||||
Some(sz) if sz > 0 && sz <= MAX_FRAME_BYTES => sz,
|
||||
other => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("drmtap_convert_dmabuf frame size out of range ({other:?} bytes)"),
|
||||
));
|
||||
}
|
||||
};
|
||||
// Channel order from the ACTUAL convert output (do NOT hardcode BGRA): the EGL
|
||||
// path normalizes to XRGB8888 (BGRA), but reading it keeps any other emitted
|
||||
// order labeled correctly for the encoder.
|
||||
let pixfmt = match frame.format {
|
||||
DRM_FORMAT_XRGB8888 | DRM_FORMAT_ARGB8888 => Pixfmt::BGRA,
|
||||
DRM_FORMAT_XBGR8888 | DRM_FORMAT_ABGR8888 => Pixfmt::RGBA,
|
||||
// Unset by an older convert -> libdrmtap's normalized BGRA.
|
||||
0 => Pixfmt::BGRA,
|
||||
other => {
|
||||
log::debug!(
|
||||
"drm: convert output fourcc {other:#010x} unrecognized; presenting as BGRA"
|
||||
);
|
||||
Pixfmt::BGRA
|
||||
}
|
||||
};
|
||||
// Borrow the context-owned pixels. The returned lifetime is tied to `&mut self`
|
||||
// (elision), so the borrow cannot outlive the next `convert()` that would
|
||||
// overwrite these bytes.
|
||||
let data = std::slice::from_raw_parts(frame.data as *const u8, len);
|
||||
Ok((data, w, h, pixfmt))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for RenderConverter {
|
||||
fn drop(&mut self) {
|
||||
if !self.ctx.is_null() {
|
||||
// SAFETY: ctx came from drmtap_open_render and is non-null. This MUST run on the
|
||||
// same thread that created and used it (thread-local EGL state + cached imports);
|
||||
// guaranteed because the !Send ctx pointer keeps the whole `RenderConverter` on
|
||||
// the owning `recv_thread`, where it is also dropped.
|
||||
unsafe { (self.lib.close)(self.ctx) };
|
||||
self.ctx = std::ptr::null_mut();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -68,6 +68,46 @@ pub struct drmtap_frame_info {
|
||||
pub _priv: *mut c_void,
|
||||
}
|
||||
|
||||
// Descriptor of an externally-supplied scanout DMA-BUF (the split-capture
|
||||
// contract). Mirrors `drmtap_dmabuf_desc` in libdrmtap include/drmtap.h EXACTLY
|
||||
// (field order + widths); a mismatch mis-reads CCS/HDR scanouts. The privileged
|
||||
// exporter fills it in one call via `drmtap_grab_desc`; the unprivileged
|
||||
// converter receives it over IPC, overwrites `dma_buf_fd` with the fd it got via
|
||||
// SCM_RIGHTS, and passes it to `drmtap_convert_dmabuf`.
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct drmtap_dmabuf_desc {
|
||||
pub dma_buf_fd: c_int, // scanout DMA-BUF; -1 for an already-imported fb_id
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub format: u32, // DRM fourcc of the scanout
|
||||
pub modifier: u64, // DRM format modifier (tiling/compression)
|
||||
pub fb_id: u32, // import-once cache key; 0 disables caching
|
||||
pub num_planes: u32, // used entries in offsets/pitches (1..4); 0 => 1
|
||||
pub offsets: [u32; 4], // per-plane byte offsets (CCS main+aux+clear-color)
|
||||
pub pitches: [u32; 4], // per-plane strides; pitches[0] = main stride
|
||||
pub hdr_eotf: u32, // DRMTAP_EOTF_* (SDR=0, PQ=2, HLG=3)
|
||||
pub hdr_max_nits: u32, // mastering/content peak luminance cd/m2; 0=unknown
|
||||
}
|
||||
|
||||
impl Default for drmtap_dmabuf_desc {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
dma_buf_fd: -1,
|
||||
width: 0,
|
||||
height: 0,
|
||||
format: 0,
|
||||
modifier: 0,
|
||||
fb_id: 0,
|
||||
num_planes: 0,
|
||||
offsets: [0; 4],
|
||||
pitches: [0; 4],
|
||||
hdr_eotf: 0,
|
||||
hdr_max_nits: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct drmtap_cursor_info {
|
||||
pub x: i32,
|
||||
@@ -91,6 +131,14 @@ type FnGrabMapped = unsafe extern "C" fn(*mut drmtap_ctx, *mut drmtap_frame_info
|
||||
type FnFrameRelease = unsafe extern "C" fn(*mut drmtap_ctx, *mut drmtap_frame_info);
|
||||
type FnGetCursor = unsafe extern "C" fn(*mut drmtap_ctx, *mut drmtap_cursor_info) -> c_int;
|
||||
type FnCursorRelease = unsafe extern "C" fn(*mut drmtap_ctx, *mut drmtap_cursor_info);
|
||||
// Split-capture entry points (libdrmtap >= 0.4.9). Bound OPTIONALLY (see below).
|
||||
// `grab_desc` runs on the privileged export side; `open_render`/`convert_dmabuf`
|
||||
// on the unprivileged converter side.
|
||||
type FnGrabDesc =
|
||||
unsafe extern "C" fn(*mut drmtap_ctx, *mut drmtap_dmabuf_desc, *mut drmtap_frame_info) -> c_int;
|
||||
type FnOpenRender = unsafe extern "C" fn(*const c_char) -> *mut drmtap_ctx;
|
||||
type FnConvertDmabuf =
|
||||
unsafe extern "C" fn(*mut drmtap_ctx, *const drmtap_dmabuf_desc, *mut drmtap_frame_info) -> c_int;
|
||||
|
||||
/// The dlopen'd libdrmtap with its resolved entry points. The `Library` is kept
|
||||
/// alive for the process lifetime (this lives in a `OnceLock`), so the raw fn
|
||||
@@ -104,6 +152,15 @@ pub struct DrmtapLib {
|
||||
pub frame_release: FnFrameRelease,
|
||||
pub get_cursor: FnGetCursor,
|
||||
pub cursor_release: FnCursorRelease,
|
||||
// Split-capture symbols (present only on libdrmtap >= 0.4.9). `None` on an
|
||||
// older .so; callers gate on `Some(..)` and fall back to the mapped path.
|
||||
// Root needs `grab_desc`; the unprivileged converter needs
|
||||
// `open_render` + `convert_dmabuf`.
|
||||
pub grab_desc: Option<FnGrabDesc>,
|
||||
pub open_render: Option<FnOpenRender>,
|
||||
pub convert_dmabuf: Option<FnConvertDmabuf>,
|
||||
// Parsed (major, minor, patch) from `drmtap_version()`, for feature gating.
|
||||
pub version: (c_int, c_int, c_int),
|
||||
}
|
||||
|
||||
// SAFETY: the resolved fn pointers are plain C entry points with no interior
|
||||
@@ -152,6 +209,14 @@ impl DrmtapLib {
|
||||
let frame_release: FnFrameRelease = *lib.get(b"drmtap_frame_release").ok()?;
|
||||
let get_cursor: FnGetCursor = *lib.get(b"drmtap_get_cursor").ok()?;
|
||||
let cursor_release: FnCursorRelease = *lib.get(b"drmtap_cursor_release").ok()?;
|
||||
// Split-capture symbols are bound OPTIONALLY (not through the `.ok()?`
|
||||
// chain above): a pre-0.4.9 .so lacks them, and forcing them here would
|
||||
// fail the WHOLE load and silently disable DRM. Each side checks the
|
||||
// symbol it needs before taking the split path.
|
||||
let grab_desc: Option<FnGrabDesc> = lib.get(b"drmtap_grab_desc").ok().map(|s| *s);
|
||||
let open_render: Option<FnOpenRender> = lib.get(b"drmtap_open_render").ok().map(|s| *s);
|
||||
let convert_dmabuf: Option<FnConvertDmabuf> =
|
||||
lib.get(b"drmtap_convert_dmabuf").ok().map(|s| *s);
|
||||
Some(DrmtapLib {
|
||||
_lib: lib,
|
||||
open,
|
||||
@@ -161,6 +226,10 @@ impl DrmtapLib {
|
||||
frame_release,
|
||||
get_cursor,
|
||||
cursor_release,
|
||||
grab_desc,
|
||||
open_render,
|
||||
convert_dmabuf,
|
||||
version: (major, minor, patch),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ cfg_if! {
|
||||
pub mod drmtap_dl;
|
||||
#[cfg(all(target_os = "linux", feature = "drm"))]
|
||||
pub mod drm_reader;
|
||||
#[cfg(all(target_os = "linux", feature = "drm"))]
|
||||
pub mod drm_render;
|
||||
pub use self::linux::*;
|
||||
pub use self::wayland::set_map_err;
|
||||
pub use self::x11::PixelBuffer;
|
||||
|
||||
Reference in New Issue
Block a user