drm: convert each display on the GPU that exports it

The unprivileged converter opened its render context with
drmtap_open_render(NULL), letting libdrmtap auto-select. On a multi-GPU host
that can land on a different GPU than the one driving the display, and importing
a scanout across vendors can fail permanently on an incompatible tiling
modifier.

The service already knows the exporting device, so it now names its render node
(drmtap_render_node, libdrmtap 0.4.15) in each DrmDisplayInfo, and the consumer
opens the converter on that node. The field is serde(default) and empty means
auto-select, so a service and a server from mismatched builds still interoperate
and a pre-0.4.15 .so degrades to exactly the previous behaviour. The path is
realpath-gated to /dev/dri before it is opened, the same gate the capture device
gets, since it arrives over IPC. When the named node cannot be opened the
converter returns None and the existing need_cpu fallback runs the convert on the
exporting GPU service-side, which is the most correct place for it anyway.

Added a wire-compat test that a pre-render_node DrmDisplayInfo payload still
decodes (empty node) and a current one round-trips the node.
This commit is contained in:
Mariano Abad
2026-07-24 01:00:17 -03:00
parent bc02676ee7
commit 919f3b5097
5 changed files with 135 additions and 15 deletions

View File

@@ -295,11 +295,22 @@ async fn recv_thread(
// Skip opening the render-node converter entirely when this display previously failed to convert
// (multi-GPU render-node mismatch): request the CPU path so the service does the conversion on the
// exporting GPU. Otherwise open it normally and fall back to CPU only if no render node is usable.
// Bind the converter to the GPU that EXPORTS this display's scanout, which the service
// named in the display list. Auto-selection can land on a different GPU on a multi-GPU
// host, and importing a scanout across vendors can fail on an incompatible tiling
// 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();
let mut converter = if force_cpu {
None
} else {
RenderConverter::open_render()
RenderConverter::open_render(Some(render_node.as_str()))
};
let need_cpu = converter.is_none();
if need_cpu {