mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-05 15:41:23 +03:00
drm: do not guess the exporting GPU when the host has several render nodes
The converter binds the render node the service names for a display, and falls back to auto-selection when that name is empty. An empty name is what an older libdrmtap produces: the service resolves it with drmtap_render_node, which only exists since 0.4.15, and rustdesk dlopens libdrmtap.so.0 by soname, so the runtime library can be older than the one the build was pinned to. Auto-selecting is not safe there. On a single-SoC multi-device host the wrong choice does not fail: a Jetson Orin exports the scanout from nvidia-drm while the first render node belongs to tegra, and importing the scanout on the tegra node SUCCEEDS and yields corrupted pixels. There is no convert error, so the prefer-cpu bit never learns anything and the stream simply looks broken with a clean log. Request the CPU-converted path instead whenever the exporter is unnamed and the host exposes more than one render node: the service converts on the device it already has open, which is correct by construction. Hosts with a single render node have nothing to pick wrong and keep the dma-buf path untouched. Verified on a Jetson Orin Nano, the two-device host: with a libdrmtap that lacks drmtap_render_node the capture used to come through visibly corrupted, and now falls back to the cpu path and renders correctly. With 0.4.15 the service names renderD129 and the dma-buf path is used as before.
This commit is contained in:
@@ -122,6 +122,24 @@ fn drm_clear_prefer_cpu() {
|
||||
DRM_PREFER_CPU.store(0, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
// How many render nodes this host exposes. Only used to tell "there is nothing to pick wrong" (one
|
||||
// node) from "auto-selection is a guess" (several), so a single readdir per stream start is enough
|
||||
// and it is deliberately not cached: a GPU can be bound or unbound while the service runs. On an
|
||||
// unreadable /dev/dri we report 0 and keep the previous auto-select behavior, because a seat that
|
||||
// cannot even list /dev/dri will fail to open a render node anyway and land on the CPU path.
|
||||
fn render_node_count() -> usize {
|
||||
std::fs::read_dir("/dev/dri").map_or(0, |entries| {
|
||||
entries
|
||||
.filter_map(|e| e.ok())
|
||||
.filter(|e| {
|
||||
e.file_name()
|
||||
.to_str()
|
||||
.map_or(false, |n| n.starts_with("renderD"))
|
||||
})
|
||||
.count()
|
||||
})
|
||||
}
|
||||
|
||||
// Coalesce the uinput-range refresh that every DrmDisplaysChanged triggers. A multi-monitor hotplug
|
||||
// delivers that message once PER captured display (one recv_thread each), but the uinput desktop rect
|
||||
// is global and idempotent, so one refresh serves the whole burst. UINPUT_REFRESH_GEN records the
|
||||
@@ -301,12 +319,22 @@ async fn recv_thread(
|
||||
// modifier. Empty (an older service or a device with no render node) means auto-select,
|
||||
// exactly as before. Every display of one device carries the same node, so a display
|
||||
// index that does not resolve still gets the right answer from the first entry.
|
||||
let force_cpu = drm_prefer_cpu(display);
|
||||
let render_node = displays
|
||||
.get(display.max(0) as usize)
|
||||
.or_else(|| displays.first())
|
||||
.map(|d| d.render_node.clone())
|
||||
.unwrap_or_default();
|
||||
// An unnamed exporter on a host that HAS more than one render node is not safe to auto-select.
|
||||
// The failure is silent: on a single-SoC multi-device host (a Jetson exports the scanout from
|
||||
// nvidia-drm while the first render node belongs to tegra) importing the other device's scanout
|
||||
// SUCCEEDS and yields corrupted pixels, so there is no convert error for the prefer-cpu bit above
|
||||
// to learn from - the stream just looks broken. The node is empty when the service ran against a
|
||||
// libdrmtap without `drmtap_render_node` (we dlopen by soname, so the runtime .so can be older
|
||||
// than the one this was built against). Ask for the CPU path instead: the service converts on the
|
||||
// device it already has open, which is correct by construction. Single-render-node hosts (the
|
||||
// common case) keep the dma-buf fast path untouched.
|
||||
let ambiguous_gpu = render_node.is_empty() && render_node_count() > 1;
|
||||
let force_cpu = drm_prefer_cpu(display) || ambiguous_gpu;
|
||||
let mut converter = if force_cpu {
|
||||
None
|
||||
} else {
|
||||
@@ -316,7 +344,10 @@ async fn recv_thread(
|
||||
if need_cpu {
|
||||
log::info!(
|
||||
"drm: requesting the CPU-converted frame path for display {display} ({})",
|
||||
if force_cpu {
|
||||
if ambiguous_gpu {
|
||||
"the service did not name the exporting GPU and this host has several render nodes; \
|
||||
auto-selecting one can import the scanout on the wrong device and silently corrupt it"
|
||||
} else if force_cpu {
|
||||
"a prior consumer convert failed, e.g. multi-GPU render-node mismatch"
|
||||
} else {
|
||||
"no render-node convert context: drmtap_open_render failed or old .so"
|
||||
|
||||
Reference in New Issue
Block a user