Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions crates/compositor/src/d3d_linux.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> {
use ash::vk;
unsafe {
self.device.as_hal::<wgpu_hal::api::Vulkan, _, _>(|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.
///
Expand Down
248 changes: 155 additions & 93 deletions crates/compositor/src/pipeline_linux.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1325,44 +1327,54 @@ 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<String>, adapter: Option<String>) -> Vec<String> {
let mut nodes: Vec<String> = 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<VaapiEncoder> {
///
/// `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<String>,
) -> Option<VaapiEncoder> {
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;
}
if av_hwdevice_ctx_create_derived(
&mut me.va_device,
AVHWDeviceType::AV_HWDEVICE_TYPE_VAAPI,
me.drm_device,
0,
) < 0
{
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() {
return None;
}
// `initial_pool_size = 0` sur LES DEUX contextes : ils ne font
Expand All @@ -1382,70 +1394,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`.
Expand Down Expand Up @@ -1809,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;
};
Expand Down Expand Up @@ -1866,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;
};
Expand Down Expand Up @@ -1951,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;
};
Expand Down
Loading
Loading