mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-19 10:51:01 +03:00
Merge branch 'master' into custom-client-no-rebuild
This commit is contained in:
@@ -43,7 +43,7 @@ once_cell = {version = "1.18", optional = true}
|
||||
percent-encoding = {version ="2.3", optional = true}
|
||||
x11-clipboard = {git="https://github.com/clslaid/x11-clipboard", branch = "feat/store-batch", optional = true}
|
||||
x11rb = {version = "0.12", features = ["all-extensions"], optional = true}
|
||||
fuser = {version = "0.15", default-features = false, optional = true}
|
||||
fuser = {git="https://github.com/rustdesk-org/fuser", branch = "refact/tag-0.16.0-cargo-1.75.0", default-features = false, optional = true}
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
cacao = {git="https://github.com/clslaid/cacao", branch = "feat/set-file-urls", optional = true}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -42,6 +42,13 @@ impl Enigo {
|
||||
&mut self.custom_mouse
|
||||
}
|
||||
|
||||
/// Override the display server guessed in `Default::default`: on "x11" every method here
|
||||
/// routes to `xdo`, and a null xdo context makes all of them silent no-ops. A caller
|
||||
/// installing custom devices knows better than the guess.
|
||||
pub fn set_is_x11(&mut self, is_x11: bool) {
|
||||
self.is_x11 = is_x11;
|
||||
}
|
||||
|
||||
/// Clear remapped keycodes
|
||||
pub fn tfc_clear_remapped(&mut self) {
|
||||
if let Some(tfc) = &mut self.tfc {
|
||||
@@ -390,3 +397,52 @@ fn test_key_seq() {
|
||||
let mut en = Enigo::new();
|
||||
en.key_sequence("^^");
|
||||
}
|
||||
|
||||
/// Both directions: the failure is silent, so a one-directional test passes against the bug.
|
||||
#[test]
|
||||
fn test_custom_mouse_dispatch_follows_is_x11() {
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
struct CountingMouse(Arc<AtomicUsize>);
|
||||
impl MouseControllable for CountingMouse {
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
fn as_mut_any(&mut self) -> &mut dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
fn mouse_move_to(&mut self, _x: i32, _y: i32) {
|
||||
self.0.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
fn mouse_move_relative(&mut self, _x: i32, _y: i32) {}
|
||||
fn mouse_down(&mut self, _button: MouseButton) -> crate::ResultType {
|
||||
Ok(())
|
||||
}
|
||||
fn mouse_up(&mut self, _button: MouseButton) {}
|
||||
fn mouse_click(&mut self, _button: MouseButton) {}
|
||||
fn mouse_scroll_x(&mut self, _length: i32) {}
|
||||
fn mouse_scroll_y(&mut self, _length: i32) {}
|
||||
}
|
||||
|
||||
let calls = Arc::new(AtomicUsize::new(0));
|
||||
let mut en = Enigo::new();
|
||||
en.set_custom_mouse(Box::new(CountingMouse(calls.clone())));
|
||||
|
||||
en.set_is_x11(false);
|
||||
en.mouse_move_to(10, 20);
|
||||
assert_eq!(
|
||||
calls.load(Ordering::Relaxed),
|
||||
1,
|
||||
"custom mouse was not reached on the non-x11 branch"
|
||||
);
|
||||
|
||||
// Negative control: on the x11 branch the custom device must be bypassed entirely.
|
||||
en.set_is_x11(true);
|
||||
en.mouse_move_to(30, 40);
|
||||
assert_eq!(
|
||||
calls.load(Ordering::Relaxed),
|
||||
1,
|
||||
"custom mouse was reached on the x11 branch"
|
||||
);
|
||||
}
|
||||
|
||||
Submodule libs/hbb_common updated: 69cea8dafe...b2b1ac453d
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rustdesk-portable-packer"
|
||||
version = "1.4.9"
|
||||
version = "1.5.0"
|
||||
edition = "2021"
|
||||
description = "RustDesk Remote Desktop"
|
||||
|
||||
|
||||
@@ -11,6 +11,16 @@ edition = "2018"
|
||||
|
||||
[features]
|
||||
wayland = ["gstreamer", "gstreamer-app", "gstreamer-video", "dbus", "tracing", "zbus"]
|
||||
# `drm` is a pure runtime-dlopen backend: rustdesk loads `libdrmtap.so.0` at runtime (`drmtap_dl.rs`)
|
||||
# and NEVER link-time links it, so the graceful PipeWire fallback when the .so or EGL is absent is
|
||||
# preserved and the drm build pulls in no libdrm/seccomp/cap/EGL link-time deps. The .so is pinned by
|
||||
# `DRMTAP_SHA` in build.py, which fetches that exact commit (libdrmtap v0.5.4). We deliberately do
|
||||
# NOT depend on the `libdrmtap-sys` crate: its build.rs statically compiles the whole libdrmtap C tree
|
||||
# and a CAP_SYS_ADMIN helper and emits `-ldrm -lseccomp -lcap`, which would defeat the dlopen model.
|
||||
# Depends on `wayland`: the three drm modules live inside the `#[cfg(feature = "wayland")]` arm of
|
||||
# common/mod.rs, so `scrap/drm` on its own would compile nothing. The root crate happens to always
|
||||
# enable `scrap/wayland`, which is what hid this.
|
||||
drm = ["wayland", "hbb_common/wayland_probe"]
|
||||
mediacodec = ["ndk"]
|
||||
linux-pkg-config = ["dep:pkg-config"]
|
||||
hwcodec = ["dep:hwcodec"]
|
||||
|
||||
477
libs/scrap/src/common/drm_reader.rs
Normal file
477
libs/scrap/src/common/drm_reader.rs
Normal file
@@ -0,0 +1,477 @@
|
||||
// Service-side DRM/KMS read engine, in the ROOT `--service`: libdrmtap reads the scanout in-process (direct mode). The DRM_DEVICE env is not consulted here.
|
||||
|
||||
use super::drmtap_dl::{
|
||||
self, drmtap_config, drmtap_ctx, drmtap_cursor_info, drmtap_device, 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};
|
||||
|
||||
// Trust-boundary limits and formats `drm_render` (the unprivileged converter) imports: two copies that drift apart would weaken one side.
|
||||
// 16384 covers 8K+ with headroom; anything larger is rejected as a bogus/hostile geometry.
|
||||
pub(crate) const MAX_DIM: u32 = 16384;
|
||||
// 256 MiB covers an 8K BGRA frame (7680x4320x4 ~= 127 MiB) with margin.
|
||||
pub(crate) const MAX_FRAME_BYTES: usize = 256 * 1024 * 1024;
|
||||
// XRGB/ARGB are little-endian B,G,R,{X,A} in memory == `Pixfmt::BGRA`; XBGR/ABGR are R,G,B,{X,A} == `Pixfmt::RGBA`.
|
||||
pub(crate) const DRM_FORMAT_XRGB8888: u32 = 0x3432_5258; // 'XR24'
|
||||
pub(crate) const DRM_FORMAT_ARGB8888: u32 = 0x3432_5241; // 'AR24'
|
||||
pub(crate) const DRM_FORMAT_XBGR8888: u32 = 0x3432_4258; // 'XB24'
|
||||
pub(crate) const DRM_FORMAT_ABGR8888: u32 = 0x3432_4241; // 'AB24'
|
||||
|
||||
/// Cursor id published when the plane reports the cursor hidden, so the id changes and, where the DRM cursor is authoritative, the client drops the last shape.
|
||||
pub const HIDDEN_CURSOR_ID: u64 = u64::MAX;
|
||||
|
||||
pub struct CursorSnapshot {
|
||||
pub id: u64,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub hotx: i32,
|
||||
pub hoty: i32,
|
||||
pub colors: Vec<u8>,
|
||||
}
|
||||
|
||||
/// One enumerated DRM display, physical geometry only (the server overlays the Wayland logical origin/scale where it can match one).
|
||||
pub struct DisplaySnapshot {
|
||||
pub name: String,
|
||||
pub crtc_id: u32,
|
||||
pub x: i32,
|
||||
pub y: i32,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub active: bool,
|
||||
}
|
||||
|
||||
pub struct DrmDevice {
|
||||
pub path: String,
|
||||
/// Render node, or empty if this device has none.
|
||||
pub render_node: String,
|
||||
pub display_count: u32,
|
||||
}
|
||||
|
||||
/// Copy a fixed C char array into a `String`, stopping at the first NUL WITHIN the array, so a
|
||||
/// field libdrmtap failed to terminate cannot read past it.
|
||||
fn cstr_field(buf: &[std::os::raw::c_char]) -> String {
|
||||
// SAFETY: c_char and u8 share size/alignment; the slice is the exact length of `buf`.
|
||||
let bytes: &[u8] =
|
||||
unsafe { std::slice::from_raw_parts(buf.as_ptr() as *const u8, buf.len()) };
|
||||
let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
|
||||
String::from_utf8_lossy(&bytes[..end]).into_owned()
|
||||
}
|
||||
|
||||
/// Enumerate every DRM device with KMS resources. `None` = unavailable, too old, or failed (the caller then scans /dev/dri/card* itself); empty `Vec` = none found.
|
||||
pub fn list_devices() -> Option<Vec<DrmDevice>> {
|
||||
let lib = drmtap_dl::get()?;
|
||||
let f = lib.list_devices?;
|
||||
const MAX: usize = 16;
|
||||
let mut raw: [drmtap_device; MAX] = unsafe { std::mem::zeroed() };
|
||||
// SAFETY: `raw` is MAX valid, zeroed drmtap_device slots; the call fills up to MAX and returns the count.
|
||||
let n = unsafe { f(raw.as_mut_ptr(), MAX as std::os::raw::c_int) };
|
||||
if n < 0 {
|
||||
log::warn!("drmtap_list_devices failed ({n}); using single-device auto-detect");
|
||||
return None;
|
||||
}
|
||||
let n = (n as usize).min(MAX);
|
||||
Some(
|
||||
raw[..n]
|
||||
.iter()
|
||||
.map(|d| DrmDevice {
|
||||
path: cstr_field(&d.path),
|
||||
render_node: cstr_field(&d.render_node),
|
||||
display_count: d.display_count,
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
/// The CANONICAL path, when `path` canonicalizes to a node directly under /dev/dri/, else `None`.
|
||||
/// Callers must open the value returned: opening the original re-resolves every symlink component after the check.
|
||||
pub(super) fn device_under_dev_dri(path: &str) -> Option<std::path::PathBuf> {
|
||||
let p = std::fs::canonicalize(path).ok()?;
|
||||
if p.parent() == Some(std::path::Path::new("/dev/dri")) {
|
||||
Some(p)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// An open DRM read context. Not Send/Sync deliberately (the raw ctx is used on one thread).
|
||||
pub struct DrmReader {
|
||||
lib: &'static DrmtapLib,
|
||||
ctx: *mut drmtap_ctx,
|
||||
buf: Vec<u8>,
|
||||
}
|
||||
|
||||
impl DrmReader {
|
||||
/// Open the DRM device. `device = None` auto-detects, `Some(path)` is realpath-gated to /dev/dri/. `crtc_id = 0` auto-selects the first active CRTC.
|
||||
pub fn open(device: Option<&str>, crtc_id: u32) -> Option<DrmReader> {
|
||||
let lib = drmtap_dl::get()?;
|
||||
let device_cstr = match device {
|
||||
None => None,
|
||||
Some(d) => {
|
||||
let Some(canonical) = device_under_dev_dri(d) else {
|
||||
log::warn!("DRM device {d:?} is not under /dev/dri; refusing to open");
|
||||
return None;
|
||||
};
|
||||
match canonical.to_str().and_then(|s| CString::new(s).ok()) {
|
||||
Some(c) => Some(c),
|
||||
None => return None,
|
||||
}
|
||||
}
|
||||
};
|
||||
let cfg = drmtap_config {
|
||||
device_path: device_cstr.as_ref().map_or(std::ptr::null(), |c| c.as_ptr()),
|
||||
crtc_id,
|
||||
helper_path: std::ptr::null(),
|
||||
debug: 0,
|
||||
};
|
||||
// SAFETY: cfg is a valid struct; device_cstr outlives this call.
|
||||
let ctx = unsafe { (lib.open)(&cfg) };
|
||||
drop(device_cstr);
|
||||
if ctx.is_null() {
|
||||
log::info!("drmtap_open failed; DRM capture unavailable");
|
||||
return None;
|
||||
}
|
||||
Some(DrmReader {
|
||||
lib,
|
||||
ctx,
|
||||
buf: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Grab one frame, tightly packed as BGRA (`w*4*h` bytes), into the internal buffer; valid until the next grab.
|
||||
pub fn grab(&mut self) -> io::Result<(&[u8], usize, usize)> {
|
||||
// SAFETY: ctx is valid; frame is zeroed before the call. The frame is released on every return path that OWNS one: a failing
|
||||
// `drmtap_grab_mapped` leaves nothing to release, and releasing anyway would be a double free.
|
||||
unsafe {
|
||||
let mut frame: drmtap_frame_info = std::mem::zeroed();
|
||||
let ret = (self.lib.grab_mapped)(self.ctx, &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());
|
||||
}
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("drmtap_grab_mapped failed: errno {errno}"),
|
||||
));
|
||||
}
|
||||
if frame.data.is_null() || frame.width == 0 || frame.height == 0 {
|
||||
(self.lib.frame_release)(self.ctx, &mut frame);
|
||||
return Err(io::ErrorKind::WouldBlock.into());
|
||||
}
|
||||
let w = frame.width;
|
||||
let h = frame.height;
|
||||
let stride = frame.stride as usize;
|
||||
// The row copy reads w*4 bytes from a source only stride*height bytes: reject sub-32bpp / insane geometry to avoid an OOB read.
|
||||
if w > MAX_DIM || h > MAX_DIM || stride < (w as usize) * 4 {
|
||||
log::warn!(
|
||||
"DRM scanout not 32-bit BGRA-compatible ({w}x{h} stride {stride} fourcc {:#010x}); falling back",
|
||||
frame.format
|
||||
);
|
||||
(self.lib.frame_release)(self.ctx, &mut frame);
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"unsupported DRM scanout format",
|
||||
));
|
||||
}
|
||||
// XBGR8888 passes the stride check but, labeled BGRA downstream, would ship red and blue swapped; a zero fourcc falls through to the stride invariant (kept for libdrmtap builds that do not set it).
|
||||
if frame.format != 0
|
||||
&& frame.format != DRM_FORMAT_XRGB8888
|
||||
&& frame.format != DRM_FORMAT_ARGB8888
|
||||
{
|
||||
log::warn!(
|
||||
"DRM scanout fourcc {:#010x} is not BGRA-compatible; falling back",
|
||||
frame.format
|
||||
);
|
||||
(self.lib.frame_release)(self.ctx, &mut frame);
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"unsupported DRM scanout format",
|
||||
));
|
||||
}
|
||||
let (w, h) = (w as usize, h as usize);
|
||||
let frame_size = match w.checked_mul(4).and_then(|x| x.checked_mul(h)) {
|
||||
Some(sz) if sz > 0 && sz <= MAX_FRAME_BYTES => sz,
|
||||
other => {
|
||||
log::warn!(
|
||||
"DRM scanout geometry {w}x{h} yields an out-of-range frame ({other:?} bytes); falling back"
|
||||
);
|
||||
(self.lib.frame_release)(self.ctx, &mut frame);
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"DRM scanout frame too large",
|
||||
));
|
||||
}
|
||||
};
|
||||
// Bound the SOURCE extent too: the row loop reads up to (h-1)*stride + w*4, and `y * stride` can overflow.
|
||||
match stride.checked_mul(h) {
|
||||
Some(sz) if sz > 0 && sz <= MAX_FRAME_BYTES => {}
|
||||
other => {
|
||||
log::warn!(
|
||||
"DRM scanout stride {stride} x {h} rows is out of range ({other:?} bytes); falling back"
|
||||
);
|
||||
(self.lib.frame_release)(self.ctx, &mut frame);
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"DRM scanout stride out of range",
|
||||
));
|
||||
}
|
||||
}
|
||||
if self.buf.len() != frame_size {
|
||||
self.buf.resize(frame_size, 0);
|
||||
}
|
||||
let src = frame.data as *const u8;
|
||||
let dst = self.buf.as_mut_ptr();
|
||||
if stride == w * 4 {
|
||||
std::ptr::copy_nonoverlapping(src, dst, frame_size);
|
||||
} else {
|
||||
for y in 0..h {
|
||||
std::ptr::copy_nonoverlapping(src.add(y * stride), dst.add(y * w * 4), w * 4);
|
||||
}
|
||||
}
|
||||
(self.lib.frame_release)(self.ctx, &mut frame);
|
||||
Ok((&self.buf, w, h))
|
||||
}
|
||||
}
|
||||
|
||||
/// Render node of the GPU this reader captures from, so the converter binds to the device that EXPORTS the scanout:
|
||||
/// importing across vendors can fail on an incompatible tiling modifier. `None` if the symbol is absent or the device is display-only.
|
||||
pub fn render_node(&mut self) -> Option<String> {
|
||||
let f = self.lib.render_node?;
|
||||
// SAFETY: self.ctx is valid; the returned pointer is owned by the context and stays valid until it is closed.
|
||||
let ptr = unsafe { f(self.ctx) };
|
||||
if ptr.is_null() {
|
||||
return None;
|
||||
}
|
||||
unsafe { std::ffi::CStr::from_ptr(ptr) }
|
||||
.to_str()
|
||||
.ok()
|
||||
.map(|s| s.to_owned())
|
||||
}
|
||||
|
||||
/// Zero-copy EXPORT grab: fills a `drmtap_dmabuf_desc` (dma-buf fd, plane layout, HDR metadata) WITHOUT mapping, detiling or copying pixels, so on this
|
||||
/// path the root process never loads libEGL/libGLESv2. The exported fd is READ-ONLY (libdrmtap drops `DRM_RDWR` and `dup` shares that open file
|
||||
/// description), so the `--server` that receives it can map the scanout but never write the live framebuffer. Validation here is METADATA ONLY.
|
||||
pub fn grab_desc(&mut self) -> io::Result<(OwnedFd, drmtap_dmabuf_desc)> {
|
||||
let grab_desc = self.lib.grab_desc;
|
||||
// SAFETY: self.ctx is valid; desc/frame are zeroed before the call. Only paths that reach a populated frame release it: on `-EINVAL`
|
||||
// libdrmtap returns before allocating, a failed inner grab has already cleaned up, and on `-ENOTSUP` libdrmtap releases the frame itself.
|
||||
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 {
|
||||
// A distinct error so the caller degrades to the mapped/PipeWire path instead of 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}"),
|
||||
));
|
||||
}
|
||||
// `desc.dma_buf_fd` is the canonical fd (what split_capture.c sends); `frame` owns it too and `frame_release` closes the library's copy.
|
||||
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());
|
||||
}
|
||||
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"),
|
||||
));
|
||||
}
|
||||
// No fourcc gate here: the converter handles every format libdrmtap supports, and gating here dropped convertible scanouts such as XR30.
|
||||
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),
|
||||
));
|
||||
}
|
||||
for p in 0..(planes as usize) {
|
||||
let extent = (desc.pitches[p] as usize)
|
||||
.checked_mul(h as usize)
|
||||
.and_then(|rows| rows.checked_add(desc.offsets[p] as usize));
|
||||
match extent {
|
||||
Some(end) if end <= MAX_FRAME_BYTES => {}
|
||||
other => {
|
||||
(self.lib.frame_release)(self.ctx, &mut frame);
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!(
|
||||
"DRM scanout plane {p} out of range (offset {} pitch {} over {h} rows -> {other:?}, cap {MAX_FRAME_BYTES})",
|
||||
desc.offsets[p], desc.pitches[p]
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
// dup BEFORE releasing the frame: after release the library may recycle its handle, while an independent fd on the same open dma-buf
|
||||
// keeps the buffer alive for the peer. F_DUPFD_CLOEXEC, not dup(): `dup` never copies close-on-exec and this root service forks elsewhere.
|
||||
let dup_fd = hbb_common::libc::fcntl(raw_fd, hbb_common::libc::F_DUPFD_CLOEXEC, 0);
|
||||
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);
|
||||
(self.lib.frame_release)(self.ctx, &mut frame);
|
||||
desc.num_planes = planes;
|
||||
desc.dma_buf_fd = -1;
|
||||
Ok((owned, desc))
|
||||
}
|
||||
}
|
||||
|
||||
/// Read the hardware cursor plane: the hidden sentinel when the plane reports the cursor invisible, the real shape when visible, and `None` when the read fails.
|
||||
pub fn cursor(&mut self) -> Option<CursorSnapshot> {
|
||||
// SAFETY: ctx valid; c zeroed; released on EVERY path after a successful get_cursor. Only a failed get_cursor returns without releasing, because then there is nothing to release.
|
||||
unsafe {
|
||||
let mut c: drmtap_cursor_info = std::mem::zeroed();
|
||||
let cret = (self.lib.get_cursor)(self.ctx, &mut c);
|
||||
if cret != 0 {
|
||||
return None;
|
||||
}
|
||||
let out = if c.visible == 0 {
|
||||
Some(CursorSnapshot {
|
||||
id: HIDDEN_CURSOR_ID,
|
||||
width: 1,
|
||||
height: 1,
|
||||
hotx: 0,
|
||||
hoty: 0,
|
||||
colors: vec![0, 0, 0, 0],
|
||||
})
|
||||
} else if !c.pixels.is_null()
|
||||
&& c.width > 0
|
||||
&& c.height > 0
|
||||
&& (c.width as i64) * (c.height as i64) <= 256 * 256
|
||||
{
|
||||
let cw = c.width as i32;
|
||||
let ch = c.height as i32;
|
||||
let n = (cw * ch) as usize;
|
||||
let src = std::slice::from_raw_parts(c.pixels, n);
|
||||
let mut hash: u64 = 1469598103934665603;
|
||||
let mut colors = Vec::with_capacity(n * 4);
|
||||
let (mut minx, mut miny, mut maxx, mut maxy) = (cw, ch, -1i32, -1i32);
|
||||
for (i, &p) in src.iter().enumerate() {
|
||||
let a = ((p >> 24) & 0xff) as u8;
|
||||
let r = ((p >> 16) & 0xff) as u8;
|
||||
let g = ((p >> 8) & 0xff) as u8;
|
||||
let b = (p & 0xff) as u8;
|
||||
colors.push(r);
|
||||
colors.push(g);
|
||||
colors.push(b);
|
||||
colors.push(a);
|
||||
hash ^= p as u64;
|
||||
hash = hash.wrapping_mul(1099511628211);
|
||||
if a >= 128 {
|
||||
let x = (i as i32) % cw;
|
||||
let y = (i as i32) / cw;
|
||||
if x < minx { minx = x; }
|
||||
if x > maxx { maxx = x; }
|
||||
if y < miny { miny = y; }
|
||||
if y > maxy { maxy = y; }
|
||||
}
|
||||
}
|
||||
let (hotx, hoty) = if c.hot_x != 0 || c.hot_y != 0 {
|
||||
(c.hot_x, c.hot_y)
|
||||
} else if maxx >= minx && maxy >= miny {
|
||||
let (bw, bh) = (maxx - minx + 1, maxy - miny + 1);
|
||||
if bh > bw * 2 {
|
||||
((minx + maxx) / 2, (miny + maxy) / 2)
|
||||
} else {
|
||||
(minx, miny)
|
||||
}
|
||||
} else {
|
||||
(0, 0)
|
||||
};
|
||||
// Fold geometry + hotspot into the id: identical pixels with a changed size or
|
||||
// hotspot must count as a new shape, otherwise drm_capture_worker suppresses the
|
||||
// update (it dedupes by id) and the client keeps rendering the stale cursor.
|
||||
let mut id = hash;
|
||||
for v in [cw as u32 as u64, ch as u32 as u64, hotx as u32 as u64, hoty as u32 as u64] {
|
||||
id ^= v;
|
||||
id = id.wrapping_mul(1099511628211);
|
||||
}
|
||||
Some(CursorSnapshot {
|
||||
id,
|
||||
width: cw as u32,
|
||||
height: ch as u32,
|
||||
hotx,
|
||||
hoty,
|
||||
colors,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
(self.lib.cursor_release)(self.ctx, &mut c);
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
pub fn displays(&mut self) -> Vec<DisplaySnapshot> {
|
||||
// SAFETY: ctx valid; raw is a zeroed, correctly-sized array; count is clamped to the buffer before indexing.
|
||||
unsafe {
|
||||
let mut raw = vec![std::mem::zeroed::<drmtap_display>(); 16];
|
||||
let cap = raw.len() as i32;
|
||||
let n = (self.lib.list_displays)(self.ctx, raw.as_mut_ptr(), cap);
|
||||
if n <= 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
let count = (n as usize).min(raw.len());
|
||||
(0..count)
|
||||
.map(|i| {
|
||||
let name_bytes: Vec<u8> = raw[i]
|
||||
.name
|
||||
.iter()
|
||||
.take_while(|&&ch| ch != 0)
|
||||
.map(|&ch| ch as u8)
|
||||
.collect();
|
||||
DisplaySnapshot {
|
||||
name: String::from_utf8_lossy(&name_bytes).to_string(),
|
||||
crtc_id: raw[i].crtc_id,
|
||||
x: raw[i].x as i32,
|
||||
y: raw[i].y as i32,
|
||||
width: raw[i].width,
|
||||
height: raw[i].height,
|
||||
active: raw[i].active != 0,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for DrmReader {
|
||||
fn drop(&mut self) {
|
||||
if !self.ctx.is_null() {
|
||||
// SAFETY: ctx came from drmtap_open and is non-null.
|
||||
unsafe { (self.lib.close)(self.ctx) };
|
||||
self.ctx = std::ptr::null_mut();
|
||||
}
|
||||
}
|
||||
}
|
||||
184
libs/scrap/src/common/drm_render.rs
Normal file
184
libs/scrap/src/common/drm_render.rs
Normal file
@@ -0,0 +1,184 @@
|
||||
// Unprivileged half of the split DRM/KMS capture path: the root `--service` exports a scanout
|
||||
// dma-buf fd + descriptor, this side imports it and EGL-detiles. libEGL/libGLESv2 are dlopen'd
|
||||
// in the UNPRIVILEGED process on this path; the root service loads them only if it falls back to
|
||||
// its own CPU-mapped grab (`drmtap_grab_mapped`).
|
||||
|
||||
use super::drmtap_dl::{self, drmtap_ctx, drmtap_dmabuf_desc, drmtap_frame_info, DrmtapLib};
|
||||
use super::Pixfmt;
|
||||
use hbb_common::log;
|
||||
use std::ffi::CString;
|
||||
use std::io;
|
||||
use std::os::fd::RawFd;
|
||||
|
||||
use super::drm_reader::{
|
||||
DRM_FORMAT_ABGR8888, DRM_FORMAT_ARGB8888, DRM_FORMAT_XBGR8888, DRM_FORMAT_XRGB8888,
|
||||
MAX_DIM, MAX_FRAME_BYTES,
|
||||
};
|
||||
|
||||
/// Unprivileged DRM render-node convert context. !Send/!Sync via the raw ctx pointer: the context
|
||||
/// and libdrmtap's thread-local EGL state must be created, used (`convert`) and closed on ONE thread.
|
||||
pub struct RenderConverter {
|
||||
lib: &'static DrmtapLib,
|
||||
ctx: *mut drmtap_ctx,
|
||||
}
|
||||
|
||||
impl RenderConverter {
|
||||
/// `node` is the render node of the GPU that exports the scanout; `None`/invalid path falls back to libdrmtap auto-selection.
|
||||
pub fn open_render(node: Option<&str>) -> Option<RenderConverter> {
|
||||
let lib = drmtap_dl::get()?;
|
||||
let open_render = lib.open_render;
|
||||
let node_cstr = match node.filter(|n| !n.is_empty()) {
|
||||
None => None,
|
||||
// Open the CANONICAL path the gate resolved: opening the IPC string would re-walk its symlinks after the check.
|
||||
Some(n) => match super::drm_reader::device_under_dev_dri(n) {
|
||||
None => {
|
||||
log::warn!("drm: render node {n:?} is not under /dev/dri; auto-selecting");
|
||||
None
|
||||
}
|
||||
Some(canonical) => canonical.to_str().and_then(|s| CString::new(s).ok()),
|
||||
},
|
||||
};
|
||||
// SAFETY: resolved C entry point; `node_cstr` outlives the call, NULL requests auto-selection.
|
||||
let ctx = unsafe {
|
||||
open_render(node_cstr.as_ref().map_or(std::ptr::null(), |c| c.as_ptr()))
|
||||
};
|
||||
if ctx.is_null() {
|
||||
log::info!(
|
||||
"drmtap_open_render({}) failed; no usable DRM render node",
|
||||
node_cstr.as_ref().map_or("NULL".to_owned(), |c| format!("{c:?}"))
|
||||
);
|
||||
return None;
|
||||
}
|
||||
match node_cstr {
|
||||
Some(c) => log::info!(
|
||||
"drm: opened unprivileged convert context on the exporting GPU ({c:?})"
|
||||
),
|
||||
None => log::info!(
|
||||
"drm: opened unprivileged render-node convert context (auto-selected)"
|
||||
),
|
||||
}
|
||||
Some(RenderConverter { lib, ctx })
|
||||
}
|
||||
|
||||
/// Returns context-owned linear pixels valid ONLY until the next `convert()`; row stride is `len / height`.
|
||||
pub fn convert(
|
||||
&mut self,
|
||||
desc: &mut drmtap_dmabuf_desc,
|
||||
received_fd: RawFd,
|
||||
) -> io::Result<(&[u8], u32, u32, Pixfmt)> {
|
||||
{
|
||||
let (w, h) = (desc.width, desc.height);
|
||||
if w == 0 || h == 0 || w > MAX_DIM || h > MAX_DIM {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("drm: refusing a dma-buf descriptor with geometry {w}x{h}"),
|
||||
));
|
||||
}
|
||||
// Reject, do not clamp, and write the normalized count back so the C reads the count bounded here.
|
||||
let planes = if desc.num_planes == 0 { 1 } else { desc.num_planes };
|
||||
if planes > 4 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"drm: refusing a dma-buf descriptor with num_planes {} (1..=4)",
|
||||
desc.num_planes
|
||||
),
|
||||
));
|
||||
}
|
||||
desc.num_planes = planes;
|
||||
let planes = planes as usize;
|
||||
for p in 0..planes {
|
||||
let extent = (desc.pitches[p] as usize)
|
||||
.checked_mul(h as usize)
|
||||
.and_then(|rows| rows.checked_add(desc.offsets[p] as usize));
|
||||
match extent {
|
||||
Some(end) if end <= MAX_FRAME_BYTES => {}
|
||||
other => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"drm: refusing dma-buf plane {p} (offset {} pitch {} over {h} rows -> {other:?}, cap {MAX_FRAME_BYTES})",
|
||||
desc.offsets[p], desc.pitches[p]
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let convert_dmabuf = self.lib.convert_dmabuf;
|
||||
// LOAD-BEARING: the fd the exporter serialized was process-local; -1 means reuse the cached import for `fb_id`.
|
||||
desc.dma_buf_fd = received_fd;
|
||||
// SAFETY: self.ctx is a valid render context; `desc` is fully initialized; `frame` is zeroed
|
||||
// before the call. libdrmtap OWNS `frame.data`: no release/free from this side (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;
|
||||
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;
|
||||
// A stride below 32bpp under-sizes the row and, read as BGRA downstream, discloses 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)"),
|
||||
));
|
||||
}
|
||||
};
|
||||
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 => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("drmtap_convert_dmabuf produced an unsupported output fourcc {other:#010x}"),
|
||||
));
|
||||
}
|
||||
};
|
||||
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; the !Send ctx pointer keeps
|
||||
// this drop on the thread that created and used it (thread-local EGL + cached imports).
|
||||
unsafe { (self.lib.close)(self.ctx) };
|
||||
self.ctx = std::ptr::null_mut();
|
||||
}
|
||||
}
|
||||
}
|
||||
421
libs/scrap/src/common/drmtap_dl.rs
Normal file
421
libs/scrap/src/common/drmtap_dl.rs
Normal file
@@ -0,0 +1,421 @@
|
||||
// Runtime loader for libdrmtap.so (the DRM/KMS capture engine), dlopen'd so the binary carries no hard libdrm/libEGL/libGLESv2 dependency.
|
||||
|
||||
use hbb_common::{libloading::Library, log};
|
||||
use std::os::raw::{c_char, c_int, c_void};
|
||||
use std::sync::OnceLock;
|
||||
|
||||
// C ABI structs: must match libdrmtap include/drmtap.h.
|
||||
|
||||
#[repr(C)]
|
||||
pub struct drmtap_ctx {
|
||||
_private: [u8; 0],
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct drmtap_config {
|
||||
pub device_path: *const c_char, // NULL = auto-detect /dev/dri/card*
|
||||
pub crtc_id: u32, // 0 = auto-select first active CRTC
|
||||
pub helper_path: *const c_char, // only consulted if the direct DRM export is denied (no CAP_SYS_ADMIN)
|
||||
pub debug: c_int,
|
||||
}
|
||||
|
||||
impl Default for drmtap_config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
device_path: std::ptr::null(),
|
||||
crtc_id: 0,
|
||||
helper_path: std::ptr::null(),
|
||||
debug: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct drmtap_display {
|
||||
pub crtc_id: u32,
|
||||
pub connector_id: u32,
|
||||
pub name: [c_char; 32],
|
||||
pub x: u32,
|
||||
pub y: u32,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub refresh_hz: u32,
|
||||
pub active: c_int,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct drmtap_device {
|
||||
pub path: [c_char; 64],
|
||||
pub render_node: [c_char; 64],
|
||||
pub driver: [c_char; 32],
|
||||
pub display_count: u32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct drmtap_frame_info {
|
||||
pub data: *mut c_void,
|
||||
pub dma_buf_fd: c_int,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub stride: u32,
|
||||
pub format: u32,
|
||||
pub modifier: u64,
|
||||
pub fb_id: u32,
|
||||
pub _priv: *mut c_void,
|
||||
}
|
||||
|
||||
// Descriptor of an externally-supplied scanout DMA-BUF: the privileged exporter fills it via
|
||||
// `drmtap_grab_desc`; the converter overwrites `dma_buf_fd` with the fd it got via SCM_RIGHTS.
|
||||
// Mirrors `drmtap_dmabuf_desc` EXACTLY (field order + widths); a mismatch mis-reads CCS/HDR scanouts.
|
||||
#[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,
|
||||
pub y: i32,
|
||||
pub hot_x: i32,
|
||||
pub hot_y: i32,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub pixels: *mut u32,
|
||||
pub visible: c_int,
|
||||
pub _priv: *mut c_void,
|
||||
}
|
||||
|
||||
// Resolved symbol typedefs.
|
||||
|
||||
type FnVersion = unsafe extern "C" fn() -> c_int;
|
||||
type FnOpen = unsafe extern "C" fn(*const drmtap_config) -> *mut drmtap_ctx;
|
||||
type FnClose = unsafe extern "C" fn(*mut drmtap_ctx);
|
||||
type FnListDisplays = unsafe extern "C" fn(*mut drmtap_ctx, *mut drmtap_display, c_int) -> c_int;
|
||||
type FnListDevices = unsafe extern "C" fn(*mut drmtap_device, c_int) -> c_int;
|
||||
type FnGrabMapped = unsafe extern "C" fn(*mut drmtap_ctx, *mut drmtap_frame_info) -> c_int;
|
||||
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.10), required: `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;
|
||||
// libdrmtap >= 0.4.15; returns a ctx-owned string, or NULL if it has none.
|
||||
type FnRenderNode = unsafe extern "C" fn(*mut drmtap_ctx) -> *const c_char;
|
||||
type FnConvertDmabuf =
|
||||
unsafe extern "C" fn(*mut drmtap_ctx, *const drmtap_dmabuf_desc, *mut drmtap_frame_info) -> c_int;
|
||||
|
||||
/// The dlopen'd libdrmtap; the `Library` is kept alive for the process lifetime, so the raw fn pointers stay valid.
|
||||
pub struct DrmtapLib {
|
||||
_lib: Library,
|
||||
pub open: FnOpen,
|
||||
pub close: FnClose,
|
||||
pub list_displays: FnListDisplays,
|
||||
pub list_devices: Option<FnListDevices>,
|
||||
pub grab_mapped: FnGrabMapped,
|
||||
pub frame_release: FnFrameRelease,
|
||||
pub get_cursor: FnGetCursor,
|
||||
pub cursor_release: FnCursorRelease,
|
||||
pub grab_desc: FnGrabDesc,
|
||||
pub open_render: FnOpenRender,
|
||||
pub convert_dmabuf: FnConvertDmabuf,
|
||||
pub render_node: Option<FnRenderNode>,
|
||||
pub version: (c_int, c_int, c_int),
|
||||
}
|
||||
|
||||
// SAFETY: the resolved fn pointers are plain C entry points with no interior mutability;
|
||||
// libdrmtap contexts are used single-threaded by the caller. The Library handle is never moved out.
|
||||
unsafe impl Send for DrmtapLib {}
|
||||
unsafe impl Sync for DrmtapLib {}
|
||||
|
||||
const DRMTAP_ABI_MAJOR: c_int = 0;
|
||||
|
||||
// Lowest (minor, patch) accepted. 0.5.0 is the floor because it fixes the padded-framebuffer read
|
||||
// (a scanout whose pitch exceeds width*bpp was decoded at the wrong stride); the whole split API
|
||||
// has been present since 0.4.10.
|
||||
const DRMTAP_MIN_MINOR_PATCH: (c_int, c_int) = (5, 0);
|
||||
|
||||
// The MINOR series this build's mirrored structs were verified against: libdrmtap's header freezes
|
||||
// only `drmtap_device` and `drmtap_dmabuf_desc`, so an unverified minor could be read at wrong offsets.
|
||||
const DRMTAP_ABI_MINOR: c_int = 5;
|
||||
|
||||
/// Whether a library reporting `major.minor.patch` may be loaded (major and minor exact, patch at or above the floor).
|
||||
fn abi_accepted(major: c_int, minor: c_int, patch: c_int) -> bool {
|
||||
major == DRMTAP_ABI_MAJOR
|
||||
&& minor == DRMTAP_ABI_MINOR
|
||||
&& (minor, patch) >= DRMTAP_MIN_MINOR_PATCH
|
||||
}
|
||||
|
||||
impl DrmtapLib {
|
||||
fn load() -> Option<Self> {
|
||||
// Absolute path FIRST: the deb bundles the .so privately under /usr/lib/rustdesk and does NOT register that dir with ld.so.
|
||||
const INSTALLED: &str = "/usr/lib/rustdesk/libdrmtap.so.0";
|
||||
// Bare sonames exist so an unpackaged development build can load a locally built .so from
|
||||
// the normal ld.so search path. They are NOT offered when running as root: this is the one
|
||||
// place where which file happens to be on the load path decides what gets mapped into the
|
||||
// CAP_SYS_ADMIN process, and the packaged service always finds the absolute path first
|
||||
// anyway. A root process that reaches the fallback has no bundled library, which is the
|
||||
// PipeWire-fallback case, not a reason to search.
|
||||
const DEV_ONLY: [&str; 2] = ["libdrmtap.so.0", "libdrmtap.so"];
|
||||
let is_root = unsafe { hbb_common::libc::geteuid() } == 0;
|
||||
let candidates: Vec<&str> = if is_root {
|
||||
vec![INSTALLED]
|
||||
} else {
|
||||
std::iter::once(INSTALLED).chain(DEV_ONLY).collect()
|
||||
};
|
||||
unsafe {
|
||||
let mut errs = Vec::new();
|
||||
let found = candidates.iter().find_map(|n| match Library::new(*n) {
|
||||
Ok(l) => Some((l, *n)),
|
||||
Err(e) => {
|
||||
errs.push(format!("{n}: {e}"));
|
||||
None
|
||||
}
|
||||
});
|
||||
let Some((lib, name)) = found else {
|
||||
// The dlerror names the real cause (a missing soname, a glibc too old for the
|
||||
// bundled build); the caller only reports that DRM capture is off.
|
||||
log::warn!("libdrmtap dlopen failed: {}", errs.join("; "));
|
||||
return None;
|
||||
};
|
||||
// Canonicalize the absolute candidate only: `dlopen` does not search the CWD for a bare
|
||||
// soname, while `canonicalize` resolves a relative name against it.
|
||||
let real = std::path::Path::new(name)
|
||||
.is_absolute()
|
||||
.then(|| std::fs::canonicalize(name).ok())
|
||||
.flatten();
|
||||
let version: FnVersion = *lib.get(b"drmtap_version").ok()?;
|
||||
let v = version();
|
||||
let (major, minor, patch) = ((v >> 16) & 0xff, (v >> 8) & 0xff, v & 0xff);
|
||||
if !abi_accepted(major, minor, patch) {
|
||||
let why = if major != DRMTAP_ABI_MAJOR {
|
||||
"the struct layouts this build mirrors track the ABI major, so reading a \
|
||||
frame descriptor through a mismatched one would mis-decode it"
|
||||
} else if minor != DRMTAP_ABI_MINOR {
|
||||
"this build mirrors the struct layouts of one minor and only that one; \
|
||||
under 0.x semver the minor is the breaking axis, so an unverified minor \
|
||||
could be read at the wrong offsets. Widening it is a deliberate act, done \
|
||||
with the layouts re-checked field by field"
|
||||
} else {
|
||||
"it predates the split-capture API, so its only capture path converts \
|
||||
in-process, which in the root service means loading the GL stack there"
|
||||
};
|
||||
let (min_minor, min_patch) = DRMTAP_MIN_MINOR_PATCH;
|
||||
log::warn!(
|
||||
"libdrmtap {name} reports v{major}.{minor}.{patch}, which this build cannot \
|
||||
use (needs ABI major {DRMTAP_ABI_MAJOR}, minor {DRMTAP_ABI_MINOR}, at least \
|
||||
v{DRMTAP_ABI_MAJOR}.{min_minor}.{min_patch}): {why}. Refusing to load; \
|
||||
falling back to PipeWire/portal."
|
||||
);
|
||||
return None;
|
||||
}
|
||||
let open: FnOpen = *lib.get(b"drmtap_open").ok()?;
|
||||
let close: FnClose = *lib.get(b"drmtap_close").ok()?;
|
||||
let list_displays: FnListDisplays = *lib.get(b"drmtap_list_displays").ok()?;
|
||||
let list_devices: Option<FnListDevices> =
|
||||
lib.get(b"drmtap_list_devices").ok().map(|s| *s);
|
||||
let grab_mapped: FnGrabMapped = *lib.get(b"drmtap_grab_mapped").ok()?;
|
||||
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()?;
|
||||
let grab: Option<FnGrabDesc> = lib.get(b"drmtap_grab_desc").ok().map(|s| *s);
|
||||
let open_r: Option<FnOpenRender> = lib.get(b"drmtap_open_render").ok().map(|s| *s);
|
||||
let conv: Option<FnConvertDmabuf> =
|
||||
lib.get(b"drmtap_convert_dmabuf").ok().map(|s| *s);
|
||||
let (grab_desc, open_render, convert_dmabuf) = match (grab, open_r, conv) {
|
||||
(Some(g), Some(o), Some(c)) => (g, o, c),
|
||||
(grab, open_r, conv) => {
|
||||
let mut missing = Vec::new();
|
||||
if grab.is_none() {
|
||||
missing.push("drmtap_grab_desc");
|
||||
}
|
||||
if open_r.is_none() {
|
||||
missing.push("drmtap_open_render");
|
||||
}
|
||||
if conv.is_none() {
|
||||
missing.push("drmtap_convert_dmabuf");
|
||||
}
|
||||
log::warn!(
|
||||
"libdrmtap {name} reports v{major}.{minor}.{patch} but does not export \
|
||||
{}: it is a stale or pre-release build, not the version it claims. \
|
||||
Refusing to load; falling back to PipeWire/portal.",
|
||||
missing.join(", ")
|
||||
);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let render_node: Option<FnRenderNode> =
|
||||
lib.get(b"drmtap_render_node").ok().map(|s| *s);
|
||||
// Log the load only now that every required symbol resolved: this fn still returns None on a missing one.
|
||||
let loaded_from = real
|
||||
.as_ref()
|
||||
.map_or_else(|| name.to_owned(), |p| p.display().to_string());
|
||||
if loaded_from == name {
|
||||
log::info!("libdrmtap loaded: {name} (v{major}.{minor}.{patch})");
|
||||
} else {
|
||||
log::info!("libdrmtap loaded: {name} -> {loaded_from} (v{major}.{minor}.{patch})");
|
||||
}
|
||||
let (no_node, no_devices) = (render_node.is_none(), list_devices.is_none());
|
||||
if (minor, patch) >= (4, 15) && (no_node || no_devices) {
|
||||
let missing = if no_node && no_devices {
|
||||
"drmtap_render_node and drmtap_list_devices"
|
||||
} else if no_node {
|
||||
"drmtap_render_node"
|
||||
} else {
|
||||
"drmtap_list_devices"
|
||||
};
|
||||
let effect = if no_node && no_devices {
|
||||
"Multi-GPU display enumeration and exporting-GPU selection stay disabled."
|
||||
} else if no_node {
|
||||
"Exporting-GPU selection stays disabled."
|
||||
} else {
|
||||
"Multi-GPU display enumeration stays disabled."
|
||||
};
|
||||
log::warn!(
|
||||
"libdrmtap at {loaded_from} reports v{major}.{minor}.{patch} but is missing \
|
||||
{missing}: it is a stale or pre-release build. Check what the soname symlink \
|
||||
points at and remove any leftover libdrmtap.so.0* beside it. {effect}"
|
||||
);
|
||||
}
|
||||
Some(DrmtapLib {
|
||||
_lib: lib,
|
||||
open,
|
||||
close,
|
||||
list_displays,
|
||||
list_devices,
|
||||
grab_mapped,
|
||||
frame_release,
|
||||
get_cursor,
|
||||
cursor_release,
|
||||
grab_desc,
|
||||
open_render,
|
||||
convert_dmabuf,
|
||||
render_node,
|
||||
version: (major, minor, patch),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static DRMTAP_LIB: OnceLock<Option<DrmtapLib>> = OnceLock::new();
|
||||
|
||||
/// The loaded libdrmtap, or None if the .so (or a runtime dep) is absent or its version/exports fall outside the ABI gate. Loaded once; a failure is remembered.
|
||||
pub fn get() -> Option<&'static DrmtapLib> {
|
||||
DRMTAP_LIB
|
||||
.get_or_init(|| {
|
||||
let lib = DrmtapLib::load();
|
||||
if lib.is_none() {
|
||||
log::info!("libdrmtap not available or not usable; DRM capture disabled");
|
||||
}
|
||||
lib
|
||||
})
|
||||
.as_ref()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{abi_accepted, DRMTAP_ABI_MAJOR, DRMTAP_ABI_MINOR, DRMTAP_MIN_MINOR_PATCH};
|
||||
|
||||
#[test]
|
||||
fn abi_gate_rejects_a_library_from_before_the_split() {
|
||||
// These are refused because their MINOR differs from the verified one, which is the only
|
||||
// reason the gate needs. Naming the pre-split releases keeps the intent readable, but do
|
||||
// not read this as the floor doing the work: see the test below.
|
||||
for (minor, patch) in [(3, 3), (4, 0), (4, 8), (4, 9)] {
|
||||
assert!(
|
||||
!abi_accepted(DRMTAP_ABI_MAJOR, minor, patch),
|
||||
"v0.{minor}.{patch} is not the verified minor and must be refused"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_patch_floor_is_currently_vacuous_and_that_is_deliberate() {
|
||||
// With MIN_MINOR_PATCH.0 == DRMTAP_ABI_MINOR the floor can never reject anything: the
|
||||
// minor equality already forces `(minor, patch) >= (minor, 0)`. It is kept because it is
|
||||
// the mechanism that WOULD do the work the next time a floor lands mid-minor, as (4, 10)
|
||||
// did for the split API. This test exists so nobody reads the pre-split test above as
|
||||
// evidence that the floor is live -- if that ever matters, this assert is the tripwire.
|
||||
let (floor_minor, floor_patch) = DRMTAP_MIN_MINOR_PATCH;
|
||||
assert_eq!(
|
||||
floor_minor, DRMTAP_ABI_MINOR,
|
||||
"the floor is inside the verified minor; a floor in a DIFFERENT minor is unreachable"
|
||||
);
|
||||
if floor_patch == 0 {
|
||||
assert!(
|
||||
abi_accepted(DRMTAP_ABI_MAJOR, DRMTAP_ABI_MINOR, 0),
|
||||
"patch 0 of the verified minor must be accepted while the floor is 0"
|
||||
);
|
||||
} else {
|
||||
assert!(!abi_accepted(DRMTAP_ABI_MAJOR, DRMTAP_ABI_MINOR, floor_patch - 1));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn abi_gate_accepts_the_floor_and_later_patches_of_the_same_minor() {
|
||||
let (min_minor, min_patch) = DRMTAP_MIN_MINOR_PATCH;
|
||||
assert!(abi_accepted(DRMTAP_ABI_MAJOR, min_minor, min_patch));
|
||||
for (minor, patch) in [(DRMTAP_ABI_MINOR, min_patch + 15), (DRMTAP_ABI_MINOR, 200)] {
|
||||
assert!(
|
||||
abi_accepted(DRMTAP_ABI_MAJOR, minor, patch),
|
||||
"v0.{minor}.{patch} is a patch of the verified minor and must be accepted"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn abi_gate_rejects_an_unknown_newer_minor() {
|
||||
// Relative to DRMTAP_ABI_MINOR, so the next bump cannot leave this test asserting that the
|
||||
// NEW verified minor must be refused -- which is what a hardcoded list did before.
|
||||
let verified = DRMTAP_ABI_MINOR;
|
||||
for (minor, patch) in [
|
||||
(verified - 1, 99),
|
||||
(verified + 1, 0),
|
||||
(verified + 1, 99),
|
||||
(verified + 4, 9),
|
||||
] {
|
||||
assert!(
|
||||
!abi_accepted(DRMTAP_ABI_MAJOR, minor, patch),
|
||||
"v0.{minor}.{patch} is an unverified minor and must be refused"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn abi_gate_rejects_another_major_in_both_directions() {
|
||||
assert!(!abi_accepted(DRMTAP_ABI_MAJOR + 1, 0, 0));
|
||||
assert!(!abi_accepted(DRMTAP_ABI_MAJOR + 1, 99, 99));
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,12 @@ cfg_if! {
|
||||
mod linux;
|
||||
mod wayland;
|
||||
mod x11;
|
||||
#[cfg(all(target_os = "linux", feature = "drm"))]
|
||||
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;
|
||||
|
||||
@@ -19,6 +19,18 @@ static MISSING_LOGICAL_SIZE_WARNED: std::sync::atomic::AtomicBool =
|
||||
|
||||
const COMMAND_TIMEOUT: Duration = Duration::from_millis(1000);
|
||||
|
||||
// drm builds only: an unnamed-endpoint failure there forks the probe child, and the pollers
|
||||
// turn every few hundred milliseconds. Every other failure is one cheap in-process error.
|
||||
#[cfg(any(test, feature = "drm"))]
|
||||
const FAILED_LOOKUP_BACKOFF: Duration = Duration::from_secs(5);
|
||||
|
||||
#[cfg(any(test, feature = "drm"))]
|
||||
static LAST_FAILED_LOOKUP: Mutex<Option<Instant>> = Mutex::new(None);
|
||||
|
||||
#[cfg(feature = "drm")]
|
||||
static LOOKUP_FAILURE_WARNED: std::sync::atomic::AtomicBool =
|
||||
std::sync::atomic::AtomicBool::new(false);
|
||||
|
||||
pub struct Displays {
|
||||
pub primary: usize,
|
||||
pub displays: Vec<WaylandDisplayInfo>,
|
||||
@@ -171,11 +183,76 @@ fn get_primary_monitor() -> Option<String> {
|
||||
.or_else(try_gdbus_primary)
|
||||
}
|
||||
|
||||
// Pure, so the backoff policy is testable without a compositor.
|
||||
#[cfg(any(test, feature = "drm"))]
|
||||
fn lookup_allowed(failed_at: Option<Instant>, now: Instant) -> bool {
|
||||
failed_at.map_or(true, |at| {
|
||||
now.saturating_duration_since(at) >= FAILED_LOOKUP_BACKOFF
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "drm")]
|
||||
fn backed_off() -> bool {
|
||||
let failed_at = *LAST_FAILED_LOOKUP.lock().unwrap();
|
||||
!lookup_allowed(failed_at, Instant::now())
|
||||
}
|
||||
|
||||
// Mirrors the probe module's gate, latch included: connecting consumes WAYLAND_SOCKET, so a
|
||||
// once-named endpoint must stay named for the life of the process.
|
||||
#[cfg(feature = "drm")]
|
||||
fn endpoint_named() -> bool {
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
static WAS_NAMED: AtomicBool = AtomicBool::new(false);
|
||||
let named = ["WAYLAND_DISPLAY", "WAYLAND_SOCKET"]
|
||||
.iter()
|
||||
.any(|key| std::env::var_os(key).is_some_and(|value| !value.is_empty()));
|
||||
if named {
|
||||
WAS_NAMED.store(true, Ordering::Release);
|
||||
}
|
||||
WAS_NAMED.load(Ordering::Acquire)
|
||||
}
|
||||
|
||||
// Enumerates and keeps the failure stamp current. Suppresses nothing itself: one-shot callers
|
||||
// (session init, pipewire) must always get a fresh read, or a transient failure latches.
|
||||
fn enumerate_displays() -> hbb_common::ResultType<Vec<WaylandDisplayInfo>> {
|
||||
// Read before connecting, which consumes WAYLAND_SOCKET.
|
||||
#[cfg(feature = "drm")]
|
||||
let named = endpoint_named();
|
||||
let probed = get_wayland_displays();
|
||||
// Only the failure that would fork stamps; a named endpoint fails cheaply in-process.
|
||||
#[cfg(feature = "drm")]
|
||||
{
|
||||
*LAST_FAILED_LOOKUP.lock().unwrap() = (probed.is_err() && !named).then(Instant::now);
|
||||
if let Err(err) = &probed {
|
||||
if !LOOKUP_FAILURE_WARNED.swap(true, std::sync::atomic::Ordering::Relaxed) {
|
||||
warn!("Failed to get wayland displays: {}", err);
|
||||
}
|
||||
} else {
|
||||
LOOKUP_FAILURE_WARNED.store(false, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
probed
|
||||
}
|
||||
|
||||
// True when a lookup now could neither hit the cache nor probe. Pollers skip their turn on
|
||||
// it and keep their last published state; one-shot callers must not consult it.
|
||||
#[cfg(feature = "drm")]
|
||||
pub fn wayland_lookup_suppressed() -> bool {
|
||||
DISPLAYS.lock().unwrap().is_none() && backed_off()
|
||||
}
|
||||
|
||||
// Whether any failure stamp exists, expired or not: pollers use it to tell a first failure
|
||||
// from one that has already persisted across a backoff.
|
||||
#[cfg(feature = "drm")]
|
||||
pub fn wayland_failure_stamped() -> bool {
|
||||
LAST_FAILED_LOOKUP.lock().unwrap().is_some()
|
||||
}
|
||||
|
||||
pub fn get_displays() -> Arc<Displays> {
|
||||
let mut lock = DISPLAYS.lock().unwrap();
|
||||
match lock.as_ref() {
|
||||
Some(displays) => displays.clone(),
|
||||
None => match get_wayland_displays() {
|
||||
None => match enumerate_displays() {
|
||||
Ok(displays) => {
|
||||
let mut primary_index = None;
|
||||
if let Some(name) = get_primary_monitor() {
|
||||
@@ -201,8 +278,9 @@ pub fn get_displays() -> Arc<Displays> {
|
||||
*lock = Some(displays.clone());
|
||||
displays
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("Failed to get wayland displays: {}", err);
|
||||
Err(_err) => {
|
||||
#[cfg(not(feature = "drm"))]
|
||||
warn!("Failed to get wayland displays: {}", _err);
|
||||
Arc::new(Displays {
|
||||
primary: 0,
|
||||
displays: Vec::new(),
|
||||
@@ -215,6 +293,8 @@ pub fn get_displays() -> Arc<Displays> {
|
||||
#[inline]
|
||||
pub fn clear_wayland_displays_cache() {
|
||||
let _ = DISPLAYS.lock().unwrap().take();
|
||||
// The failure stamp survives on purpose: it describes the seat, not the cache, and the
|
||||
// capturer rebuild loop clears about once a second.
|
||||
}
|
||||
|
||||
// Return (min_x, max_x, min_y, max_y)
|
||||
@@ -223,17 +303,21 @@ pub fn get_desktop_rect_for_uinput() -> Option<(i32, i32, i32, i32)> {
|
||||
desktop_rect_of(&wayland_displays.displays)
|
||||
}
|
||||
|
||||
// The desktop rect and per-display logical rects, always read live from the
|
||||
// compositor in a single roundtrip. Skips the displays cache and the primary-monitor
|
||||
// detection (which may spawn external commands), so it is cheap enough to poll for
|
||||
// layout changes. https://github.com/rustdesk/rustdesk/issues/15601
|
||||
// The desktop rect and per-display logical rects, read live from the compositor in a single
|
||||
// roundtrip (drm builds may skip a turn during the failure backoff). Skips the displays cache
|
||||
// and the primary-monitor detection, cheap enough to poll. rustdesk/rustdesk#15601
|
||||
pub fn get_layout_for_uinput_live() -> Option<((i32, i32, i32, i32), Vec<DisplayRect>)> {
|
||||
match get_wayland_displays() {
|
||||
#[cfg(feature = "drm")]
|
||||
if backed_off() {
|
||||
return None;
|
||||
}
|
||||
match enumerate_displays() {
|
||||
Ok(displays) => {
|
||||
desktop_rect_of(&displays).map(|rect| (rect, logical_rects_of(&displays)))
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("Failed to get wayland displays: {}", err);
|
||||
Err(_err) => {
|
||||
#[cfg(not(feature = "drm"))]
|
||||
warn!("Failed to get wayland displays: {}", _err);
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -386,6 +470,40 @@ fn map_axis(v: i32, base_origin: i32, base_extent: i32, live_origin: i32, live_e
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_lookup_backoff_boundaries() {
|
||||
// Future `now`s sidestep Instant subtraction, which can panic near boot.
|
||||
let failed_at = Instant::now();
|
||||
assert!(lookup_allowed(None, failed_at));
|
||||
assert!(!lookup_allowed(
|
||||
Some(failed_at),
|
||||
failed_at + FAILED_LOOKUP_BACKOFF / 2
|
||||
));
|
||||
assert!(lookup_allowed(
|
||||
Some(failed_at),
|
||||
failed_at + FAILED_LOOKUP_BACKOFF
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lookup_stamp_from_the_future_only_waits() {
|
||||
// saturating_duration_since answers zero rather than underflowing.
|
||||
let now = Instant::now();
|
||||
assert!(!lookup_allowed(Some(now + FAILED_LOOKUP_BACKOFF), now));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clear_keeps_the_failure_stamp() {
|
||||
// The stamp describes the seat, not the cache: the ~1/s capturer rebuild loop clears,
|
||||
// and dropping the stamp with it would defeat the backoff. Sole test touching these
|
||||
// statics; serialize before adding another.
|
||||
*LAST_FAILED_LOOKUP.lock().unwrap() = Some(Instant::now());
|
||||
clear_wayland_displays_cache();
|
||||
let stamp = *LAST_FAILED_LOOKUP.lock().unwrap();
|
||||
assert!(stamp.is_some());
|
||||
*LAST_FAILED_LOOKUP.lock().unwrap() = None;
|
||||
}
|
||||
|
||||
fn display(
|
||||
x: i32,
|
||||
y: i32,
|
||||
|
||||
Reference in New Issue
Block a user