feat: add KWin/KDE Plasma screen capture via xdg-desktop-portal ScreenCast + PipeWire

Add a second capture backend for compositors without wlr-screencopy
(KWin, GNOME, etc.) using the xdg-desktop-portal ScreenCast interface
and PipeWire DMA-BUF streaming.

New files:
- src/backend_detect.rs: auto-detect wlr-screencopy vs portal backend
- src/cap_portal.rs: Portal session setup + PipeWire DMA-BUF thread
- src/state_portal.rs: StatePortal encoder pipeline (DMA-BUF → VAAPI)

Changes:
- Cargo.toml: add ashpd 0.13, tokio 1, pipewire 0.9, libspa 0.9,
  crossbeam-channel 0.5
- src/args.rs: add --backend CLI flag
- src/avhw.rs: extract create_encoder() from inline State code
- src/main.rs: route to portal or wlr-screencopy based on backend
- src/state.rs: fix params.destroy() on dup failure, cleanup
  in_flight_surface on copy fail, use create_encoder()
- tests/integration_test.rs: add --backend flag tests
This commit is contained in:
dailz
2026-05-11 08:49:08 +08:00
parent 2972216a02
commit d7fbb5256c
12 changed files with 2198 additions and 79 deletions

View File

@@ -465,6 +465,8 @@ impl<S: CaptureSource> State<S> {
let fd_dup = unsafe { libc::dup(obj.fd) };
if fd_dup < 0 {
tracing::error!("failed to dup dma-buf fd");
// wayland-client does not auto-destroy params on Drop.
params.destroy();
self.errored = true;
return;
}
@@ -553,8 +555,27 @@ impl<S: CaptureSource> State<S> {
}
}
pub fn on_copy_fail(&mut self) {
pub fn on_copy_fail(&mut self)
where
S::Frame: Default,
{
tracing::error!("compositor copy failed");
let taken = mem::replace(&mut self.in_flight_surface, InFlightSurface::None);
match taken {
InFlightSurface::CopyQueued {
buffer,
frame,
..
} => {
drop(buffer);
if let EncConstructionStage::Streaming { cap, .. } = &mut self.stage {
cap.on_done_with_frame(frame);
}
}
other => {
self.in_flight_surface = other;
}
}
self.errored = true;
}
@@ -576,25 +597,19 @@ impl<S: CaptureSource> State<S> {
};
let (output_info, output, cap, screencopy_manager, dmabuf) = stage_data;
let drm_path = self.resolve_drm_path();
let bitrate = self.args.bitrate.unwrap_or_else(|| {
let fps = self.args.fps as u64;
2 * (width as u64) * (height as u64) * fps / 100
});
let gop_size = self.args.gop_size.unwrap_or(self.args.fps);
let fps = self.args.fps;
let (enc_w, enc_h) =
transpose_if_transform_transposed(output_info.transform, width as i32, height as i32);
let enc = match EncState::new(
let bitrate = self.args.bitrate.unwrap_or_else(|| {
2 * (width as u64) * (height as u64) * (fps as u64) / 100
});
let enc = match crate::avhw::create_encoder(
&drm_path,
Path::new(&self.args.output),
width,
height,
enc_w as u32,
enc_h as u32,
bitrate,
gop_size,
fps,
output_info.transform,
self.args.bitrate,
self.args.gop_size,
) {
Ok(enc) => enc,
Err(e) => {
@@ -1175,21 +1190,24 @@ impl<S: CaptureSource> Dispatch<ZwpLinuxBufferParamsV1, ()> for State<S> {
}
BufferParamsEvent::Failed => {
tracing::error!("DMA-BUF buffer creation failed");
state.errored = true;
match mem::replace(&mut state.in_flight_surface, InFlightSurface::None) {
let taken = mem::replace(&mut state.in_flight_surface, InFlightSurface::None);
match taken {
InFlightSurface::CopyQueued {
surface: _,
drm_map: _,
frame: _,
buffer,
frame,
..
} => {
drop(buffer);
if let EncConstructionStage::Streaming { cap, .. } = &mut state.stage {
cap.on_done_with_frame(frame);
}
}
other => {
state.in_flight_surface = other;
}
}
proxy.destroy();
state.errored = true;
}
_ => {}
}