mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-14 08:21:02 +03:00
drm: address the phase-2 split review
1- do not depend on the libdrmtap-sys crate for the pin: its build.rs statically compiles the whole libdrmtap C tree and a CAP_SYS_ADMIN helper and links -ldrm/-lseccomp/-lcap, which defeats the runtime-dlopen model. keep drm a pure dlopen backend and pin the .so by the build.py DRMTAP_REF release tag, guarded by a strict vX.Y.Z regex. drops the now-moot Cargo.lock freshness CI checks. 2- render-node-less consumers no longer lose the stream: the --server signals need_cpu on DrmStart when it cannot open a convert context, and the --service streams the CPU-converted frame path for that connection instead of a dma-buf fd the consumer cannot detile (which used to fall through to a PipeWire path nobody can approve on an unattended seat). 3- mark PipeWire initialized only after every per-display capturer is created, so a partial failure retries instead of the flag falsely reporting a complete init. 4- reject a degenerate (zero width/height) or short CPU frame before it reaches PixelBuffer::new (which derives stride as data.len()/height, dividing by zero). 5- keep the export-ledger epoch at DRM_DISPLAY_GENERATION so a hotplug invalidates cached buffers (elision stays off until the recycled-fb_id inode case is handled). 6- validate the udev uevent source (kernel nl_pid, multicast) with recvmsg so a local process cannot unicast a spoofed drm-change event to the root listener.
This commit is contained in:
24
.github/workflows/flutter-build.yml
vendored
24
.github/workflows/flutter-build.yml
vendored
@@ -1708,11 +1708,12 @@ jobs:
|
|||||||
export DRMTAP_REPO="https://github.com/rustdesk-org/libdrmtap"
|
export DRMTAP_REPO="https://github.com/rustdesk-org/libdrmtap"
|
||||||
export DRMTAP_REF="v0.4.13"
|
export DRMTAP_REF="v0.4.13"
|
||||||
# Guard: refuse a loose/branch ref so a moving `main` can never silently
|
# Guard: refuse a loose/branch ref so a moving `main` can never silently
|
||||||
# regress the pin. Only a vX.Y.Z tag is accepted.
|
# regress the pin. Only an EXACT vX.Y.Z tag is accepted (a strict anchored
|
||||||
case "$DRMTAP_REF" in
|
# match, so values like v0.4.13-ci or a branch that resolves under
|
||||||
v[0-9]*.[0-9]*.[0-9]*) ;;
|
# `git clone --branch` are rejected).
|
||||||
*) echo "FATAL: DRMTAP_REF must be a pinned vX.Y.Z tag, got '$DRMTAP_REF'"; exit 1;;
|
if ! printf '%s' "$DRMTAP_REF" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+$'; then
|
||||||
esac
|
echo "FATAL: DRMTAP_REF must be a pinned vX.Y.Z tag, got '$DRMTAP_REF'"; exit 1
|
||||||
|
fi
|
||||||
git config --global --add safe.directory '*' || true
|
git config --global --add safe.directory '*' || true
|
||||||
rm -rf third_party/libdrmtap
|
rm -rf third_party/libdrmtap
|
||||||
git clone --depth 1 --branch "$DRMTAP_REF" "$DRMTAP_REPO" third_party/libdrmtap
|
git clone --depth 1 --branch "$DRMTAP_REF" "$DRMTAP_REPO" third_party/libdrmtap
|
||||||
@@ -1731,16 +1732,9 @@ jobs:
|
|||||||
[ -n "$(find "$DRMTAP_PREBUILT_DIR" -name 'libdrmtap.so.0.*' -type f)" ] \
|
[ -n "$(find "$DRMTAP_PREBUILT_DIR" -name 'libdrmtap.so.0.*' -type f)" ] \
|
||||||
|| { echo "FATAL: prebuilt libdrmtap.so missing"; exit 1; }
|
|| { echo "FATAL: prebuilt libdrmtap.so missing"; exit 1; }
|
||||||
ls -l "$DRMTAP_PREBUILT_DIR"
|
ls -l "$DRMTAP_PREBUILT_DIR"
|
||||||
# Lock-freshness guard, BEFORE the `--locked` build (which itself fails on a
|
# The drm backend is pure runtime-dlopen (no libdrmtap-sys crate dependency),
|
||||||
# checksum mismatch): prove Cargo.lock is in sync with Cargo.toml and actually
|
# so the pin is the DRMTAP_REF tag verified above plus the prebuilt .so; there
|
||||||
# contains the pinned libdrmtap-sys (the integrator must have run
|
# is nothing to assert in Cargo.lock for it.
|
||||||
# `cargo update -p libdrmtap-sys --precise 0.4.13`). This turns a stale/loose lock
|
|
||||||
# into an explicit, readable failure instead of a confusing error deep in the build.
|
|
||||||
# The `=0.4.13` requirement in libs/scrap/Cargo.toml + `--locked` already force the
|
|
||||||
# exact resolved version + its checksum; this only confirms the lock was regenerated.
|
|
||||||
cargo update --locked --dry-run
|
|
||||||
cargo tree --locked --features drm -p libdrmtap-sys -i >/dev/null \
|
|
||||||
|| { echo "FATAL: Cargo.lock does not contain the pinned libdrmtap-sys (run: cargo update -p libdrmtap-sys --precise 0.4.13)"; exit 1; }
|
|
||||||
cargo build --locked --lib $JOBS --features hwcodec,flutter,unix-file-copy-paste,drm --release
|
cargo build --locked --lib $JOBS --features hwcodec,flutter,unix-file-copy-paste,drm --release
|
||||||
python3 ./build.py --flutter --drm --skip-cargo
|
python3 ./build.py --flutter --drm --skip-cargo
|
||||||
for name in rustdesk-unattended-wayland*??.deb; do
|
for name in rustdesk-unattended-wayland*??.deb; do
|
||||||
|
|||||||
11
Cargo.lock
generated
11
Cargo.lock
generated
@@ -4485,16 +4485,6 @@ dependencies = [
|
|||||||
"pkg-config",
|
"pkg-config",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "libdrmtap-sys"
|
|
||||||
version = "0.4.13"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e6c52136bd2bf94bb1071f3926eff327148faddc704338f3fca473e1d1974723"
|
|
||||||
dependencies = [
|
|
||||||
"cc",
|
|
||||||
"pkg-config",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libgit2-sys"
|
name = "libgit2-sys"
|
||||||
version = "0.14.2+1.5.1"
|
version = "0.14.2+1.5.1"
|
||||||
@@ -7620,7 +7610,6 @@ dependencies = [
|
|||||||
"hwcodec",
|
"hwcodec",
|
||||||
"jni",
|
"jni",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"libdrmtap-sys",
|
|
||||||
"log",
|
"log",
|
||||||
"ndk 0.7.0",
|
"ndk 0.7.0",
|
||||||
"ndk-context",
|
"ndk-context",
|
||||||
|
|||||||
10
build.py
10
build.py
@@ -335,11 +335,11 @@ def ffi_bindgen_function_refactor():
|
|||||||
# libdrmtap is fetched at build time by cloning the rustdesk-org fork at a pinned
|
# libdrmtap is fetched at build time by cloning the rustdesk-org fork at a pinned
|
||||||
# ref — the same way rustdesk sources its other native build deps (vcpkg,
|
# ref — the same way rustdesk sources its other native build deps (vcpkg,
|
||||||
# flutter_rust_bridge, ...), rather than carrying a git submodule. The ref is an
|
# flutter_rust_bridge, ...), rather than carrying a git submodule. The ref is an
|
||||||
# EXACT release tag (vX.Y.Z), NOT a moving branch: the bundled runtime .so version must
|
# EXACT release tag (vX.Y.Z), NOT a moving branch, and it is the ONLY pin for the
|
||||||
# match the `libdrmtap-sys = "=0.4.13"` crate pinned in libs/scrap/Cargo.toml (the dlopen
|
# drm backend: rustdesk dlopens this .so at runtime and does not depend on the
|
||||||
# loader only checks ABI-major, so a minor skew between the two artifacts would go undetected).
|
# libdrmtap-sys crate (whose build.rs would statically link the C tree, a helper and
|
||||||
# Bump this tag and the crate pin together. Override the repo/ref via env (DRMTAP_REPO /
|
# libdrm/seccomp/cap). Override the repo/ref via env (DRMTAP_REPO / DRMTAP_REF) for
|
||||||
# DRMTAP_REF) for local testing or another fork.
|
# local testing or another fork.
|
||||||
LIBDRMTAP_REPO = os.environ.get('DRMTAP_REPO', 'https://github.com/rustdesk-org/libdrmtap')
|
LIBDRMTAP_REPO = os.environ.get('DRMTAP_REPO', 'https://github.com/rustdesk-org/libdrmtap')
|
||||||
LIBDRMTAP_REF = os.environ.get('DRMTAP_REF', 'v0.4.13')
|
LIBDRMTAP_REF = os.environ.get('DRMTAP_REF', 'v0.4.13')
|
||||||
|
|
||||||
|
|||||||
@@ -11,11 +11,13 @@ edition = "2018"
|
|||||||
|
|
||||||
[features]
|
[features]
|
||||||
wayland = ["gstreamer", "gstreamer-app", "gstreamer-video", "dbus", "tracing", "zbus"]
|
wayland = ["gstreamer", "gstreamer-app", "gstreamer-video", "dbus", "tracing", "zbus"]
|
||||||
# `drm` pulls in the exact-pinned `libdrmtap-sys` crate (see the Linux target table below). rustdesk
|
# `drm` is a pure runtime-dlopen backend: rustdesk loads `libdrmtap.so.0` at runtime (`drmtap_dl.rs`)
|
||||||
# still dlopen's `libdrmtap.so.0` at runtime (`drmtap_dl.rs`) rather than link-time linking it, so the
|
# and NEVER link-time links it, so the graceful PipeWire fallback when the .so or EGL is absent is
|
||||||
# graceful PipeWire fallback when the .so/EGL is absent is preserved; the crate is pinned purely so its
|
# preserved and the drm build pulls in no libdrm/seccomp/cap/EGL link-time deps. The .so is version
|
||||||
# committed Cargo.lock checksum supply-chain-pins the vendored source that build.py bundles.
|
# pinned by build.py's `DRMTAP_REF = v0.4.13` (an exact release tag, not `main`). We deliberately do
|
||||||
drm = ["dep:libdrmtap-sys"]
|
# 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.
|
||||||
|
drm = []
|
||||||
mediacodec = ["ndk"]
|
mediacodec = ["ndk"]
|
||||||
linux-pkg-config = ["dep:pkg-config"]
|
linux-pkg-config = ["dep:pkg-config"]
|
||||||
hwcodec = ["dep:hwcodec"]
|
hwcodec = ["dep:hwcodec"]
|
||||||
@@ -63,16 +65,6 @@ gstreamer = { version = "0.16", optional = true }
|
|||||||
gstreamer-app = { version = "0.16", features = ["v1_10"], optional = true }
|
gstreamer-app = { version = "0.16", features = ["v1_10"], optional = true }
|
||||||
gstreamer-video = { version = "0.16", optional = true }
|
gstreamer-video = { version = "0.16", optional = true }
|
||||||
zbus = { version = "3.15", 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]
|
[dependencies.hwcodec]
|
||||||
git = "https://github.com/rustdesk-org/hwcodec"
|
git = "https://github.com/rustdesk-org/hwcodec"
|
||||||
optional = true
|
optional = true
|
||||||
|
|||||||
78
src/ipc.rs
78
src/ipc.rs
@@ -494,7 +494,11 @@ pub enum Data {
|
|||||||
// the same `DrmConn` send (see `DrmConn::send_msg`), so it has NO trailing `send_raw()` body.
|
// the same `DrmConn` send (see `DrmConn::send_msg`), so it has NO trailing `send_raw()` body.
|
||||||
/// Client -> service: begin streaming the chosen display.
|
/// Client -> service: begin streaming the chosen display.
|
||||||
#[cfg(all(target_os = "linux", feature = "drm"))]
|
#[cfg(all(target_os = "linux", feature = "drm"))]
|
||||||
DrmStart { display: i32 },
|
// `need_cpu` is set by an unprivileged consumer that could not open a render-node convert context
|
||||||
|
// (drmtap_open_render failed, or an old .so lacks the split symbols). The service then streams the
|
||||||
|
// CPU-converted `DrmFrame` path for this connection instead of a dma-buf fd the consumer cannot
|
||||||
|
// detile, so a render-node-less seat still captures instead of losing the stream.
|
||||||
|
DrmStart { display: i32, need_cpu: bool },
|
||||||
/// Service -> client: the enumerated DRM displays (sent once, before frames).
|
/// Service -> client: the enumerated DRM displays (sent once, before frames).
|
||||||
#[cfg(all(target_os = "linux", feature = "drm"))]
|
#[cfg(all(target_os = "linux", feature = "drm"))]
|
||||||
DrmDisplayList(Vec<DrmDisplayInfo>),
|
DrmDisplayList(Vec<DrmDisplayInfo>),
|
||||||
@@ -1651,8 +1655,12 @@ impl Drop for DrmStopGuard {
|
|||||||
/// and modifier; eliding then would serve a stale EGLImage. libdrmtap's own import cache keys on
|
/// and modifier; eliding then would serve a stale EGLImage. libdrmtap's own import cache keys on
|
||||||
/// `fb_id + dma-buf inode` and can re-import ONLY when it is handed a real fd. Because always sending
|
/// `fb_id + dma-buf inode` and can re-import ONLY when it is handed a real fd. Because always sending
|
||||||
/// the fd is cheap (the converter still imports once per `fb_id` and closes the surplus fd) and is
|
/// the fd is cheap (the converter still imports once per `fb_id` and closes the surplus fd) and is
|
||||||
/// strictly safe, `DRM_FD_ELISION` defaults to `false` for v1 (always send). The ledger is fully
|
/// strictly safe, `DRM_FD_ELISION` defaults to `false` for v1 (always send). The ledger's `epoch`
|
||||||
/// wired so flipping the const on enables elision once the recycled-fb_id case is validated.
|
/// tracks `DRM_DISPLAY_GENERATION` (bumped by the udev listener on a connector-topology change), so a
|
||||||
|
/// hotplug/modeset invalidates every cached buffer and forces a real fd; but the ledger still cannot
|
||||||
|
/// see the dma-buf inode, so a recycled fb_id within the SAME generation (identical geometry +
|
||||||
|
/// modifier) would elide onto a stale EGLImage. Enabling elision needs that inode case validated
|
||||||
|
/// first.
|
||||||
#[cfg(all(target_os = "linux", feature = "drm"))]
|
#[cfg(all(target_os = "linux", feature = "drm"))]
|
||||||
const DRM_FD_ELISION: bool = false;
|
const DRM_FD_ELISION: bool = false;
|
||||||
|
|
||||||
@@ -1660,14 +1668,14 @@ const DRM_FD_ELISION: bool = false;
|
|||||||
struct SeenBuf {
|
struct SeenBuf {
|
||||||
modifier: u64,
|
modifier: u64,
|
||||||
dims: (u32, u32),
|
dims: (u32, u32),
|
||||||
epoch: u32,
|
epoch: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(all(target_os = "linux", feature = "drm"))]
|
#[cfg(all(target_os = "linux", feature = "drm"))]
|
||||||
struct ExportLedger {
|
struct ExportLedger {
|
||||||
seen: HashMap<u32, SeenBuf>,
|
seen: HashMap<u32, SeenBuf>,
|
||||||
order: std::collections::VecDeque<u32>, // insertion order, for evict-oldest
|
order: std::collections::VecDeque<u32>, // insertion order, for evict-oldest
|
||||||
epoch: u32,
|
epoch: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(all(target_os = "linux", feature = "drm"))]
|
#[cfg(all(target_os = "linux", feature = "drm"))]
|
||||||
@@ -1852,7 +1860,22 @@ fn drm_udev_listener() {
|
|||||||
// datagram is truncated by `recv` and simply re-enumerates on the next matching event.
|
// datagram is truncated by `recv` and simply re-enumerates on the next matching event.
|
||||||
let mut buf = [0u8; 8192];
|
let mut buf = [0u8; 8192];
|
||||||
loop {
|
loop {
|
||||||
let n = unsafe { libc::recv(sock, buf.as_mut_ptr() as *mut libc::c_void, buf.len(), 0) };
|
// recvmsg (not recv) so the source address is available: bound to the kernel-uevent multicast
|
||||||
|
// group, a genuine uevent comes from the kernel (source nl_pid == 0) via a multicast group
|
||||||
|
// (nl_groups != 0). A local unprivileged process could otherwise UNICAST a spoofed
|
||||||
|
// "change@.../drm/..." datagram to this root listener and drive it to re-enumerate at will;
|
||||||
|
// dropping any non-kernel/non-multicast source closes that.
|
||||||
|
let mut src: libc::sockaddr_nl = unsafe { std::mem::zeroed() };
|
||||||
|
let mut iov = libc::iovec {
|
||||||
|
iov_base: buf.as_mut_ptr() as *mut libc::c_void,
|
||||||
|
iov_len: buf.len(),
|
||||||
|
};
|
||||||
|
let mut mhdr: libc::msghdr = unsafe { std::mem::zeroed() };
|
||||||
|
mhdr.msg_name = &mut src as *mut libc::sockaddr_nl as *mut libc::c_void;
|
||||||
|
mhdr.msg_namelen = std::mem::size_of::<libc::sockaddr_nl>() as libc::socklen_t;
|
||||||
|
mhdr.msg_iov = &mut iov;
|
||||||
|
mhdr.msg_iovlen = 1;
|
||||||
|
let n = unsafe { libc::recvmsg(sock, &mut mhdr, 0) };
|
||||||
if n <= 0 {
|
if n <= 0 {
|
||||||
let err = std::io::Error::last_os_error();
|
let err = std::io::Error::last_os_error();
|
||||||
if n < 0 && err.kind() == std::io::ErrorKind::Interrupted {
|
if n < 0 && err.kind() == std::io::ErrorKind::Interrupted {
|
||||||
@@ -1861,6 +1884,14 @@ fn drm_udev_listener() {
|
|||||||
log::info!("drm: udev uevent recv ended ({err}); hotplug refresh stopped");
|
log::info!("drm: udev uevent recv ended ({err}); hotplug refresh stopped");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
// Trust only a kernel-originated (nl_pid == 0), multicast-delivered (nl_groups != 0) datagram
|
||||||
|
// with a full source address; drop a unicast or user-spoofed message.
|
||||||
|
if (mhdr.msg_namelen as usize) < std::mem::size_of::<libc::sockaddr_nl>()
|
||||||
|
|| src.nl_pid != 0
|
||||||
|
|| src.nl_groups == 0
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if !uevent_is_drm_change(&buf[..n as usize]) {
|
if !uevent_is_drm_change(&buf[..n as usize]) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -2010,8 +2041,9 @@ async fn handle_drm_conn(stream: Connection) -> ResultType<()> {
|
|||||||
|
|
||||||
// worker -> task: display list, frames, cursor (bounded = backpressure).
|
// worker -> task: display list, frames, cursor (bounded = backpressure).
|
||||||
let (frame_tx, mut frame_rx) = tokio::sync::mpsc::channel::<DrmProducerMsg>(2);
|
let (frame_tx, mut frame_rx) = tokio::sync::mpsc::channel::<DrmProducerMsg>(2);
|
||||||
// task -> worker: the chosen CRTC, sent once after the client's DrmStart.
|
// task -> worker: the chosen CRTC + whether the consumer needs the CPU path, sent once after the
|
||||||
let (crtc_tx, crtc_rx) = std::sync::mpsc::channel::<u32>();
|
// client's DrmStart.
|
||||||
|
let (crtc_tx, crtc_rx) = std::sync::mpsc::channel::<(u32, bool)>();
|
||||||
let stop = Arc::new(AtomicBool::new(false));
|
let stop = Arc::new(AtomicBool::new(false));
|
||||||
let _stop_guard = DrmStopGuard(stop.clone());
|
let _stop_guard = DrmStopGuard(stop.clone());
|
||||||
let worker_stop = stop.clone();
|
let worker_stop = stop.clone();
|
||||||
@@ -2031,9 +2063,9 @@ async fn handle_drm_conn(stream: Connection) -> ResultType<()> {
|
|||||||
|
|
||||||
// Wait for the client to choose a display before streaming. `recv_msg_timeout2` gates only the
|
// Wait for the client to choose a display before streaming. `recv_msg_timeout2` gates only the
|
||||||
// wait for the first byte, so a timeout leaves the stream at a clean frame boundary.
|
// wait for the first byte, so a timeout leaves the stream at a clean frame boundary.
|
||||||
let display_idx = loop {
|
let (display_idx, need_cpu) = loop {
|
||||||
match conn.recv_msg_timeout2(10_000).await {
|
match conn.recv_msg_timeout2(10_000).await {
|
||||||
Some(Ok((Data::DrmStart { display }, _fd))) => break display,
|
Some(Ok((Data::DrmStart { display, need_cpu }, _fd))) => break (display, need_cpu),
|
||||||
Some(Ok((_, _fd))) => continue, // ignore unexpected messages; drop any stray fd
|
Some(Ok((_, _fd))) => continue, // ignore unexpected messages; drop any stray fd
|
||||||
Some(Err(e)) => return Err(e),
|
Some(Err(e)) => return Err(e),
|
||||||
None => return Ok(()), // timed out: client never chose a display
|
None => return Ok(()), // timed out: client never chose a display
|
||||||
@@ -2057,8 +2089,9 @@ async fn handle_drm_conn(stream: Connection) -> ResultType<()> {
|
|||||||
);
|
);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
// Hand the CRTC to the worker; an error means it already gave up (reader vanished).
|
// Hand the CRTC + the consumer's CPU-path request to the worker; an error means it already gave up
|
||||||
if crtc_tx.send(target_crtc).is_err() {
|
// (reader vanished).
|
||||||
|
if crtc_tx.send((target_crtc, need_cpu)).is_err() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2075,6 +2108,10 @@ async fn handle_drm_conn(stream: Connection) -> ResultType<()> {
|
|||||||
let mut seen_gen = DRM_DISPLAY_GENERATION.load(Ordering::Acquire);
|
let mut seen_gen = DRM_DISPLAY_GENERATION.load(Ordering::Acquire);
|
||||||
while let Some(msg) = frame_rx.recv().await {
|
while let Some(msg) = frame_rx.recv().await {
|
||||||
let gen = DRM_DISPLAY_GENERATION.load(Ordering::Acquire);
|
let gen = DRM_DISPLAY_GENERATION.load(Ordering::Acquire);
|
||||||
|
// Keep the ledger's epoch at the live generation so a hotplug/modeset (which may recycle an
|
||||||
|
// fb_id onto a new buffer) invalidates every cached buffer and forces a real fd on the next
|
||||||
|
// frame. Cheap (one field write) and only observable when DRM_FD_ELISION is enabled.
|
||||||
|
ledger.epoch = gen;
|
||||||
if gen != seen_gen {
|
if gen != seen_gen {
|
||||||
seen_gen = gen;
|
seen_gen = gen;
|
||||||
let fresh = DRM_DISPLAY_CACHE.lock().unwrap().clone();
|
let fresh = DRM_DISPLAY_CACHE.lock().unwrap().clone();
|
||||||
@@ -2134,7 +2171,7 @@ async fn handle_drm_conn(stream: Connection) -> ResultType<()> {
|
|||||||
#[cfg(all(target_os = "linux", feature = "drm"))]
|
#[cfg(all(target_os = "linux", feature = "drm"))]
|
||||||
fn drm_capture_worker(
|
fn drm_capture_worker(
|
||||||
frame_tx: tokio::sync::mpsc::Sender<DrmProducerMsg>,
|
frame_tx: tokio::sync::mpsc::Sender<DrmProducerMsg>,
|
||||||
crtc_rx: std::sync::mpsc::Receiver<u32>,
|
crtc_rx: std::sync::mpsc::Receiver<(u32, bool)>,
|
||||||
stop: std::sync::Arc<std::sync::atomic::AtomicBool>,
|
stop: std::sync::Arc<std::sync::atomic::AtomicBool>,
|
||||||
) {
|
) {
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
@@ -2170,8 +2207,9 @@ fn drm_capture_worker(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for the task to relay the client's chosen CRTC (Err => the task gave up / disconnected).
|
// Wait for the task to relay the client's chosen CRTC + CPU-path request (Err => the task gave up
|
||||||
let target_crtc = match crtc_rx.recv() {
|
// / disconnected).
|
||||||
|
let (target_crtc, need_cpu) = match crtc_rx.recv() {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(_) => return,
|
Err(_) => return,
|
||||||
};
|
};
|
||||||
@@ -2199,10 +2237,12 @@ fn drm_capture_worker(
|
|||||||
static DRM_CONN_EPOCH: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
|
static DRM_CONN_EPOCH: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
|
||||||
let conn_epoch = DRM_CONN_EPOCH.fetch_add(1, Ordering::Relaxed);
|
let conn_epoch = DRM_CONN_EPOCH.fetch_add(1, Ordering::Relaxed);
|
||||||
|
|
||||||
// Prefer the zero-copy split export (root does NO EGL / convert / copy). If the loaded libdrmtap
|
// Prefer the zero-copy split export (root does NO EGL / convert / copy). Fall back to the
|
||||||
// predates the split API, or grab_desc later reports ENOTSUP (no transferable dma-buf on this
|
// CPU-mapped path for this connection (pixels cross the wire) when: the loaded libdrmtap predates
|
||||||
// seat), fall back to the CPU-mapped path for this connection (pixels cross the wire).
|
// the split API, grab_desc later reports ENOTSUP (no transferable dma-buf on this seat), OR the
|
||||||
let mut use_dmabuf = reader.supports_grab_desc();
|
// consumer asked for the CPU path because it has no render-node convert context (need_cpu) — in
|
||||||
|
// that last case the dma-buf fd would be useless to it and the stream would be lost.
|
||||||
|
let mut use_dmabuf = reader.supports_grab_desc() && !need_cpu;
|
||||||
|
|
||||||
let mut last_cursor_id: u64 = 0;
|
let mut last_cursor_id: u64 = 0;
|
||||||
let mut stalled: u32 = 0;
|
let mut stalled: u32 = 0;
|
||||||
|
|||||||
@@ -249,25 +249,30 @@ async fn recv_thread(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if let Err(err) = conn.send_msg(&Data::DrmStart { display }, None).await {
|
// Open the unprivileged render-node convert context ONCE, on THIS thread, BEFORE the handshake; it
|
||||||
|
// is dropped on this same thread when the loop exits (its EGL state + import-once cache are
|
||||||
|
// thread-local). `None` means no usable render node (a locked-down seat, or an old `.so` without
|
||||||
|
// the split symbols): we then ask the service for the CPU-converted `DrmFrame` path via
|
||||||
|
// `need_cpu`, so a render-node-less seat still captures instead of the service streaming a dma-buf
|
||||||
|
// fd we cannot detile (which would lose the stream and force a PipeWire fallback nobody may be
|
||||||
|
// present to approve on an unattended seat).
|
||||||
|
let mut converter = RenderConverter::open_render();
|
||||||
|
let need_cpu = converter.is_none();
|
||||||
|
if need_cpu {
|
||||||
|
log::info!(
|
||||||
|
"drm: no render-node convert context (drmtap_open_render failed or old .so); \
|
||||||
|
requesting the CPU-converted frame path for this stream"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Err(err) = conn
|
||||||
|
.send_msg(&Data::DrmStart { display, need_cpu }, None)
|
||||||
|
.await
|
||||||
|
{
|
||||||
let _ = tx.send(Err(err));
|
let _ = tx.send(Err(err));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let _ = tx.send(Ok(displays));
|
let _ = tx.send(Ok(displays));
|
||||||
|
|
||||||
// Open the unprivileged render-node convert context ONCE, on THIS thread; it is dropped on this
|
|
||||||
// same thread when the loop exits (its EGL state + import-once cache are thread-local). `None`
|
|
||||||
// means no usable render node (a locked-down seat, or an old `.so` without the split symbols): the
|
|
||||||
// CPU-fallback `DrmFrame` path still works, but a `DrmFrameDmabuf` we cannot convert ends the
|
|
||||||
// stream so the caller falls back (PipeWire per-display).
|
|
||||||
let mut converter = RenderConverter::open_render();
|
|
||||||
if converter.is_none() {
|
|
||||||
log::info!(
|
|
||||||
"drm: no render-node convert context (drmtap_open_render failed or old .so); \
|
|
||||||
only the CPU-fallback frame path will work on this stream"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stream until stopped or the connection ends. Poll the header read with a short timeout (rather
|
// Stream until stopped or the connection ends. Poll the header read with a short timeout (rather
|
||||||
// than blocking indefinitely) so a dropped capturer re-checks `stop` and tears down promptly even
|
// than blocking indefinitely) so a dropped capturer re-checks `stop` and tears down promptly even
|
||||||
// when the producer has stalled (no frames arriving). A dma-buf frame carries its fd inline on the
|
// when the producer has stalled (no frames arriving). A dma-buf frame carries its fd inline on the
|
||||||
@@ -337,15 +342,33 @@ async fn recv_thread(
|
|||||||
}
|
}
|
||||||
// CPU-fallback path (old `.so` / no transferable dma-buf): the producer packed BGRA and
|
// CPU-fallback path (old `.so` / no transferable dma-buf): the producer packed BGRA and
|
||||||
// sent it over the wire after the header. Store it as-is (BGRA); no convert needed.
|
// sent it over the wire after the header. Store it as-is (BGRA); no convert needed.
|
||||||
Data::DrmFrame { width, height } => match conn.next_raw().await {
|
Data::DrmFrame { width, height } => {
|
||||||
Ok(raw) => {
|
// Reject degenerate geometry before it reaches the slot: `frame()` hands this to
|
||||||
let mut slot = shared.slot.lock().unwrap();
|
// PixelBuffer::new which derives the stride as `data.len() / height`, so height==0
|
||||||
slot.latest =
|
// would divide by zero, and a zero width is meaningless. Require the body to hold at
|
||||||
Some((width as usize, height as usize, Pixfmt::BGRA, raw.to_vec()));
|
// least width*height*4 BGRA bytes so a short body cannot misframe downstream.
|
||||||
shared.cv.notify_one();
|
if width == 0 || height == 0 {
|
||||||
|
break format!("cpu frame: degenerate geometry {width}x{height}");
|
||||||
}
|
}
|
||||||
Err(err) => break format!("frame body: {err}"),
|
let need = (width as usize)
|
||||||
},
|
.saturating_mul(height as usize)
|
||||||
|
.saturating_mul(4);
|
||||||
|
match conn.next_raw().await {
|
||||||
|
Ok(raw) => {
|
||||||
|
if raw.len() < need {
|
||||||
|
break format!(
|
||||||
|
"cpu frame: body {} bytes < {need} for {width}x{height}",
|
||||||
|
raw.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let mut slot = shared.slot.lock().unwrap();
|
||||||
|
slot.latest =
|
||||||
|
Some((width as usize, height as usize, Pixfmt::BGRA, raw.to_vec()));
|
||||||
|
shared.cv.notify_one();
|
||||||
|
}
|
||||||
|
Err(err) => break format!("frame body: {err}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
Data::DrmCursor {
|
Data::DrmCursor {
|
||||||
id,
|
id,
|
||||||
width,
|
width,
|
||||||
|
|||||||
@@ -228,7 +228,6 @@ pub(super) async fn check_init() -> ResultType<()> {
|
|||||||
}
|
}
|
||||||
log::debug!("Attempting to fix logical size with try_fix_logical_size()");
|
log::debug!("Attempting to fix logical size with try_fix_logical_size()");
|
||||||
try_fix_logical_size(&mut all);
|
try_fix_logical_size(&mut all);
|
||||||
*PIPEWIRE_INITIALIZED.write().unwrap() = true;
|
|
||||||
let num = all.len();
|
let num = all.len();
|
||||||
let primary = super::display_service::get_primary_2(&all);
|
let primary = super::display_service::get_primary_2(&all);
|
||||||
let mut displays = super::display_service::update_sync_displays(&all);
|
let mut displays = super::display_service::update_sync_displays(&all);
|
||||||
@@ -269,6 +268,12 @@ pub(super) async fn check_init() -> ResultType<()> {
|
|||||||
|
|
||||||
lock.insert(idx, cap_display_info as u64);
|
lock.insert(idx, cap_display_info as u64);
|
||||||
}
|
}
|
||||||
|
// Mark PipeWire initialized only AFTER every per-display capturer was created and
|
||||||
|
// stored. Setting it earlier meant a partial failure above (a `Capturer::new` error
|
||||||
|
// propagated by `?`) returned Err with the flag already true, so the next check_init
|
||||||
|
// saw "initialized", skipped re-init, and left CAP_DISPLAY_INFO empty (no capture).
|
||||||
|
// This matters more now that the per-display DRM->PipeWire fallback funnels through here.
|
||||||
|
*PIPEWIRE_INITIALIZED.write().unwrap() = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user