From df815d7309e138d63b55e730c6fe9e0c036ddc1c Mon Sep 17 00:00:00 2001 From: PyVoyager <230523196+PyVoyager@users.noreply.github.com> Date: Fri, 13 Nov 2026 03:07:15 +0100 Subject: [PATCH 1/3] fix(linux): multi-GPU VA-API render-node probing - Probe candidate DRM render nodes (/dev/dri/renderD128..135) with fallback in dmabuf_import.rs, encoder.rs, and pipeline_linux.rs for hybrid GPU systems. - Gate render node candidate discovery in encoder.rs to VA-API only. - Support OPENSCREEN_LINUX_RENDER_NODE override with automatic fallback. Split out of #1476. Refs #1340 --- crates/compositor/src/pipeline_linux.rs | 203 ++++++++++-------- .../pipewire-capture/src/dmabuf_import.rs | 136 ++++++++---- .../native/pipewire-capture/src/encoder.rs | 93 +++++--- 3 files changed, 267 insertions(+), 165 deletions(-) diff --git a/crates/compositor/src/pipeline_linux.rs b/crates/compositor/src/pipeline_linux.rs index edc9ea623d..2a7d843d7f 100644 --- a/crates/compositor/src/pipeline_linux.rs +++ b/crates/compositor/src/pipeline_linux.rs @@ -1330,39 +1330,29 @@ impl VaapiEncoder { /// manque : l'appelant retombe alors sur l'encodeur software. pub unsafe fn open(w: i32, h: i32, fps: i32, bit_rate: i64) -> Option { use crate::ffi::*; - let mut me = VaapiEncoder { - ctx: ptr::null_mut(), - drm_device: ptr::null_mut(), - va_device: ptr::null_mut(), - drm_frames: ptr::null_mut(), - va_frames: ptr::null_mut(), - graph: ptr::null_mut(), - vpp_in: ptr::null_mut(), - vpp_out: ptr::null_mut(), - w, - h, - }; // LE DEVICE DRM D'ABORD, PUIS VAAPI DERIVE DE LUI. L'ordre inverse // (VAAPI ouvert seul) rend ENOSYS sur radeonsi : le mapping veut les deux - // cotes d'un meme device. - let node = std::ffi::CString::new("/dev/dri/renderD128").ok()?; - if av_hwdevice_ctx_create( - &mut me.drm_device, - AVHWDeviceType::AV_HWDEVICE_TYPE_DRM, - node.as_ptr(), - ptr::null_mut(), - 0, - ) < 0 - { - return None; + // cotes d'un meme device. Sur les portables hybrides, la carte avec VAAPI + // peut etre sur renderD128 ou renderD129 : on honore OPENSCREEN_LINUX_RENDER_NODE + // puis on sonde les noeuds DRM disponibles. + let mut candidates: Vec = Vec::new(); + if let Ok(explicit) = std::env::var("OPENSCREEN_LINUX_RENDER_NODE") { + if !explicit.is_empty() { + candidates.push(explicit); + } } - if av_hwdevice_ctx_create_derived( - &mut me.va_device, - AVHWDeviceType::AV_HWDEVICE_TYPE_VAAPI, - me.drm_device, - 0, - ) < 0 - { + for idx in 128..=135 { + let p = format!("/dev/dri/renderD{idx}"); + if Path::new(&p).exists() && !candidates.contains(&p) { + candidates.push(p); + } + } + if candidates.is_empty() { + candidates.push("/dev/dri/renderD128".to_owned()); + } + let name = std::ffi::CString::new("h264_vaapi").ok()?; + let enc = avcodec_find_encoder_by_name(name.as_ptr()); + if enc.is_null() { return None; } // `initial_pool_size = 0` sur LES DEUX contextes : ils ne font @@ -1382,70 +1372,107 @@ impl VaapiEncoder { (*c).height = h; (*c).initial_pool_size = 0; if av_hwframe_ctx_init(frames) < 0 { + let mut f = frames; + av_buffer_unref(&mut f); return ptr::null_mut(); } frames }; - me.drm_frames = mk_frames(me.drm_device, AVPixelFormat::AV_PIX_FMT_DRM_PRIME); - me.va_frames = mk_frames(me.va_device, AVPixelFormat::AV_PIX_FMT_VAAPI); - if me.drm_frames.is_null() || me.va_frames.is_null() { - return None; - } - // Le pilote decide. Une chaine illisible compte comme « pas iHD » : on - // garde alors le chemin direct, celui d'avant la copie. - if va_vendor(me.va_device).is_some_and(|v| needs_vpp_copy(&v)) { - me.build_vpp()?; - } - let name = std::ffi::CString::new("h264_vaapi").ok()?; - let enc = avcodec_find_encoder_by_name(name.as_ptr()); - if enc.is_null() { - return None; - } - me.ctx = avcodec_alloc_context3(enc); - if me.ctx.is_null() { - return None; - } - (*me.ctx).width = w; - (*me.ctx).height = h; - (*me.ctx).pix_fmt = AVPixelFormat::AV_PIX_FMT_VAAPI as i32; - (*me.ctx).time_base = AVRational { num: 1, den: fps }; - (*me.ctx).framerate = AVRational { num: fps, den: 1 }; - (*me.ctx).bit_rate = bit_rate; - // MEME INTERVALLE D'IMAGES CLES QUE LE CHEMIN SOFTWARE, et pour la meme - // raison : deux secondes est le compromis choisi pour un fichier de - // sortie, il n'a pas a dependre de l'encodeur qui se trouve disponible. - // - // Sans cette ligne le resultat est correct A 60 FPS ET NULLE PART - // AILLEURS : `h264_vaapi` a un defaut de 120 frames (mesure), qui vaut - // deux secondes a 60 fps par coincidence. A 30 fps ca donnerait quatre - // secondes, a 120 fps une seule. Le defaut de `libopenh264` est -1, ce - // qui est un autre probleme encore (une seule image cle pour tout le - // fichier) traite dans `try_open`. - (*me.ctx).gop_size = (fps * 2).max(1); - // MP4 veut SPS/PPS dans l'extradata, pas repetes devant chaque image - // cle. Le chemin software le pose depuis toujours (`try_open`) ; l'avoir - // oublie ici produisait un fichier qui se lit quand meme, parce que le - // muxer recupere ce qu'il trouve — mais un lecteur qui se fie a - // `codecpar` seul aurait de quoi echouer. - (*me.ctx).flags |= AV_CODEC_FLAG_GLOBAL_HEADER as i32; - // La matrice des frames que `send_dmabuf` pose, celle de `yuv.wgsl`. - tag_export_color(me.ctx, AVColorSpace::AVCOL_SPC_SMPTE170M); - // Avec la copie, les surfaces de SORTIE du VPP, pas celles du mapping : - // c'est elles que l'encodeur recoit. - let out_frames = if me.graph.is_null() { - me.va_frames - } else { - av_buffersink_get_hw_frames_ctx(me.vpp_out) - }; - if out_frames.is_null() { - return None; - } - (*me.ctx).hw_frames_ctx = av_buffer_ref(out_frames); - if avcodec_open2(me.ctx, enc, ptr::null_mut()) < 0 { - return None; + for cand in candidates { + let Ok(node) = std::ffi::CString::new(cand) else { + continue; + }; + let mut me = VaapiEncoder { + ctx: ptr::null_mut(), + drm_device: ptr::null_mut(), + va_device: ptr::null_mut(), + drm_frames: ptr::null_mut(), + va_frames: ptr::null_mut(), + graph: ptr::null_mut(), + vpp_in: ptr::null_mut(), + vpp_out: ptr::null_mut(), + w, + h, + }; + if av_hwdevice_ctx_create( + &mut me.drm_device, + AVHWDeviceType::AV_HWDEVICE_TYPE_DRM, + node.as_ptr(), + ptr::null_mut(), + 0, + ) < 0 + { + continue; + } + if av_hwdevice_ctx_create_derived( + &mut me.va_device, + AVHWDeviceType::AV_HWDEVICE_TYPE_VAAPI, + me.drm_device, + 0, + ) < 0 + { + continue; + } + me.drm_frames = mk_frames(me.drm_device, AVPixelFormat::AV_PIX_FMT_DRM_PRIME); + me.va_frames = mk_frames(me.va_device, AVPixelFormat::AV_PIX_FMT_VAAPI); + if me.drm_frames.is_null() || me.va_frames.is_null() { + continue; + } + // Le pilote decide. Une chaine illisible compte comme « pas iHD » : on + // garde alors le chemin direct, celui d'avant la copie. + if va_vendor(me.va_device).is_some_and(|v| needs_vpp_copy(&v)) + && me.build_vpp().is_none() + { + continue; + } + + me.ctx = avcodec_alloc_context3(enc); + if me.ctx.is_null() { + continue; + } + (*me.ctx).width = w; + (*me.ctx).height = h; + (*me.ctx).pix_fmt = AVPixelFormat::AV_PIX_FMT_VAAPI as i32; + (*me.ctx).time_base = AVRational { num: 1, den: fps }; + (*me.ctx).framerate = AVRational { num: fps, den: 1 }; + (*me.ctx).bit_rate = bit_rate; + // MEME INTERVALLE D'IMAGES CLES QUE LE CHEMIN SOFTWARE, et pour la meme + // raison : deux secondes est le compromis choisi pour un fichier de + // sortie, il n'a pas a dependre de l'encodeur qui se trouve disponible. + // + // Sans cette ligne le resultat est correct A 60 FPS ET NULLE PART + // AILLEURS : `h264_vaapi` a un defaut de 120 frames (mesure), qui vaut + // deux secondes a 60 fps par coincidence. A 30 fps ca donnerait quatre + // secondes, a 120 fps une seule. Le defaut de `libopenh264` est -1, ce + // qui est un autre probleme encore (une seule image cle pour tout le + // fichier) traite dans `try_open`. + (*me.ctx).gop_size = (fps * 2).max(1); + // MP4 veut SPS/PPS dans l'extradata, pas repetes devant chaque image + // cle. Le chemin software le pose depuis toujours (`try_open`) ; l'avoir + // oublie ici produisait un fichier qui se lit quand meme, parce que le + // muxer recupere ce qu'il trouve — mais un lecteur qui se fie a + // `codecpar` seul aurait de quoi echouer. + (*me.ctx).flags |= AV_CODEC_FLAG_GLOBAL_HEADER as i32; + // La matrice des frames que `send_dmabuf` pose, celle de `yuv.wgsl`. + tag_export_color(me.ctx, AVColorSpace::AVCOL_SPC_SMPTE170M); + // Avec la copie, les surfaces de SORTIE du VPP, pas celles du mapping : + // c'est elles que l'encodeur recoit. + let out_frames = if me.graph.is_null() { + me.va_frames + } else { + av_buffersink_get_hw_frames_ctx(me.vpp_out) + }; + if out_frames.is_null() { + continue; + } + (*me.ctx).hw_frames_ctx = av_buffer_ref(out_frames); + if avcodec_open2(me.ctx, enc, ptr::null_mut()) < 0 { + continue; + } + return Some(me); } - Some(me) + None } /// Construit `buffer (VAAPI, surface mappee) -> scale_vaapi -> buffersink`. diff --git a/electron/native/pipewire-capture/src/dmabuf_import.rs b/electron/native/pipewire-capture/src/dmabuf_import.rs index 59e900a84a..e1cdf23476 100644 --- a/electron/native/pipewire-capture/src/dmabuf_import.rs +++ b/electron/native/pipewire-capture/src/dmabuf_import.rs @@ -107,6 +107,14 @@ impl DmabufImporter { /// incoming dmabuf size (the whole stream); `out` is the recorded size — equal /// to `src` for a monitor, or the window's crop rectangle for a window (the /// graph then crops the source region down to it, on the GPU). + /// + /// Probes `/dev/dri/renderD128` first (unchanged default for working single-GPU + /// systems) and falls back across `/dev/dri/renderD129..renderD135` if DRM or + /// VAAPI setup fails. Because dmabuf mapping itself (`av_hwframe_map`) can only + /// be validated when frames arrive in [`Self::import`], hybrid-GPU hosts where + /// the default node opens VAAPI but drops imported compositor frames can set + /// `OPENSCREEN_LINUX_RENDER_NODE` as an optional workaround to select the + /// compositor's render node directly. pub fn new( src_width: i32, src_height: i32, @@ -119,21 +127,6 @@ impl DmabufImporter { // SAFETY: every pointer is checked before use and freed in Drop. unsafe { - let mut me = DmabufImporter { - src_width, - src_height, - out_width, - out_height, - sw_format, - va_device: ptr::null_mut(), - drm_device: ptr::null_mut(), - drm_frames: ptr::null_mut(), - va_map_frames: ptr::null_mut(), - graph: ptr::null_mut(), - buffersrc_ctx: ptr::null_mut(), - buffersink_ctx: ptr::null_mut(), - }; - // Order matters: create the DRM device on the render node FIRST, then // derive VAAPI from it. The reverse (DRM derived from VAAPI) returns // ENOSYS on radeonsi — VAAPI knows how to open on a DRM fd, but not the @@ -142,40 +135,91 @@ impl DmabufImporter { // /dev/dri/renderD128 is the default, but on hybrid graphics the // compositor may render on a different node — mapping a dmabuf from the // wrong GPU then fails on every frame, with `available()` still passing. - // Honour an override so such a machine can point at the right node - // without a rebuild; the single-GPU default is unchanged. - let node_path = std::env::var("OPENSCREEN_LINUX_RENDER_NODE") - .unwrap_or_else(|_| "/dev/dri/renderD128".to_owned()); - let node = std::ffi::CString::new(node_path) - .map_err(|_| "OPENSCREEN_LINUX_RENDER_NODE has an interior NUL byte".to_owned())?; - let created = ff::av_hwdevice_ctx_create( - &mut me.drm_device, - ff::AV_HWDEVICE_TYPE_DRM, - node.as_ptr(), - ptr::null_mut(), - 0, - ); - if created < 0 { - return Err(format!("av_hwdevice_ctx_create(DRM): {}", ff::err_to_string(created))); + // Honour `OPENSCREEN_LINUX_RENDER_NODE` as an optional workaround when + // frames are dropped on the default node; single-GPU systems with a + // working `renderD128` do not need to set it. + let mut candidates: Vec = Vec::new(); + if let Ok(explicit) = std::env::var("OPENSCREEN_LINUX_RENDER_NODE") { + if !explicit.is_empty() { + candidates.push(explicit); + } } - - let derived = ff::av_hwdevice_ctx_create_derived( - &mut me.va_device, - ff::AV_HWDEVICE_TYPE_VAAPI, - me.drm_device, - 0, - ); - if derived < 0 { - return Err(format!( - "av_hwdevice_ctx_create_derived(VAAPI): {}", - ff::err_to_string(derived) - )); + for idx in 128..=135 { + let p = format!("/dev/dri/renderD{idx}"); + if std::path::Path::new(&p).exists() && !candidates.contains(&p) { + candidates.push(p); + } + } + if candidates.is_empty() { + candidates.push("/dev/dri/renderD128".to_owned()); } - me.drm_frames = me.alloc_frames(me.drm_device, ff::AV_PIX_FMT_DRM_PRIME)?; - me.va_map_frames = me.alloc_frames(me.va_device, ff::AV_PIX_FMT_VAAPI)?; - me.build_graph()?; - Ok(me) + let mut last_err = String::from("no DRM/VAAPI render node available"); + for node_path in candidates { + let node = std::ffi::CString::new(node_path) + .map_err(|_| "OPENSCREEN_LINUX_RENDER_NODE has an interior NUL byte".to_owned())?; + let mut me = DmabufImporter { + src_width, + src_height, + out_width, + out_height, + sw_format, + va_device: ptr::null_mut(), + drm_device: ptr::null_mut(), + drm_frames: ptr::null_mut(), + va_map_frames: ptr::null_mut(), + graph: ptr::null_mut(), + buffersrc_ctx: ptr::null_mut(), + buffersink_ctx: ptr::null_mut(), + }; + + let created = ff::av_hwdevice_ctx_create( + &mut me.drm_device, + ff::AV_HWDEVICE_TYPE_DRM, + node.as_ptr(), + ptr::null_mut(), + 0, + ); + if created < 0 { + last_err = format!("av_hwdevice_ctx_create(DRM): {}", ff::err_to_string(created)); + continue; + } + + let derived = ff::av_hwdevice_ctx_create_derived( + &mut me.va_device, + ff::AV_HWDEVICE_TYPE_VAAPI, + me.drm_device, + 0, + ); + if derived < 0 { + last_err = format!( + "av_hwdevice_ctx_create_derived(VAAPI): {}", + ff::err_to_string(derived) + ); + continue; + } + + me.drm_frames = match me.alloc_frames(me.drm_device, ff::AV_PIX_FMT_DRM_PRIME) { + Ok(frames) => frames, + Err(err) => { + last_err = err; + continue; + } + }; + me.va_map_frames = match me.alloc_frames(me.va_device, ff::AV_PIX_FMT_VAAPI) { + Ok(frames) => frames, + Err(err) => { + last_err = err; + continue; + } + }; + if let Err(err) = me.build_graph() { + last_err = err; + continue; + } + return Ok(me); + } + Err(last_err) } } diff --git a/electron/native/pipewire-capture/src/encoder.rs b/electron/native/pipewire-capture/src/encoder.rs index 9ffbcc9c5e..79dfddf343 100644 --- a/electron/native/pipewire-capture/src/encoder.rs +++ b/electron/native/pipewire-capture/src/encoder.rs @@ -576,43 +576,74 @@ impl VideoEncoder { return Ok(()); } - let created = ff::av_hwdevice_ctx_create( - &mut self.hw_device, - device_type, - ptr::null(), - ptr::null_mut(), - 0, - ); - if created < 0 { - return Err(format!( - "av_hwdevice_ctx_create: {}", - ff::err_to_string(created) - )); + let mut candidate_nodes: Vec> = Vec::new(); + if device_type == ff::AV_HWDEVICE_TYPE_VAAPI { + if let Ok(explicit) = std::env::var("OPENSCREEN_LINUX_RENDER_NODE") { + if let Ok(cstr) = CString::new(explicit) { + candidate_nodes.push(Some(cstr)); + } + } + candidate_nodes.push(None); + for idx in 128..=135 { + let path = format!("/dev/dri/renderD{idx}"); + if Path::new(&path).exists() { + if let Ok(cstr) = CString::new(path) { + candidate_nodes.push(Some(cstr)); + } + } + } + } else { + candidate_nodes.push(None); } if device_type == ff::AV_HWDEVICE_TYPE_VULKAN && !vulkan_can_encode_h264(self.hw_device) { return Err("the Vulkan device has no H.264 encode queue".to_owned()); } - self.hw_frames = ff::av_hwframe_ctx_alloc(self.hw_device); - if self.hw_frames.is_null() { - return Err("av_hwframe_ctx_alloc returned null".to_owned()); + let mut last_err = String::from("no hardware device available"); + let mut ready = false; + for node in &candidate_nodes { + let device_ptr = node.as_ref().map_or(ptr::null(), |c| c.as_ptr()); + let created = ff::av_hwdevice_ctx_create( + &mut self.hw_device, + device_type, + device_ptr, + ptr::null_mut(), + 0, + ); + if created < 0 { + last_err = format!("av_hwdevice_ctx_create: {}", ff::err_to_string(created)); + continue; + } + + self.hw_frames = ff::av_hwframe_ctx_alloc(self.hw_device); + if self.hw_frames.is_null() { + last_err = "av_hwframe_ctx_alloc returned null".to_owned(); + ff::av_buffer_unref(&mut self.hw_device); + continue; + } + let frames_ctx = (*self.hw_frames).data as *mut ff::AVHWFramesContext; + (*frames_ctx).format = self.backend.codec_pixel_format(); + (*frames_ctx).sw_format = self.backend.upload_format(); + (*frames_ctx).width = params.width; + (*frames_ctx).height = params.height; + // Enough surfaces that the encoder can hold a few references while we + // are filling the next one. Too small a pool stalls; too large just + // wastes VRAM. + (*frames_ctx).initial_pool_size = 8; + + let initialised = ff::av_hwframe_ctx_init(self.hw_frames); + if initialised < 0 { + last_err = format!("av_hwframe_ctx_init: {}", ff::err_to_string(initialised)); + ff::av_buffer_unref(&mut self.hw_frames); + ff::av_buffer_unref(&mut self.hw_device); + continue; + } + + ready = true; + break; } - let frames_ctx = (*self.hw_frames).data as *mut ff::AVHWFramesContext; - (*frames_ctx).format = self.backend.codec_pixel_format(); - (*frames_ctx).sw_format = self.backend.upload_format(); - (*frames_ctx).width = params.width; - (*frames_ctx).height = params.height; - // Enough surfaces that the encoder can hold a few references while we - // are filling the next one. Too small a pool stalls; too large just - // wastes VRAM. - (*frames_ctx).initial_pool_size = 8; - - let initialised = ff::av_hwframe_ctx_init(self.hw_frames); - if initialised < 0 { - return Err(format!( - "av_hwframe_ctx_init: {}", - ff::err_to_string(initialised) - )); + if !ready { + return Err(last_err); } (*self.codec_ctx).hw_frames_ctx = ff::av_buffer_ref(self.hw_frames); From 0adf2522b2707f6d4b9744dc7b867c2983e3c164 Mon Sep 17 00:00:00 2001 From: EtienneLescot Date: Fri, 13 Nov 2026 03:07:47 +0100 Subject: [PATCH 2/3] fix(linux): walk one render-node list in the importer and the encoder The dmabuf importer and the VA-API encoder now try the nodes in the same order, from one function, so they settle on the same GPU. The encoder no longer tries the libva default between the override and the scan; it falls back to it only when no node exists. Check the Vulkan H.264 queue after the device is created, not before: the check read a null device. Refs #1340 --- .../pipewire-capture/src/dmabuf_import.rs | 13 +---- .../native/pipewire-capture/src/encoder.rs | 52 ++++++++++++------- 2 files changed, 35 insertions(+), 30 deletions(-) diff --git a/electron/native/pipewire-capture/src/dmabuf_import.rs b/electron/native/pipewire-capture/src/dmabuf_import.rs index e1cdf23476..8f618b5fa4 100644 --- a/electron/native/pipewire-capture/src/dmabuf_import.rs +++ b/electron/native/pipewire-capture/src/dmabuf_import.rs @@ -138,18 +138,7 @@ impl DmabufImporter { // Honour `OPENSCREEN_LINUX_RENDER_NODE` as an optional workaround when // frames are dropped on the default node; single-GPU systems with a // working `renderD128` do not need to set it. - let mut candidates: Vec = Vec::new(); - if let Ok(explicit) = std::env::var("OPENSCREEN_LINUX_RENDER_NODE") { - if !explicit.is_empty() { - candidates.push(explicit); - } - } - for idx in 128..=135 { - let p = format!("/dev/dri/renderD{idx}"); - if std::path::Path::new(&p).exists() && !candidates.contains(&p) { - candidates.push(p); - } - } + let mut candidates = crate::encoder::render_node_candidates(); if candidates.is_empty() { candidates.push("/dev/dri/renderD128".to_owned()); } diff --git a/electron/native/pipewire-capture/src/encoder.rs b/electron/native/pipewire-capture/src/encoder.rs index 79dfddf343..2ec93f32fa 100644 --- a/electron/native/pipewire-capture/src/encoder.rs +++ b/electron/native/pipewire-capture/src/encoder.rs @@ -158,6 +158,25 @@ pub fn prepare_environment() { add_flag("RADV_EXPERIMENTAL", "video_encode"); } +/// DRM render nodes to try for VA-API, in order: `OPENSCREEN_LINUX_RENDER_NODE` +/// when set, then every `/dev/dri/renderD128..=135` that exists. The dmabuf +/// importer and the encoder both walk this list, so they pick the same node. +pub(crate) fn render_node_candidates() -> Vec { + let mut nodes = Vec::new(); + if let Ok(explicit) = std::env::var("OPENSCREEN_LINUX_RENDER_NODE") { + if !explicit.is_empty() { + nodes.push(explicit); + } + } + for idx in 128..=135 { + let path = format!("/dev/dri/renderD{idx}"); + if Path::new(&path).exists() && !nodes.contains(&path) { + nodes.push(path); + } + } + nodes +} + /// True when libva is new enough that libavutil's VA-API code will not abort. /// /// See the module header. This deliberately checks the SYMBOL rather than a @@ -576,27 +595,18 @@ impl VideoEncoder { return Ok(()); } + // VA-API tries the nodes in the same order as the dmabuf importer, so + // both settle on the same GPU. Other backends use their own default. let mut candidate_nodes: Vec> = Vec::new(); if device_type == ff::AV_HWDEVICE_TYPE_VAAPI { - if let Ok(explicit) = std::env::var("OPENSCREEN_LINUX_RENDER_NODE") { - if let Ok(cstr) = CString::new(explicit) { - candidate_nodes.push(Some(cstr)); - } - } - candidate_nodes.push(None); - for idx in 128..=135 { - let path = format!("/dev/dri/renderD{idx}"); - if Path::new(&path).exists() { - if let Ok(cstr) = CString::new(path) { - candidate_nodes.push(Some(cstr)); - } - } - } - } else { - candidate_nodes.push(None); + candidate_nodes.extend( + render_node_candidates() + .into_iter() + .filter_map(|node| CString::new(node).ok().map(Some)), + ); } - if device_type == ff::AV_HWDEVICE_TYPE_VULKAN && !vulkan_can_encode_h264(self.hw_device) { - return Err("the Vulkan device has no H.264 encode queue".to_owned()); + if candidate_nodes.is_empty() { + candidate_nodes.push(None); } let mut last_err = String::from("no hardware device available"); @@ -614,6 +624,12 @@ impl VideoEncoder { last_err = format!("av_hwdevice_ctx_create: {}", ff::err_to_string(created)); continue; } + if device_type == ff::AV_HWDEVICE_TYPE_VULKAN + && !vulkan_can_encode_h264(self.hw_device) + { + ff::av_buffer_unref(&mut self.hw_device); + return Err("the Vulkan device has no H.264 encode queue".to_owned()); + } self.hw_frames = ff::av_hwframe_ctx_alloc(self.hw_device); if self.hw_frames.is_null() { From 415a3b8e74b754d89ebc5d3371733bcaaab91edd Mon Sep 17 00:00:00 2001 From: EtienneLescot Date: Fri, 13 Nov 2026 03:51:57 +0100 Subject: [PATCH 3/3] fix(compositor): encode on the render node wgpu renders on VaapiEncoder::open imports dmabufs exported by the compositor's wgpu device, so it must open on that GPU. Read its render node from VK_EXT_physical_device_drm (Gpu::render_node) and try it right after OPENSCREEN_LINUX_RENDER_NODE. The renderD128..135 scan now runs only when the driver does not expose the node. Refs #1340 --- crates/compositor/src/d3d_linux.rs | 32 ++++++++++ crates/compositor/src/pipeline_linux.rs | 81 ++++++++++++++++++------- 2 files changed, 90 insertions(+), 23 deletions(-) diff --git a/crates/compositor/src/d3d_linux.rs b/crates/compositor/src/d3d_linux.rs index 1b11fadb64..2900892c36 100644 --- a/crates/compositor/src/d3d_linux.rs +++ b/crates/compositor/src/d3d_linux.rs @@ -271,6 +271,38 @@ impl Gpu { probe() } + /// Le noeud DRM render du GPU sur lequel ce device rend (`/dev/dri/renderDN`), + /// lu par `VK_EXT_physical_device_drm`. C'est celui ou l'encodeur VAAPI doit + /// importer les dmabufs du compositeur. `None` si le pilote ne l'expose pas + /// (lavapipe, instance Vulkan 1.0) : l'appelant balaie alors les noeuds. + pub fn render_node(&self) -> Option { + use ash::vk; + unsafe { + self.device.as_hal::(|dev| { + let dev = dev?; + let shared = dev.shared_instance(); + // `vkGetPhysicalDeviceProperties2` est du coeur 1.1. + if shared.instance_api_version() < vk::API_VERSION_1_1 { + return None; + } + let instance = shared.raw_instance(); + let phys = dev.raw_physical_device(); + let exts = instance.enumerate_device_extension_properties(phys).ok()?; + if !exts + .iter() + .any(|e| e.extension_name_as_c_str() == Ok(ash::ext::physical_device_drm::NAME)) + { + return None; + } + let mut drm = vk::PhysicalDeviceDrmPropertiesEXT::default(); + let mut props = vk::PhysicalDeviceProperties2::default().push_next(&mut drm); + instance.get_physical_device_properties2(phys, &mut props); + (drm.has_render == vk::TRUE) + .then(|| format!("/dev/dri/renderD{}", drm.render_minor)) + }) + } + } + /// Le device de TOUS les tests unitaires du crate : cree une fois par `create_auto`, /// jamais detruit. `None` (et le test se saute) sans adaptateur Vulkan. /// diff --git a/crates/compositor/src/pipeline_linux.rs b/crates/compositor/src/pipeline_linux.rs index 2a7d843d7f..0c90da36eb 100644 --- a/crates/compositor/src/pipeline_linux.rs +++ b/crates/compositor/src/pipeline_linux.rs @@ -931,7 +931,9 @@ fn run_multi_inner( Some(name) => name.contains("vaapi"), }; let hw = if hw_allowed && matches!(params.codec, ExportCodec::H264) { - unsafe { VaapiEncoder::open(out_w as i32, out_h as i32, out_fps, bit_rate) } + unsafe { + VaapiEncoder::open(out_w as i32, out_h as i32, out_fps, bit_rate, gpu.render_node()) + } .and_then(|v| { // PLUS DE TAMPONS QUE L'ENCODEUR N'A DE LATENCE. Deux suffisaient // pour recouvrir composition et encodage, mais pas pour la @@ -1325,31 +1327,51 @@ unsafe extern "C" fn drm_desc_free(_opaque: *mut std::ffi::c_void, data: *mut u8 crate::ffi::av_free(data as *mut std::ffi::c_void); } +/// Les noeuds DRM a essayer, dans l'ordre. `OPENSCREEN_LINUX_RENDER_NODE` d'abord +/// (echappatoire), puis le noeud du GPU de wgpu. Le balayage de `renderD128..135` +/// ne sert que si ce noeud est illisible : sur un portable hybride, un autre GPU +/// ouvrirait VAAPI sans encombre puis refuserait chaque dmabuf du compositeur. +fn render_node_candidates(explicit: Option, adapter: Option) -> Vec { + let mut nodes: Vec = explicit.into_iter().filter(|n| !n.is_empty()).collect(); + if let Some(node) = adapter { + if !nodes.contains(&node) { + nodes.push(node); + } + return nodes; + } + for idx in 128..=135 { + let p = format!("/dev/dri/renderD{idx}"); + if Path::new(&p).exists() && !nodes.contains(&p) { + nodes.push(p); + } + } + if nodes.is_empty() { + nodes.push("/dev/dri/renderD128".to_owned()); + } + nodes +} + impl VaapiEncoder { /// Ouvre la chaine DRM -> VAAPI -> `h264_vaapi`. `None` si quoi que ce soit /// manque : l'appelant retombe alors sur l'encodeur software. - pub unsafe fn open(w: i32, h: i32, fps: i32, bit_rate: i64) -> Option { + /// + /// `adapter_node` est le noeud DRM du GPU sur lequel wgpu rend + /// (`Gpu::render_node`) : les dmabufs viennent de lui. + pub unsafe fn open( + w: i32, + h: i32, + fps: i32, + bit_rate: i64, + adapter_node: Option, + ) -> Option { use crate::ffi::*; // LE DEVICE DRM D'ABORD, PUIS VAAPI DERIVE DE LUI. L'ordre inverse // (VAAPI ouvert seul) rend ENOSYS sur radeonsi : le mapping veut les deux - // cotes d'un meme device. Sur les portables hybrides, la carte avec VAAPI - // peut etre sur renderD128 ou renderD129 : on honore OPENSCREEN_LINUX_RENDER_NODE - // puis on sonde les noeuds DRM disponibles. - let mut candidates: Vec = Vec::new(); - if let Ok(explicit) = std::env::var("OPENSCREEN_LINUX_RENDER_NODE") { - if !explicit.is_empty() { - candidates.push(explicit); - } - } - for idx in 128..=135 { - let p = format!("/dev/dri/renderD{idx}"); - if Path::new(&p).exists() && !candidates.contains(&p) { - candidates.push(p); - } - } - if candidates.is_empty() { - candidates.push("/dev/dri/renderD128".to_owned()); - } + // cotes d'un meme device. + let candidates = render_node_candidates( + std::env::var("OPENSCREEN_LINUX_RENDER_NODE").ok(), + adapter_node, + ); let name = std::ffi::CString::new("h264_vaapi").ok()?; let enc = avcodec_find_encoder_by_name(name.as_ptr()); if enc.is_null() { @@ -1836,7 +1858,7 @@ mod vaapi_tests { gpu.device.poll(wgpu::Maintain::Wait); unsafe { - let Some(mut enc) = VaapiEncoder::open(w, h, 60, 4_000_000) else { + let Some(mut enc) = VaapiEncoder::open(w, h, 60, 4_000_000, gpu.render_node()) else { eprintln!("h264_vaapi indisponible — test saute"); return; }; @@ -1893,7 +1915,7 @@ mod vaapi_tests { gpu.device.poll(wgpu::Maintain::Wait); unsafe { - let Some(mut enc) = VaapiEncoder::open(w as i32, h as i32, 30, 8_000_000) else { + let Some(mut enc) = VaapiEncoder::open(w as i32, h as i32, 30, 8_000_000, gpu.render_node()) else { eprintln!("h264_vaapi indisponible — test saute"); return; }; @@ -1978,12 +2000,25 @@ mod vaapi_tests { } } + /// Le noeud du GPU de wgpu remplace le balayage ; l'echappatoire passe devant. + #[test] + fn le_noeud_du_gpu_remplace_le_balayage() { + let gpu = Some("/dev/dri/renderD129".to_owned()); + assert_eq!(render_node_candidates(None, gpu.clone()), ["/dev/dri/renderD129"]); + assert_eq!( + render_node_candidates(Some("/dev/dri/renderD130".to_owned()), gpu.clone()), + ["/dev/dri/renderD130", "/dev/dri/renderD129"] + ); + assert_eq!(render_node_candidates(Some(String::new()), gpu), ["/dev/dri/renderD129"]); + assert!(!render_node_candidates(None, None).is_empty()); + } + /// Sur une vraie machine : la chaine du pilote se lit, et elle choisit le /// chemin que l'encodeur a pris. Se saute sans VAAPI, comme les autres. #[test] fn le_pilote_choisit_le_chemin() { unsafe { - let Some(enc) = VaapiEncoder::open(640, 480, 30, 4_000_000) else { + let Some(enc) = VaapiEncoder::open(640, 480, 30, 4_000_000, None) else { eprintln!("h264_vaapi indisponible — test saute"); return; };